wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
chunked_file.hpp
Go to the documentation of this file.
1#pragma once
2
21
22#include <meta>
23
24#include <array>
25#include <cstddef>
26#include <cstdint>
27#include <cstring>
28#include <format>
29#include <optional>
30#include <span>
31#include <type_traits>
32#include <vector>
33
34#include <welder/vocabulary.hpp>
35
38#include <wowlib/core/error.hpp>
44
45namespace wowlib::formats {
47 struct UnknownChunk {
48 std::uint32_t fourcc = 0;
49 std::vector<std::byte> bytes;
50
51 bool operator==(const UnknownChunk&) const = default;
52 };
53
56 struct JournalEntry {
57 std::uint32_t fourcc = 0;
58
61 std::int32_t member = -1;
62
65 std::uint32_t occurrence = 0;
66
67 bool operator==(const JournalEntry&) const = default;
68 };
69
73 struct ChunkExtras {
74 [[=welder::mark::exclude]] std::vector<JournalEntry> journal;
75 [[=welder::mark::exclude]] std::vector<UnknownChunk> unknown;
76 [[=welder::mark::exclude]] std::vector<std::byte> trailing;
77
78 // excluded: the parameter type is this unwelded base, not the entity
79 [[=welder::mark::exclude]]
80 bool operator==(const ChunkExtras&) const = default;
81 };
82
89 template <typename Derived>
90 struct ChunkedFile : ChunkExtras {
91 [[=welder::doc("Deserialize file bytes into this entity, replacing its "
92 "contents. Unmodeled chunks are preserved so an unmodified "
93 "entity rewrites byte-for-byte.")]]
94 Result<void> read(std::span<const std::byte> data [[=welder::doc("the file bytes")]]);
95
96 [[nodiscard]]
97 [[=welder::doc("Serialize this entity."),
98 =welder::returns("the file bytes")]]
100
101 [[nodiscard]]
102 [[=welder::doc(R"(
103 Check the logical integrity contracts this file must satisfy to LOAD in
104 the client — companion-chunk counts, index ranges, flag/presence
105 coherence — which write() deliberately never enforces. Call it before
106 writing when you want to know the result will load. A file read from a
107 client and left unmodified reports no errors; warnings mark states real
108 client files ship.)"),
109 =welder::returns("every violated contract, in member order")]]
112 [[nodiscard]]
113 [[=welder::doc("Validate and raise on the first error instead of returning "
114 "a report — the assert-style face of validate()."),
115 =welder::returns("nothing; raises when validate() finds any error")]]
117
118 [[=welder::mark::exclude]]
119 bool operator==(const ChunkedFile&) const = default;
120 };
122 struct [[
123 =welder::weld,
124 =welder::doc(
125 "An unparsed chunk payload, preserved verbatim for round-trip. "
126 "Backs chunks wowlib keeps opaque — offset-based (MOTA, MDDL) "
127 "or undocumented (MPVD, MOMX).")
128 ]] ChunkBlob {
129 [[=welder::mark::exclude]] std::vector<std::byte> bytes;
130
133
134 [[=welder::mark::exclude]]
135 Result<void> read(std::span<const std::byte> payload) {
136 bytes.assign(payload.begin(), payload.end());
137 return {};
138 }
139
141 @param out the destination buffer (appended, not cleared).
142 @return nothing; emitting raw bytes cannot fail. */
143 [[=welder::mark::exclude]]
144 Result<void> write(FileBuffer& out) const {
145 out.insert(out.end(), bytes.begin(), bytes.end());
146 return {};
147 }
148
149 [[nodiscard]]
150 [[=welder::getter, =welder::doc("Whether the payload holds any bytes.")]]
151 bool empty() const {
152 return bytes.empty();
153 }
155 [[=welder::getter, =welder::doc("The payload size in bytes.")]]
156 std::size_t size() const {
157 return bytes.size();
159
160 bool operator==(const ChunkBlob&) const = default;
161 };
162
167 annotation). */
168 template <typename T, std::size_t N>
169 class Repeated {
170 public:
171
173 using value_type = T;
174
178 static constexpr bool ValidationSlots = true;
182 T* push() {
183 if (_count == N) return nullptr;
184 return &_slots[_count++];
185 }
186
188 [[nodiscard]] std::size_t size() const { return _count; }
189
191 [[nodiscard]] bool empty() const { return _count == 0; }
194 [[nodiscard]] bool full() const { return _count == N; }
197 void clear() {
198 _slots = {};
199 _count = 0;
200 }
203 [[nodiscard]] static constexpr std::size_t capacity() { return N; }
207 [[nodiscard]] T& operator[](std::size_t i) { return _slots[i]; }
208
210 [[nodiscard]] const T& operator[](std::size_t i) const { return _slots[i]; }
211
213 [[nodiscard]] T* begin() { return _slots.data(); }
214
216 [[nodiscard]] T* end() { return _slots.data() + _count; }
217
219 [[nodiscard]] const T* begin() const { return _slots.data(); }
220
222 [[nodiscard]] const T* end() const { return _slots.data() + _count; }
224 private:
225 std::array<T, N> _slots{};
226 std::size_t _count = 0;
227 };
228
231 template <typename E> concept ChunkedEntity = std::derived_from<E, ChunkExtras> && requires {
232 { E::Version } -> std::convertible_to<ClientVersion>;
233 };
234
236 it the raw payload on read and the output buffer on write (StringBlock,
237 ChunkBlob). */
238 template <typename T> concept SelfSerializing = requires(T& value,
239 const T& constant,
240 std::span<const std::byte> payload,
242 { value.read(payload) } -> std::same_as<Result<void>>; { constant.write(out) } -> std::same_as<Result<void>>; {
243 constant.empty()
244 } -> std::convertible_to<bool>;
245 };
246
247 namespace detail {
248 /** Trait: is @a T a Repeated<U, N> (a repeats-annotated member)? Exposes
249 the element type and capacity when it is. */
250 template <typename T>
251 struct RepeatedTraits {
252 static constexpr bool Value = false;
253 };
254
255 template <typename U, std::size_t N>
256 struct RepeatedTraits<Repeated<U, N>> {
257 static constexpr bool Value = true;
258 using Element = U;
259 static constexpr std::size_t Capacity = N;
260 };
263 consteval std::uint32_t chunkMagicOf(std::meta::info member) {
264 auto anns = std::meta::annotations_of_with_type(member, ^^ChunkSpec);
265 return anns.empty() ? 0u : std::meta::extract<ChunkSpec>(anns[0]).magic;
266 }
267
275 template <typename E>
276 consteval auto writeOrder() {
277 constexpr auto members = membersOf<E>();
278 std::vector<std::size_t> order;
279 if constexpr (requires { E::ChunkOrder; }) {
280 for (std::uint32_t magic : E::ChunkOrder)
281 for (std::size_t i = 0; i < members.size(); ++i)
282 if (chunkMagicOf(members[i]) == magic) {
283 order.push_back(i);
284 break;
285 }
286 std::size_t chunkMembers = 0;
287 for (std::size_t i = 0; i < members.size(); ++i) chunkMembers += (chunkMagicOf(members[i]) != 0);
288 if (order.size() != chunkMembers) throw "chunk_order must list every chunk member exactly once";
290 else
291 for (std::size_t i = 0; i < members.size(); ++i) order.push_back(i);
292 return std::define_static_array(order);
293 }
294
297
304 inline std::unexpected<Error> chunkError(ErrorCode code,
305 std::uint32_t fourcc,
306 std::size_t offset,
307 std::string_view what,
309 return makeError(code, std::format("chunk {} at offset {:#x}: {}", fourccToString(fourcc, endian), offset,
310 what));
311 }
312
318 template <typename E>
320 if constexpr (requires { E::UnknownFourccEndian; }) return E::UnknownFourccEndian;
321 else return FourCCEndian::Reversed;
322 }
323
324 // --- single-value (chunk payload <-> member) transfer ---------------------
325
334 template <typename M>
335 Result<void> readValue(M& dst,
336 std::span<const std::byte> payload,
337 std::uint32_t fourcc,
338 std::size_t offset,
339 FourCCEndian endian);
340
346 template <typename M>
347 Result<void> writeValue(const M& src, FileBuffer& out);
348
349 template <ChunkedEntity E>
350 bool entityEngaged(const E& entity);
351
353 Required chunks always write (clients ship size-0 required chunks);
354 optional ones only when they hold data. A plain data-chunk member's
355 engagement is not observable — it always writes, so optional plain
356 data chunks must not be declared (single-record optional chunks like
357 MDAL are modeled as vectors instead).
358 @param value the member value.
359 @return whether the member holds observable data. */
360 template <typename M>
361 bool engaged(const M& value) {
362 if constexpr (IsVectorV<M> || RepeatedTraits<M>::Value || SelfSerializing<M>) return !value.empty();
363 else if constexpr (ChunkedEntity<M>) return entityEngaged(value);
364 else return true;
365 }
366
368 class ChunkWriter {
369 public:
371 explicit ChunkWriter(FileBuffer& out) : _out{out} {}
376 std::size_t beginChunk(std::uint32_t fourcc) {
377 append(&fourcc, sizeof fourcc);
378 const std::size_t sizeAt = _out.size();
379 const std::uint32_t placeholder = 0;
380 append(&placeholder, sizeof placeholder);
381 return sizeAt;
382 }
383
384
387 void endChunk(std::size_t sizeAt) {
388 const auto size = static_cast<std::uint32_t>(_out.size() - sizeAt - sizeof(std::uint32_t));
389 std::memcpy(_out.data() + sizeAt, &size, sizeof size);
390 }
391
395 void append(const void* bytes, std::size_t n) {
396 const auto* p = static_cast<const std::byte*>(bytes);
397 _out.insert(_out.end(), p, p + n);
398 }
399
400 private:
401 FileBuffer& _out;
402 };
403
410 template <ChunkedEntity E>
411 Result<void> readEntity(E& entity, std::span<const std::byte> data) {
412 static constexpr auto Members = detail::membersOf<E>();
413 std::size_t pos = 0;
414
415 // phase 1: container-header prelude members, in declaration order
416 template for (constexpr auto m : Members) {
417 if constexpr (detail::annotation<detail::HeaderSpec, m>().has_value()) {
418 using M = [:std::meta::type_of(m):];
419 static_assert(std::is_trivially_copyable_v<M>, "header members are raw binary structs");
420 if (data.size() - pos < sizeof(M))
422 std::format("header prelude at offset {:#x}: {} bytes needed, {} left", pos, sizeof(M),
423 data.size() - pos));
424 std::memcpy(&entity.[:m:], data.data() + pos, sizeof(M));
425 pos += sizeof(M);
426 }
427 }
428
429 // phase 2: order-independent chunk scan
430 std::array<std::uint32_t, Members.size()> occurrences{};
431 while (data.size() - pos >= 2 * sizeof(std::uint32_t)) {
432 std::uint32_t fourcc = 0;
433 std::uint32_t size = 0;
434 std::memcpy(&fourcc, data.data() + pos, sizeof fourcc);
435 std::memcpy(&size, data.data() + pos + sizeof fourcc, sizeof size);
436 if (size > data.size() - pos - 2 * sizeof(std::uint32_t))
438 std::format("size {} overruns the buffer", size),
440 const auto payload = data.subspan(pos + 2 * sizeof(std::uint32_t), size);
441
442 bool matched = false;
443 std::int32_t index = -1;
444 template for (constexpr auto m : Members) {
445 ++index;
446 if constexpr (constexpr auto spec = detail::annotation<detail::ChunkSpec, m>(); spec.has_value() &&
448 using M = [:std::meta::type_of(m):];
449 if (!matched && fourcc == spec->magic) {
450 const auto memberSlot = static_cast<std::size_t>(index);
451 if constexpr (detail::RepeatedTraits<M>::Value) {
452 static_assert(detail::annotation<detail::RepeatsSpec, m>().has_value(),
453 "Repeated<> members must carry a repeats() annotation");
454 if (auto* slot = entity.[:m:].push()) {
455 auto r = detail::readValue(*slot, payload, fourcc, pos, spec->endian);
456 if (!r) return std::unexpected{r.error()};
457 matched = true;
458 entity.journal.push_back({fourcc, index, occurrences[memberSlot]++});
459 }
460 // all slots taken: falls through to the unknown route below
461 }
462 else if constexpr (detail::annotation<detail::RepeatingSpec, m>().has_value()) {
463 // one element per encounter, unbounded (see the repeating
464 // annotation): the payload reads into a fresh back element
465 static_assert(detail::IsVectorV<M>, "repeating members must be std::vector<Element>");
466 auto r = detail::readValue(entity.[:m:].emplace_back(), payload, fourcc, pos, spec->endian);
467 if (!r) return std::unexpected{r.error()};
468 matched = true;
469 entity.journal.push_back({fourcc, index, occurrences[memberSlot]++});
470 }
471 else if (occurrences[memberSlot] == 0) {
472 auto r = detail::readValue(entity.[:m:], payload, fourcc, pos, spec->endian);
473 if (!r) return std::unexpected{r.error()};
474 matched = true;
475 entity.journal.push_back({fourcc, index, occurrences[memberSlot]++});
476 }
477 // duplicate of a non-repeated chunk: preserved as unknown below
478 }
479 }
480 }
481 if (!matched) {
482 entity.journal.push_back({fourcc, -1, static_cast<std::uint32_t>(entity.unknown.size())});
483 entity.unknown.push_back({fourcc, {payload.begin(), payload.end()}});
484 }
485 pos += 2 * sizeof(std::uint32_t) + size;
486 }
487 if (pos != data.size())
488 entity.trailing.assign(data.begin() + static_cast<std::ptrdiff_t>(pos), data.end());
489
490 // phase 3: required-presence check
491 std::int32_t index = -1;
492 std::optional<Error> missing;
493 template for (constexpr auto m : Members) {
494 ++index;
495 if constexpr (constexpr auto spec = detail::annotation<detail::ChunkSpec, m>(); spec.has_value() &&
497 if (!missing && occurrences[static_cast<std::size_t>(index)] == 0)
498 missing = Error{
500 std::format("required chunk {} is absent", fourccToString(spec->magic, spec->endian))
501 };
502 }
503 }
504 if (missing) return std::unexpected{*missing};
505 return {};
506 }
507
513 template <ChunkedEntity E>
514 Result<void> writeEntity(const E& entity, FileBuffer& out) {
515 static constexpr auto Members = detail::membersOf<E>();
516 detail::ChunkWriter writer{out};
517 const std::size_t imageStart = out.size();
518
519 // Journal resequencing: an entity may declare
520 // Result<std::optional<std::vector<JournalEntry>>> resequencedJournal() const
521 // to replace the replay order for THIS write — nullopt keeps the stored
522 // journal. WDL uses it to interleave its per-tile repeating chunks
523 // (MARE/MAHO) once tiles were added or removed, where the stored journal
524 // no longer matches the entity's content.
525 std::vector<JournalEntry> resequenced;
526 std::span<const JournalEntry> journal{entity.journal};
527 if constexpr (requires { entity.resequencedJournal(); }) {
528 auto r = entity.resequencedJournal();
529 if (!r) return std::unexpected{r.error()};
530 if (*r) {
531 resequenced = std::move(**r);
532 journal = resequenced;
533 }
534 }
535
536 // header prelude members first, mirroring readEntity's phase 1
537 template for (constexpr auto m : Members) {
538 if constexpr (detail::annotation<detail::HeaderSpec, m>().has_value()) writer.append(
539 &entity.[:m:], sizeof(entity.[:m:]));
540 }
541
542 std::array<std::uint32_t, Members.size()> written{};
543 const auto emit = [&](std::int32_t target, std::uint32_t occurrence) -> Result<void> {
544 std::int32_t index = -1;
545 template for (constexpr auto m : Members) {
546 ++index;
547 if constexpr (constexpr auto spec = detail::annotation<detail::ChunkSpec, m>(); spec.has_value() &&
549 using M = [:std::meta::type_of(m):];
550 if (index == target) {
551 const std::size_t sizeAt = writer.beginChunk(spec->magic);
553 has_value()) {
554 if (occurrence >= entity.[:m:].size())
556 std::format("journal references occurrence {} of chunk {}, " "only {} present",
557 occurrence, fourccToString(spec->magic, spec->endian),
558 entity.[:m:].size()));
559 if (auto r = detail::writeValue(entity.[:m:][occurrence], out); !r) return r;
560 }
561 else if (auto r = detail::writeValue(entity.[:m:], out); !r) return r;
562 writer.endChunk(sizeAt);
563 // Derived-field stamping: an entity may declare
564 // void patchChunk(std::uint32_t fourcc, std::span<std::byte>) const
565 // to overwrite binary fields whose source of truth is other
566 // members (e.g. WMORoot stamping the MOHD counts from its
567 // vectors). The hook sees the finished payload in place, so
568 // write() stays const and entity state is untouched.
569 if constexpr (requires {
570 entity.patchChunk(std::uint32_t{}, std::span<std::byte>{});
571 })
572 entity.patchChunk(spec->magic, std::span<std::byte>{
573 out.data() + sizeAt + sizeof(std::uint32_t),
574 out.size() - sizeAt - sizeof(std::uint32_t)
575 });
576 ++written[static_cast<std::size_t>(index)];
577 }
578 }
579 }
580 return {};
581 };
582
583 for (const JournalEntry& entry : journal) {
584 if (entry.member < 0) {
585 if (entry.occurrence >= entity.unknown.size())
587 std::format("journal references unknown chunk {}, only {} preserved", entry.occurrence,
588 entity.unknown.size()));
589 const UnknownChunk& u = entity.unknown[entry.occurrence];
590 const std::size_t sizeAt = writer.beginChunk(u.fourcc);
591 writer.append(u.bytes.data(), u.bytes.size());
592 writer.endChunk(sizeAt);
593 }
594 else if (auto r = emit(entry.member, entry.occurrence); !r) return r;
595 }
596
597 // members engaged but not journaled: fresh entities (in canonical chunk
598 // order — see writeOrder) and post-read additions alike
599 static constexpr auto Order = detail::writeOrder<E>();
600 std::optional<Error> failed;
601 template for (constexpr std::size_t idx : Order) {
602 constexpr auto m = Members[idx];
603 constexpr auto index = static_cast<std::int32_t>(idx);
604 if constexpr (constexpr auto spec = detail::annotation<detail::ChunkSpec, m>(); spec.has_value() &&
606 using M = [:std::meta::type_of(m):];
608 has_value()) {
609 // journaled occurrences already replayed; append the slots added since
610 for (std::uint32_t i = written[idx]; !failed && i < entity.[:m:].size(); ++i)
611 if (auto r = emit(index, i); !r) failed = r.error();
612 }
613 else if (!failed && written[idx] == 0) {
614 constexpr bool required = !detail::annotation<detail::OptionalSpec, m>().has_value();
615 if (required || detail::engaged(entity.[:m:]))
616 if (auto r = emit(index, 0); !r) failed = r.error();
617 }
618 }
619 }
620 if (failed) return std::unexpected{*failed};
621
622 writer.append(entity.trailing.data(), entity.trailing.size());
623
624 // Whole-image stamping: an entity may declare
625 // Result<void> patchFile(std::span<std::byte>) const
626 // to overwrite binary fields whose source of truth is the finished LAYOUT —
627 // fields patchChunk cannot serve because they reference bytes written
628 // after their own chunk (WDL's MAOF table of absolute MARE offsets; the
629 // ADT header offsets later). The hook sees this entity's complete
630 // serialized image in place.
631 if constexpr (requires(std::span<std::byte> image) {
632 { entity.patchFile(image) } -> std::same_as<Result<void>>;
633 })
634 if (auto r = entity.patchFile(std::span<std::byte>{out.data() + imageStart, out.size() - imageStart}); !r)
635 return r;
636 return {};
637 }
638
644 template <typename E>
645 consteval std::int32_t chunkMemberIndex(std::uint32_t magic) {
646 constexpr auto members = membersOf<E>();
647 for (std::size_t i = 0; i < members.size(); ++i)
648 if (chunkMagicOf(members[i]) == magic) return static_cast<std::int32_t>(i);
649 return -1;
650 }
651
660 template <ChunkedEntity E>
661 std::vector<JournalEntry> freshJournal(const E& entity) {
662 static constexpr auto Members = detail::membersOf<E>();
663 static constexpr auto Order = detail::writeOrder<E>();
664 std::vector<JournalEntry> out;
665 template for (constexpr std::size_t idx : Order) {
666 constexpr auto m = Members[idx];
667 constexpr auto index = static_cast<std::int32_t>(idx);
668 if constexpr (constexpr auto spec = detail::annotation<detail::ChunkSpec, m>(); spec.has_value() &&
670 using M = [:std::meta::type_of(m):];
672 has_value()) {
673 for (std::uint32_t i = 0; i < entity.[:m:].size(); ++i) out.push_back({spec->magic, index, i});
674 }
675 else {
676 constexpr bool required = !detail::annotation<detail::OptionalSpec, m>().has_value();
677 if (required || detail::engaged(entity.[:m:])) out.push_back({spec->magic, index, 0});
678 }
679 }
680 }
681 return out;
682 }
683
691 template <ChunkedEntity E>
692 bool entityEngaged(const E& entity) {
693 if (!entity.journal.empty() || !entity.unknown.empty() || !entity.trailing.empty()) return true;
694 static constexpr auto Members = membersOf<E>();
695 bool any = false;
696 template for (constexpr auto m : Members) {
698 E::Version, m>()) {
699 using M = [:std::meta::type_of(m):];
700 if constexpr (IsVectorV<M> || RepeatedTraits<M>::Value || SelfSerializing<M>) any = any || !entity.[:m:].
701 empty();
702 else if constexpr (ChunkedEntity<M>) any = any || entityEngaged(entity.[:m:]);
703 }
704 }
705 return any;
706 }
707
708 template <typename M>
709 Result<void> readValue(M& dst,
710 std::span<const std::byte> payload,
711 std::uint32_t fourcc,
712 std::size_t offset,
713 FourCCEndian endian) {
714 if constexpr (ChunkedEntity<M>) {
715 // a container chunk: its payload is a chunk stream of its own
716 return readEntity(dst, payload);
717 }
718 else if constexpr (SelfSerializing<M>) {
719 return dst.read(payload);
720 }
721 else if constexpr (IsVectorV<M>) {
722 using T = typename M::value_type;
723 static_assert(std::is_trivially_copyable_v<T>, "array chunks hold raw binary structs");
724 if (payload.size() % sizeof(T) != 0)
725 return chunkError(ErrorCode::ChunkSizeMismatch, fourcc, offset,
726 std::format("size {} is not a multiple of the {}-byte element", payload.size(), sizeof(T)),
727 endian);
728 dst.resize(payload.size() / sizeof(T));
729 std::memcpy(dst.data(), payload.data(), payload.size());
730 return {};
731 }
732 else {
733 static_assert(std::is_trivially_copyable_v<M>, "data chunks hold raw binary structs");
734 if (payload.size() != sizeof(M))
735 return chunkError(ErrorCode::ChunkSizeMismatch, fourcc, offset,
736 std::format("size {} != expected {}", payload.size(), sizeof(M)), endian);
737 std::memcpy(&dst, payload.data(), sizeof(M));
738 return {};
739 }
740 }
741
742 template <typename M>
743 Result<void> writeValue(const M& src, FileBuffer& out) {
744 if constexpr (ChunkedEntity<M>) return writeEntity(src, out);
745 // a container chunk's payload: nested entity
746 else if constexpr (SelfSerializing<M>) return src.write(out);
747 else {
748 ChunkWriter writer{out};
749 if constexpr (IsVectorV<M>)
750 writer.append(src.data(), src.size() * sizeof(typename M::value_type));
751 else writer.append(&src, sizeof(M));
752 return {};
753 }
754 }
755 }
756
757 // --- ChunkedFile method definitions (declared above) -----------------
758
759 template <typename Derived>
760 Result<void> ChunkedFile<Derived>::read(std::span<const std::byte> data) {
761 return detail::readEntity(static_cast<Derived&>(*this), data);
762 }
763
764 template <typename Derived>
766 FileBuffer out;
767 if (auto r = detail::writeEntity(static_cast<const Derived&>(*this), out); !r) return std::unexpected{r.error()};
768 return out;
769 }
770
771 template <typename Derived>
773 ValidationReport report;
774 detail::validateEntity(static_cast<const Derived&>(*this), report);
775 return report;
776 }
777
778 template <typename Derived>
779 Result<void> ChunkedFile<Derived>::ensureValid() const {
780 return validate().toResult();
781 }
782}
The owning byte buffer file contents are read into.
Storage for a chunk that may appear up to N times in one entity (MOTV texcoord sets,...
void clear()
Reset to no filled slots; slot contents return to default-constructed.
static constexpr bool ValidationSlots
Each slot is a complete occurrence of the chunk, not one element of a larger array: a count contract ...
T * push()
Claim the next slot (the serializer's per-encounter hook).
const T & operator[](std::size_t i) const
static constexpr std::size_t capacity()
T & operator[](std::size_t i)
T value_type
The per-occurrence payload type — spelled like a standard container's so the generic walkers (validat...
std::size_t size() const
Append-only chunk emission with size backpatching.
std::size_t beginChunk(std::uint32_t fourcc)
Emit a chunk header with a placeholder size.
void append(const void *bytes, std::size_t n)
Append n raw bytes from bytes.
void endChunk(std::size_t sizeAt)
Backpatch the size of the chunk opened at sizeAt with the byte count appended since.
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
A type the chunk serializer can read and write: carries the round-trip bookkeeping and knows which cl...
A member type that owns its chunk-payload encoding: the serializer hands it the raw payload on read a...
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...
The chunk annotation vocabulary format entities declare their binary mapping with.
FourCC chunk identifiers: compile-time conversion of the four-letter codes to the host integers chunk...
RoundtripReport failed(std::string stage, std::string error)
A failed outcome.
Definition detail.hpp:34
consteval std::optional< Spec > annotation()
The first annotation of type Spec on reflected member M, if any.
void validateEntity(const E &entity, ValidationReport &report)
Validate a whole entity — the engine behind every validate() method; see ChunkedFile::validate() for ...
Result< void > writeEntity(const E &entity, FileBuffer &out)
Serialize entity, appending to out — the engine behind ChunkedFile::write(); see that method for the ...
consteval std::uint32_t chunkMagicOf(std::meta::info member)
The chunk() fourcc of member member, or 0 if it carries none.
std::vector< JournalEntry > freshJournal(const E &entity)
The journal a FRESH write of entity would produce: one entry per engaged (or required) chunk member i...
consteval std::int32_t chunkMemberIndex(std::uint32_t magic)
The flattened member index (a JournalEntry::member value) of the chunk member carrying magic,...
Result< void > writeValue(const M &src, FileBuffer &out)
Append member src's chunk payload to out (payload only — the caller emits the chunk header).
std::unexpected< Error > chunkError(ErrorCode code, std::uint32_t fourcc, std::size_t offset, std::string_view what, FourCCEndian endian=FourCCEndian::Reversed)
Build a chunk-scoped error value.
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)?
consteval FourCCEndian unknownFourccEndian()
The id layout to display for chunks the entity has no spec for (garbage magics, unknown chunks): the ...
consteval auto membersOf()
The reflected member list of E, public bases flattened in (see collectMembers).
Result< void > readEntity(E &entity, std::span< const std::byte > data)
Deserialize data into entity — the engine behind ChunkedFile::read(); see that method for the full co...
bool engaged(const M &value)
Is the member's chunk written when it never appeared in the journal?
Result< void > readValue(M &dst, std::span< const std::byte > payload, std::uint32_t fourcc, std::size_t offset, FourCCEndian endian)
Read one chunk payload into member dst, dispatching on the member kind (nested entity,...
consteval auto writeOrder()
The member indices (into membersOf<E>) in the order fresh entities emit their chunks.
bool entityEngaged(const E &entity)
Does a never-journaled nested entity hold anything worth a chunk?
@ Error
The client would misread or crash on a file written like this.
constexpr std::uint32_t fourcc(const char(&cc)[5], FourCCEndian endian=FourCCEndian::Reversed)
The host integer a scanned chunk id compares equal to for code cc.
Definition fourcc.hpp:33
constexpr std::string fourccToString(std::uint32_t fourcc, FourCCEndian endian=FourCCEndian::Reversed)
The readable four-character spelling of a scanned chunk id.
Definition fourcc.hpp:46
FourCCEndian
How a chunk's FourCC characters are laid out on disk.
Definition fourcc.hpp:14
@ Reversed
The characters are stored reversed: 'MVER' appears in the file as the bytes "REVM".
Definition fourcc.hpp:18
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
ErrorCode
Machine-readable failure category carried by every Error.
Definition error.hpp:18
@ ChunkSizeMismatch
A chunk's size disagrees with its binary struct layout.
Definition error.hpp:37
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
Definition error.hpp:36
@ ChunkMissing
A chunk the format requires is absent from the file.
Definition error.hpp:38
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
Definition buffer.hpp:16
StringBlock — the decoded representation of a chunk of zero-terminated strings (MOTX,...
A wowlib operation failure: a machine-readable code, a human-readable message, and the originating na...
Definition error.hpp:63
An unparsed chunk payload, preserved verbatim for round-trip.
std::vector< std::byte > bytes
Result< void > write(FileBuffer &out) const
Append the payload to out (the serializer's write hook).
bool operator==(const ChunkBlob &) const =default
std::size_t size() const
The payload size in bytes.
bool empty() const
Whether the payload holds any bytes.
Result< void > read(std::span< const std::byte > payload)
Replace the payload with payload (the serializer's read hook).
Round-trip bookkeeping common to every chunked entity: the encounter journal, unmodeled chunks,...
std::vector< std::byte > trailing
std::vector< UnknownChunk > unknown
std::vector< JournalEntry > journal
bool operator==(const ChunkExtras &) const =default
The serialization face of a chunked entity, mixed in CRTP-style: an entity struct E : ChunkedFile<E> ...
Result< FileBuffer > write() const
Serialize this entity.
Result< void > read(std::span< const std::byte > data)
Deserialize file bytes into this entity, replacing its contents.
Result< void > ensureValid() const
Validate and raise on the first error instead of returning a report — the assert-style face of valida...
ValidationReport validate() const
bool operator==(const ChunkedFile &) const =default
One chunk encounter in file order — the write path replays the journal to reproduce the original byte...
bool operator==(const JournalEntry &) const =default
std::int32_t member
Declaration index of the member the chunk was read into, or -1 for an unknown chunk.
std::uint32_t occurrence
Which occurrence of the member this was (repeated chunks), or the index into unknown for member == -1...
std::uint32_t fourcc
The id as scanned.
A chunk the entity does not model, preserved verbatim for round-trip.
std::uint32_t fourcc
The id as scanned (memcpy'd host u32).
bool operator==(const UnknownChunk &) const =default
std::vector< std::byte > bytes
The payload, verbatim.
Stored form of a chunk annotation: the member's binary identity.
Trait: is T a Repeated<U, N> (a repeats-annotated member)?
The validation vocabulary: the severity scale, the single finding and the report validate() fills.