wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
validation.hpp
Go to the documentation of this file.
1#pragma once
2
17
18#include <meta>
19
20#include <algorithm>
21#include <concepts>
22#include <cstddef>
23#include <cstdint>
24#include <format>
25#include <limits>
26#include <string>
27#include <string_view>
28#include <type_traits>
29#include <vector>
30
31#include <welder/vocabulary.hpp>
32
34#include <wowlib/core/error.hpp>
36
37namespace wowlib::formats {
38 enum class [[
39 =welder::weld,
40 =welder::doc(
41 "How a validation finding affects the file's fitness for the "
42 "client.")
43 ]] ValidationSeverity : std::uint8_t {
44 Warning [[=welder::doc(
45 "Suspicious, but real client files ship it; the file "
46 "loads.")]],
47 Error [[=welder::doc("The client would misread or crash on a file written "
48 "like this.")]]
49 };
50
51 struct [[
52 =welder::weld,
53 =welder::doc(
54 "One validate() finding: where it is, how bad it is, and what "
55 "is wrong.")
57 [[=welder::doc(
58 "Whether the client would misread the file (error) or merely "
59 "find it unusual (warning).")]]
61
62 [[=welder::doc(R"(Member path from the validated entity, e.g.
63 "groups[3].body.indices".)")]]
64 std::string path;
65
66 [[=welder::doc("What is wrong, with the numbers involved.")]]
67 std::string message;
68
69 bool operator==(const ValidationIssue&) const = default;
70 };
71
72 class [[
73 =welder::weld,
74 =welder::doc(R"(
75 Everything one validate() pass found, in member order. Validation is
76 linter-style — it never stops at the first finding, so a broken entity
77 reports every violated contract at once. A report is ok() when it holds
78 no errors; warnings mark states real client files ship.)")
80 public:
85 static constexpr std::size_t MaxFindings = 1000;
86
90 entity-level findings; nesting walkers prefix it).
91 @param message what is wrong, with the numbers involved. */
92 [[=welder::mark::exclude]]
93 void add(ValidationSeverity severity, std::string path, std::string message) {
94 if (full()) {
95 _truncated = true;
96 return;
97 }
98 _issues.push_back({severity, std::move(path), std::move(message)});
99 }
100
103 [[nodiscard]] [[=welder::mark::exclude]]
104 bool full() const { return _issues.size() >= MaxFindings; }
105
106 [[nodiscard]]
107 [[=welder::getter,
108 =welder::doc("Whether findings were dropped because the report hit its "
109 "size cap.")]]
110 bool truncated() const { return _truncated; }
115 [[=welder::mark::exclude]]
116 void addError(std::string path, std::string message) {
117 add(ValidationSeverity::Error, std::move(path), std::move(message));
118 }
119
123 [[=welder::mark::exclude]]
124 void addWarning(std::string path, std::string message) {
125 add(ValidationSeverity::Warning, std::move(path), std::move(message));
126 }
127
128 [[nodiscard]]
129 [[=welder::getter,
130 =welder::doc("Whether the entity is fit to write: no error-severity "
131 "findings (warnings do not fail a report).")]]
132 bool ok() const {
133 return std::ranges::none_of(_issues, [](const ValidationIssue& issue) {
134 return issue.severity == ValidationSeverity::Error;
135 });
136 }
137
138 [[nodiscard]]
139 [[=welder::getter, =welder::doc("The number of error-severity findings.")]]
140 std::size_t errorCount() const {
141 return static_cast<std::size_t>(
142 std::ranges::count(_issues, ValidationSeverity::Error, &ValidationIssue::severity));
143 }
144
145 [[nodiscard]]
146 [[=welder::getter, =welder::doc("The number of warning-severity findings.")]
147 ]
148 std::size_t warningCount() const {
149 return _issues.size() - errorCount();
150 }
151
152 [[nodiscard]]
153 [[=welder::getter,
154 =welder::doc("Every finding, in the order the walk recorded them.")]]
155 const std::vector<ValidationIssue>& issues() const { return _issues; }
156
157 [[nodiscard]]
158 [[=welder::getter,
159 =welder::doc("The total finding count (errors and warnings).")]]
160 std::size_t size() const { return _issues.size(); }
161
169 [[=welder::mark::exclude]]
170 void prefixFrom(std::size_t mark, std::string_view prefix) {
171 for (std::size_t i = mark; i < _issues.size(); ++i) {
172 ValidationIssue& issue = _issues[i];
173 issue.path = issue.path.empty()
174 ? std::string{prefix}
175 : issue.path.front() == '['
176 ? std::format("{}{}", prefix, issue.path)
177 : std::format("{}.{}", prefix, issue.path);
178 }
179 }
180
186 [[nodiscard]] [[=welder::mark::exclude]]
187 Result<void> toResult() const {
188 if (ok())
189 return {};
190 constexpr std::size_t maxListed = 8;
191 std::string message =
192 std::format(
193 "entity fails validation with {} error(s), {} warning(s){}:",
194 errorCount(),
195 warningCount(), _truncated ? " (report truncated)" : "");
196 std::size_t listed = 0;
197 for (const ValidationIssue& issue : _issues) {
198 if (listed == maxListed) {
199 message += std::format("\n ... and {} more",
200 _issues.size() - listed);
201 break;
202 }
203 message += std::format("\n {}: {}: {}",
204 issue.severity == ValidationSeverity::Error
205 ? "error"
206 : "warning",
207 issue.path, issue.message);
208 ++listed;
209 }
210 return makeError(ErrorCode::InvalidEntityState, std::move(message));
211 }
212
213 bool operator==(const ValidationReport&) const = default;
214
215 private:
216 std::vector<ValidationIssue> _issues;
217 bool _truncated = false;
218 };
219
222 (chunked files, M2 offset blocks, ADT tiles and map chunks alike). */
223 template <typename E>
224 concept VersionedEntity = requires {
225 { E::Version } -> std::convertible_to<ClientVersion>;
226 };
227
228 namespace detail {
231 template <typename T>
232 concept ValidatableSequence = requires(const T& value) {
233 typename T::value_type;
234 { value.size() } -> std::convertible_to<std::size_t>;
235 value[std::size_t{0}];
236 };
237
243 template <typename T>
244 concept SlotSequence = ValidatableSequence<T> && requires {
245 { T::ValidationSlots } -> std::convertible_to<bool>;
246 } && T::ValidationSlots;
247
250 template <typename T>
251 concept HasValidateExtra = requires(const T& value,
252 ValidationReport& report) {
253 value.validateExtra(report);
254 };
255
257 consteval bool hasValidationAnnotation(std::meta::info member) {
258 for (std::meta::info spec :
259 {
263 ^^IndexesSpec,
268 })
269 if (!std::meta::annotations_of_with_type(member, spec).empty())
270 return true;
271 return false;
272 }
273
278
286 template <typename T>
287 consteval bool hasValidationContent() {
288 if constexpr (HasValidateExtra<T>)
289 return true;
290 else if constexpr (std::is_trivially_copyable_v<T>)
291 return false;
292 else if constexpr (ValidatableSequence<T>)
294 else if constexpr (std::is_class_v<T> && !isStdType<T>()) {
295 bool any = false;
296 template for (constexpr auto m : membersOf<T>()) {
298 any = true;
299 else if (hasValidationContent<typename [:std::meta::type_of(m):]>())
300 any = true;
301 }
302 return any;
303 }
304 else
305 return false;
306 }
307
308
315 @param report the report findings land in. */
316 template <typename Values>
317 void validateIndexElements(const Values& values,
318 std::size_t targetCount,
319 std::string_view member,
320 std::string_view target,
321 ValidationReport& report) {
322 constexpr std::size_t maxReported = 8;
323 std::size_t bad = 0;
324 for (std::size_t i = 0; i < values.size(); ++i)
325 if (static_cast<std::size_t>(values[i]) >= targetCount)
326 if (++bad <= maxReported)
327 report.addError(std::format("{}[{}]", member, i),
328 std::format(
329 "index {} out of range: {} holds {} element(s)",
330 values[i], target, targetCount));
331 if (bad > maxReported)
332 report.addError(std::string{member},
333 std::format(
334 "... and {} more out-of-range indices into {}",
335 bad - maxReported, target));
336 }
337
343 template <std::integral T>
344 constexpr bool isNoIndex(T value) {
345 if constexpr (std::is_signed_v < T >)
346 return value < 0;
347 else
348 return value == std::numeric_limits < T > ::max();
349 }
350
354
359 template <typename Values>
360 void validateOptionalIndexElements(const Values& values,
361 std::size_t targetCount,
362 std::string_view member,
363 std::string_view target,
364 ValidationReport& report) {
365 constexpr std::size_t maxReported = 8;
366 std::size_t bad = 0;
367 for (std::size_t i = 0; i < values.size(); ++i) {
368 if (isNoIndex(values[i]))
369 continue;
370 if (static_cast<std::size_t>(values[i]) >= targetCount)
371 if (++bad <= maxReported)
372 report.addError(std::format("{}[{}]", member, i),
373 std::format(
374 "index {} out of range: {} holds {} element(s)",
375 values[i], target, targetCount));
376 }
377 if (bad > maxReported)
378 report.addError(std::string{member},
379 std::format(
380 "... and {} more out-of-range indices into {}",
381 bad - maxReported, target));
382 }
383
391 @param report the report findings land in. */
392 inline void validateCountMatches(std::size_t count,
393 std::uint32_t scale,
394 std::size_t targetCount,
395 std::string_view member,
396 std::string_view target,
397 ValidationReport& report) {
398 if (count == 0 || count * scale == targetCount)
399 return;
400 report.addError(std::string{member},
401 scale == 1
402 ? std::format("count {} != {} count {}", count, target,
403 targetCount)
404 : std::format("count {} x {} != {} count {}", count,
405 scale, target,
406 targetCount));
407 }
408
409 template <ClientVersion V, typename T>
410 void validateValue(const T& value, ValidationReport& report);
411
419 template <ClientVersion V, typename T>
420 void validateMembers(const T& value, ValidationReport& report) {
421 static constexpr auto Members = membersOf<T>();
422 template for (constexpr auto m : Members) {
423 if constexpr (versionActive<V, m>()) {
424 constexpr const char* ident = std::define_static_string(
425 std::meta::identifier_of(m));
426 using M = [:std::meta::type_of(m):];
427 const M& member = value.[:m:];
428
429 if constexpr (constexpr auto ev = annotation<ExpectedValueSpec, m>()
430 ; ev.has_value()) {
431 static_assert(std::is_integral_v<M>,
432 "expected_value applies to integral members");
433 if (static_cast<std::uint64_t>(member) != ev->value)
434 report.addError(
435 ident,
436 std::format("value {} != required {}", member, ev->value));
437 }
438
439 if constexpr (annotation<NonemptySpec, m>().has_value()) {
440 static_assert(requires { member.empty(); },
441 "nonempty applies to members with observable emptiness")
442 ;
443 if (member.empty())
444 report.addError(ident, "must not be empty");
445 }
446
447 if constexpr (constexpr auto ce = annotation<CountExactlySpec, m>();
448 ce.has_value()) {
449 static_assert(ValidatableSequence<M>,
450 "count_exactly applies to sequence members");
451 if (!member.empty() && member.size() != ce->count)
452 report.addError(ident, std::format(
453 "count {} != required {}", member.size(),
454 ce->count));
455 }
456
457 if constexpr (constexpr auto cmo = annotation<
459 cmo.has_value()) {
460 static_assert(ValidatableSequence<M>,
461 "count_multiple_of applies to sequence members");
462 if (!member.empty() && member.size() % cmo->divisor != 0)
463 report.addError(ident, std::format(
464 "count {} is not a multiple of {}",
465 member.size(), cmo->divisor));
466 }
467
468 if constexpr (constexpr auto cm = annotation<CountMatchesSpec, m>();
469 cm.has_value()) {
470 constexpr auto sibling = memberNamed<T>(cm->view());
471 static_assert(sibling != std::meta::info{},
472 "count_matches names no member of this entity");
473 using S = [:std::meta::type_of(sibling):];
474 static_assert(ValidatableSequence<S>,
475 "count_matches sibling must be a sequence");
476 static_assert(ValidatableSequence<M>,
477 "count_matches applies to sequence members");
478 const S& target = value.[:sibling:];
479 if constexpr (SlotSequence<M>) {
480 // each occurrence is a complete array of its own: the MOTV
481 // texcoord sets each hold one entry per MOVT vertex
482 for (std::size_t slot = 0; slot < member.size(); ++slot)
483 validateCountMatches(member[slot].size(), cm->scale,
484 target.size(),
485 std::format("{}[{}]", ident, slot),
486 cm->view(), report);
487 }
488 else {
489 validateCountMatches(member.size(), cm->scale, target.size(),
490 ident, cm->view(),
491 report);
492 // Parallel nested arrays pair element-wise too (a WotLK+ M2 track
493 // holds one timestamp array and one value array per sequence).
494 // Only a std::vector element counts as nesting — a fixed-extent
495 // element (a per-vertex std::array of bone indices) is one VALUE,
496 // however sequence-shaped it looks.
499 for (std::size_t i = 0; i < member.size() && i < target.size();
500 ++i)
501 if (member[i].size() != target[i].size())
502 report.addError(std::format("{}[{}]", ident, i),
503 std::format(
504 "count {} != {}[{}] count {}",
505 member[i].size(),
506 cm->view(), i, target[i].size()));
507 }
508 }
509
510 if constexpr (constexpr auto ix = annotation<IndexesSpec, m>(); ix.
511 has_value()) {
512 static_assert(
513 IsVectorV<M> && std::is_integral_v < typename M::value_type >,
514 "indexes applies to integral vector members");
515 constexpr auto sibling = memberNamed<T>(ix->view());
516 static_assert(sibling != std::meta::info{},
517 "indexes names no member of this entity");
518 using S = [:std::meta::type_of(sibling):];
519 static_assert(ValidatableSequence<S>,
520 "indexes sibling must be a sequence");
521 validateIndexElements(member, value.[:sibling:].size(), ident,
522 ix->view(), report);
523 }
524
525 if constexpr (constexpr auto ixo = annotation<
527 ixo.has_value()) {
528 static_assert(
529 IsVectorV<M> && std::is_integral_v < typename M::value_type >,
530 "indexes_optional applies to integral vector members");
531 constexpr auto sibling = memberNamed<T>(ixo->view());
532 static_assert(sibling != std::meta::info{},
533 "indexes_optional names no member of this entity");
534 using S = [:std::meta::type_of(sibling):];
535 static_assert(ValidatableSequence<S>,
536 "indexes_optional sibling must be a sequence");
537 validateOptionalIndexElements(member, value.[:sibling:].size(),
538 ident, ixo->view(),
539 report);
540 }
542 // descend: nested entities, records, and sequences of either
543 if constexpr (hasValidationContent<M>()) {
545 for (std::size_t i = 0; i < member.size() && !report.full(); ++
546 i) {
547 const std::size_t mark = report.size();
548 validateValue<V>(member[i], report);
549 report.prefixFrom(mark, std::format("{}[{}]", ident, i));
550 }
551 }
552 else {
553 const std::size_t mark = report.size();
554 validateValue<V>(member, report);
555 report.prefixFrom(mark, ident);
556 }
557 }
558 }
559 }
560 }
561
562
568 @param report the report findings land in. */
569 template <ClientVersion V, typename T>
570 void validateValue(const T& value, ValidationReport& report) {
571 if constexpr (!hasValidationContent<T>())
572 return;
573 else if constexpr (ValidatableSequence<T> && !HasValidateExtra<T>) {
574 for (std::size_t i = 0; i < value.size() && !report.full(); ++i) {
575 const std::size_t mark = report.size();
576 validateValue<V>(value[i], report);
577 report.prefixFrom(mark, std::format("[{}]", i));
578 }
579 }
580 else {
581 if constexpr (VersionedEntity<T>)
582 validateMembers<T::Version>(value, report);
583 else
584 validateMembers<V>(value, report);
585 // the imperative complement: record-interior and flag/presence
586 // contracts the annotations cannot express
587 if constexpr (HasValidateExtra<T>)
588 value.validateExtra(report);
589 }
590 }
591
596 template <VersionedEntity E>
597 void validateEntity(const E& entity, ValidationReport& report) {
598 validateValue<E::Version>(entity, report);
599 }
600 }
601}
void addWarning(std::string path, std::string message)
Record a warning finding (see add()).
std::size_t errorCount() const
The number of error-severity findings.
bool truncated() const
Whether findings were dropped because the report hit its size cap.
static constexpr std::size_t MaxFindings
The most findings one report holds.
void add(ValidationSeverity severity, std::string path, std::string message)
Record a finding, unless the report is already full().
bool ok() const
Whether the entity is fit to write: no error-severity findings (warnings do not fail a report).
void addError(std::string path, std::string message)
Record an error finding (see add()).
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
An entity the walker can gate by client version: anything carrying the static constexpr ClientVersion...
A type carrying the imperative half of its contracts (see the validateExtra hook).
A container of independent SLOTS rather than one logical array — a chunk that may appear several time...
A member the walker can iterate element-wise: std::vector, std::array, Repeated<> and std::string all...
The reflection helpers every format engine shares: reading a member's annotations,...
The error-handling vocabulary: ErrorCode, Error and the Result<T> alias every fallible wowlib operati...
consteval std::optional< Spec > annotation()
The first annotation of type Spec on reflected member M, if any.
consteval bool hasValidationContent()
Whether validating a T can produce anything — it declares a validateExtra hook, carries an annotated ...
consteval bool isStdType()
Whether T is a standard-library type (see nestedInStd).
consteval bool hasValidationAnnotation(std::meta::info member)
Whether member carries any validation annotation.
void validateEntity(const E &entity, ValidationReport &report)
Validate a whole entity — the engine behind every validate() method; see ChunkedFile::validate() for ...
void validateMembers(const T &value, ValidationReport &report)
Walk value's annotation-declared contracts, gating members by client version V and descending into ev...
void validateValue(const T &value, ValidationReport &report)
Validate one value of any shape: a versioned entity (gated by its OWN version), a plain record (gated...
consteval bool versionActive()
Whether member M participates for entity version V, per its since/until annotations (absent bounds me...
constexpr bool IsVectorV
Trait: is T a std::vector (an array-chunk member)?
void validateOptionalIndexElements(const Values &values, std::size_t targetCount, std::string_view member, std::string_view target, ValidationReport &report)
Report every non-sentinel element of values that is not a valid index into a targetCount-element targ...
consteval auto membersOf()
The reflected member list of E, public bases flattened in (see collectMembers).
void validateCountMatches(std::size_t count, std::uint32_t scale, std::size_t targetCount, std::string_view member, std::string_view target, ValidationReport &report)
Apply one member's countMatches contract against its resolved sibling count (see the annotation): eng...
constexpr bool isNoIndex(T value)
Whether value is the client's "no reference" sentinel for an index element: negative in a signed look...
consteval std::meta::info memberNamed(std::string_view name)
The reflected member of E named name (public bases flattened, like membersOf), or the null reflection...
void validateIndexElements(const Values &values, std::size_t targetCount, std::string_view member, std::string_view target, ValidationReport &report)
Report every element of values that is not a valid index into a targetCount-element target,...
ValidationSeverity
How a validation finding affects the file's fitness for the client.
@ Warning
Suspicious, but real client files ship it; the file loads.
@ Error
The client would misread or crash on a file written like this.
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ InvalidEntityState
An entity's members disagree (e.g.
Definition error.hpp:40
A wowlib operation failure: a machine-readable code, a human-readable message, and the originating na...
Definition error.hpp:63
One validate() finding: where it is, how bad it is, and what is wrong.
bool operator==(const ValidationIssue &) const =default
std::string message
What is wrong, with the numbers involved.
ValidationSeverity severity
Whether the client would misread the file (error) or merely find it unusual (warning).
Stored form of countExactly: the member's element count is fixed by the format (an ADT map chunk's 14...
Stored form of countMatches: when the member is engaged, its element count times scale must equal the...
Stored form of countMultipleOf: the member's element count must be a multiple of divisor.
Stored form of expectedValue: the (integral) data member must hold exactly this value (format-version...
Stored form of indexesInRoot: every element of the (integral) vector member must be a valid index int...
Stored form of indexesOptional: like indexes, but the client's "no reference" sentinel (a negative va...
Stored form of indexes: every element of the (integral) vector member must be a valid index into the ...
Stored form of nonempty: the member must hold data for the file to be meaningful to the client,...