34#include <welder/vocabulary.hpp>
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;
79 [[=welder::mark::exclude]]
89 template <
typename Derived>
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")]]);
97 [[=welder::doc(
"Serialize this entity."),
98 =welder::returns(
"the file bytes")]]
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")]]
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")]]
118 [[=welder::mark::exclude]]
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).")
129 [[=welder::mark::exclude]] std::vector<std::byte>
bytes;
134 [[=welder::mark::exclude]]
136 bytes.assign(payload.begin(), payload.end());
143 [[=welder::mark::exclude]]
150 [[=welder::getter, =welder::doc(
"Whether the payload holds any bytes.")]]
152 return bytes.empty();
155 [[=welder::getter, =welder::doc(
"The payload size in bytes.")]]
156 std::size_t
size()
const {
160 bool operator==(
const ChunkBlob&)
const =
default;
168 template <
typename T, std::
size_t N>
183 if (_count == N)
return nullptr;
184 return &_slots[_count++];
188 [[nodiscard]] std::size_t
size()
const {
return _count; }
191 [[nodiscard]]
bool empty()
const {
return _count == 0; }
194 [[nodiscard]]
bool full()
const {
return _count == N; }
203 [[nodiscard]]
static constexpr std::size_t
capacity() {
return N; }
207 [[nodiscard]] T&
operator[](std::size_t i) {
return _slots[i]; }
210 [[nodiscard]]
const T&
operator[](std::size_t i)
const {
return _slots[i]; }
213 [[nodiscard]] T*
begin() {
return _slots.data(); }
216 [[nodiscard]] T*
end() {
return _slots.data() + _count; }
219 [[nodiscard]]
const T*
begin()
const {
return _slots.data(); }
222 [[nodiscard]]
const T*
end()
const {
return _slots.data() + _count; }
225 std::array<T, N> _slots{};
226 std::size_t _count = 0;
231 template <
typename E>
concept ChunkedEntity = std::derived_from<E, ChunkExtras> &&
requires {
232 { E::Version } -> std::convertible_to<ClientVersion>;
240 std::span<const std::byte> payload,
242 { value.read(payload) } -> std::same_as<Result<void>>; { constant.write(out) } -> std::same_as<Result<void>>; {
244 } -> std::convertible_to<bool>;
250 template <
typename T>
252 static constexpr bool Value =
false;
255 template <
typename U, std::
size_t N>
256 struct RepeatedTraits<
Repeated<U, N>> {
257 static constexpr bool Value =
true;
259 static constexpr std::size_t Capacity = N;
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;
275 template <
typename 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)
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";
291 for (std::size_t i = 0; i < members.size(); ++i) order.push_back(i);
292 return std::define_static_array(order);
307 std::string_view what,
318 template <
typename E>
320 if constexpr (
requires { E::UnknownFourccEndian; })
return E::UnknownFourccEndian;
334 template <
typename M>
336 std::span<const std::byte> payload,
346 template <
typename M>
349 template <ChunkedEntity E>
360 template <
typename M>
378 const std::size_t sizeAt = _out.size();
379 const std::uint32_t placeholder = 0;
380 append(&placeholder,
sizeof placeholder);
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);
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);
410 template <ChunkedEntity E>
416 template for (
constexpr auto m : Members) {
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),
424 std::memcpy(&entity.[:m:], data.data() + pos,
sizeof(M));
430 std::array<std::uint32_t, Members.size()> occurrences{};
431 while (data.size() - pos >= 2 *
sizeof(std::uint32_t)) {
433 std::uint32_t size = 0;
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);
442 bool matched =
false;
443 std::int32_t index = -1;
444 template for (
constexpr auto m : Members) {
448 using M = [:std::meta::type_of(m):];
449 if (!matched &&
fourcc == spec->magic) {
450 const auto memberSlot =
static_cast<std::size_t
>(index);
453 "Repeated<> members must carry a repeats() annotation");
454 if (
auto* slot = entity.[:m:].push()) {
456 if (!r)
return std::unexpected{r.error()};
458 entity.journal.push_back({
fourcc, index, occurrences[memberSlot]++});
467 if (!r)
return std::unexpected{r.error()};
469 entity.journal.push_back({
fourcc, index, occurrences[memberSlot]++});
471 else if (occurrences[memberSlot] == 0) {
473 if (!r)
return std::unexpected{r.error()};
475 entity.journal.push_back({
fourcc, index, occurrences[memberSlot]++});
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()}});
485 pos += 2 *
sizeof(std::uint32_t) + size;
487 if (pos != data.size())
488 entity.trailing.assign(data.begin() +
static_cast<std::ptrdiff_t
>(pos), data.end());
491 std::int32_t index = -1;
492 std::optional<Error> missing;
493 template for (
constexpr auto m : Members) {
497 if (!missing && occurrences[
static_cast<std::size_t
>(index)] == 0)
500 std::format(
"required chunk {} is absent",
fourccToString(spec->magic, spec->endian))
504 if (missing)
return std::unexpected{*missing};
513 template <ChunkedEntity E>
516 detail::ChunkWriter writer{out};
517 const std::size_t imageStart = out.size();
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()};
531 resequenced = std::move(**r);
532 journal = resequenced;
537 template for (
constexpr auto m : Members) {
539 &entity.[:m:],
sizeof(entity.[:m:]));
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) {
549 using M = [:std::meta::type_of(m):];
550 if (index == target) {
551 const std::size_t sizeAt = writer.beginChunk(spec->magic);
554 if (occurrence >= entity.[:m:].size())
556 std::format(
"journal references occurrence {} of chunk {}, " "only {} present",
558 entity.[:m:].size()));
562 writer.endChunk(sizeAt);
569 if constexpr (
requires {
570 entity.patchChunk(std::uint32_t{}, std::span<std::byte>{});
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)
576 ++written[
static_cast<std::size_t
>(index)];
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);
594 else if (
auto r = emit(entry.member, entry.occurrence); !r)
return r;
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);
606 using M = [:std::meta::type_of(m):];
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();
613 else if (!failed && written[idx] == 0) {
616 if (
auto r = emit(index, 0); !r) failed = r.error();
620 if (failed)
return std::unexpected{*
failed};
622 writer.append(entity.trailing.data(), entity.trailing.size());
631 if constexpr (
requires(std::span<std::byte> image) {
632 { entity.patchFile(image) } -> std::same_as<Result<void>>;
634 if (
auto r = entity.patchFile(std::span<std::byte>{out.data() + imageStart, out.size() - imageStart}); !r)
644 template <
typename E>
645 consteval std::int32_t chunkMemberIndex(std::uint32_t magic) {
647 for (std::size_t i = 0; i < members.size(); ++i)
648 if (
chunkMagicOf(members[i]) == magic)
return static_cast<std::int32_t
>(i);
660 template <ChunkedEntity E>
661 std::vector<JournalEntry> freshJournal(
const E& entity) {
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);
670 using M = [:std::meta::type_of(m):];
673 for (std::uint32_t i = 0; i < entity.[:m:].size(); ++i) out.push_back({spec->magic, index, i});
677 if (required ||
detail::engaged(entity.[:m:])) out.push_back({spec->magic, index, 0});
691 template <ChunkedEntity E>
693 if (!entity.journal.empty() || !entity.unknown.empty() || !entity.trailing.empty())
return true;
696 template for (
constexpr auto m : Members) {
699 using M = [:std::meta::type_of(m):];
708 template <
typename M>
709 Result<void> readValue(M& dst,
710 std::span<const std::byte> payload,
711 std::uint32_t fourcc,
713 FourCCEndian endian) {
714 if constexpr (ChunkedEntity<M>) {
716 return readEntity(dst, payload);
718 else if constexpr (SelfSerializing<M>) {
719 return dst.read(payload);
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)),
728 dst.resize(payload.size() /
sizeof(T));
729 std::memcpy(dst.data(), payload.data(), payload.size());
733 static_assert(std::is_trivially_copyable_v<M>,
"data chunks hold raw binary structs");
734 if (payload.size() !=
sizeof(M))
736 std::format(
"size {} != expected {}", payload.size(),
sizeof(M)), endian);
737 std::memcpy(&dst, payload.data(),
sizeof(M));
742 template <
typename M>
743 Result<void>
writeValue(
const M& src, FileBuffer& out) {
744 if constexpr (ChunkedEntity<M>)
return writeEntity(src, out);
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));
759 template <
typename Derived>
764 template <
typename Derived>
767 if (
auto r =
detail::writeEntity(
static_cast<const Derived&
>(*
this), out); !r)
return std::unexpected{r.error()};
771 template <
typename Derived>
778 template <
typename Derived>
779 Result<void> ChunkedFile<Derived>::ensureValid()
const {
780 return validate().toResult();
The owning byte buffer file contents are read into.
Client version identity, the flavor axis that separates a client's CONTENT version from the engine ge...
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...
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.
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
ErrorCode
Machine-readable failure category carried by every Error.
@ ChunkSizeMismatch
A chunk's size disagrees with its binary struct layout.
@ ChunkTruncated
A chunk header or payload overruns the file buffer.
@ ChunkMissing
A chunk the format requires is absent from the file.
std::vector< std::byte > FileBuffer
Owning byte buffer for file contents read out of a client storage.
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...
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.
The validation vocabulary: the severity scale, the single finding and the report validate() fills.