wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
read.cpp
Go to the documentation of this file.
1
6
8
9#include <algorithm>
10#include <cstring>
11#include <format>
12#include <limits>
13#include <string>
14#include <vector>
15
18#include <wowlib/db/schema.hpp>
19
20namespace wowlib::db::wdc {
21 namespace {
27 std::string readCString(std::span<const std::byte> file, std::size_t at, std::size_t limit) {
28 limit = std::min(limit, file.size());
29 if (at >= limit) return {};
30 const auto* bytes = reinterpret_cast<const char*>(file.data());
31 std::size_t end = at;
32 while (end < limit && bytes[end] != '\0') ++end;
33 return std::string{bytes + at, end - at};
34 }
35
42 std::uint64_t signedFit(std::uint64_t raw, std::size_t bits, bool isSigned) {
43 if (!isSigned || bits == 0 || bits >= 64) return raw;
44 const std::uint64_t signBit = std::uint64_t{1} << (bits - 1);
45 if (raw & signBit) return raw | ~((std::uint64_t{1} << bits) - 1);
46 return raw;
47 }
48
56 class Decoder {
57 public:
61 Decoder(const WdcImage& img, const TableInfo& info, RecordSink& sink)
62 : _img{img}, _info{info}, _sink{sink}, _additional{img.fieldAdditionalOffsets()} {
63 // The single non-inline $relation$ column, when the table has one —
64 // its values live in the relationship block, not the record image.
65 std::size_t colidx = 0;
66 for (const Column& col : info.schema) {
67 if (col.isRelation && col.noninline) _relationCol = colidx;
68 ++colidx;
69 }
70 // Cross-section string geometry. WDC2+ string references are relative
71 // to the referencing field's position inside the CLIENT'S loading
72 // blob — all sections' records back to back, then all sections'
73 // string blocks — not inside the file. Precompute each section's
74 // record/string start inside that blob so references can be mapped
75 // back to file offsets (single-section files degenerate to "file
76 // offset + reference", which is why the difference only shows on
77 // multi-section tables).
78 _recordsBefore.reserve(img.sections.size());
79 std::uint64_t recSum = 0;
80 for (const WdcSection& sec : img.sections) {
81 _recordsBefore.push_back(recSum);
82 recSum += sec.records.size();
83 }
84 _totalRecordBytes = recSum;
85 }
86
90 Result<void> run(TableState& state) {
91 for (const WdcSection& sec : _img.sections) {
92 // A key-flagged section whose records are all zero is genuinely
93 // undecryptable (the storage lacked the TACT key and shipped
94 // zeros); a key-flagged section with real bytes was decrypted by
95 // the storage and decodes normally.
96 if (sec.encrypted && db::detail::allZero(sec.records)) {
97 reportEncrypted(sec, state);
98 continue;
99 }
100 if (auto r = _decodeSection(sec); !r) return r;
101 }
102 return {};
103 }
104
105 private:
111 void reportEncrypted(const WdcSection& sec, TableState& state) const {
112 EncryptedSection report{.keyHash = sec.header.tactKeyHash, .recordCount = sec.header.recordCount};
113 if (!sec.encryptedIds.empty()) report.ids = sec.encryptedIds;
114 else {
115 const auto ids = std::span{
116 reinterpret_cast<const std::uint32_t*>(sec.idList.data()),
117 sec.idList.size() / 4
118 };
119 report.ids.assign(ids.begin(), ids.end());
120 }
121 state.encrypted.push_back(std::move(report));
122 }
123
128 Result<void> _decodeSection(const WdcSection& sec) {
129 const std::size_t first = _sink.size();
130 if (_img.isSparse()) {
131 if (auto r = _decodeSparseSection(sec); !r) return r;
132 }
133 else {
134 const std::size_t stride = _img.header.recordSize;
135 const std::size_t sectionIndex = _indexOf(sec);
136 for (std::uint32_t r = 0; r < sec.header.recordCount; ++r) {
137 const std::uint32_t id = _recordId(sec, r);
138 const auto recordBytes = sec.records.subspan(std::size_t{r} * stride, stride);
139 const std::uint64_t recordBlob = _recordsBefore[sectionIndex] + std::uint64_t{r} * stride;
140 const std::size_t rec = _sink.add();
141 _decodeRecord(rec, recordBytes, recordBlob, id);
142 }
143 }
144 applyRelationship(sec, first);
145 expandCopies(sec);
146 return {};
147 }
148
156 Result<void> _decodeSparseSection(const WdcSection& sec) {
157 for (std::uint32_t r = 0; r < sec.header.offsetMapIdCount; ++r) {
158 std::uint32_t offset = 0, id = 0;
159 std::uint16_t size = 0;
160 std::memcpy(&offset, sec.offsetMap.data() + std::size_t{r} * 6, 4);
161 std::memcpy(&size, sec.offsetMap.data() + std::size_t{r} * 6 + 4, 2);
162 if (std::size_t{r} * 4 + 4 <= sec.offsetMapIds.size()) std::memcpy(
163 &id, sec.offsetMapIds.data() + std::size_t{r} * 4, 4);
164 if (offset + size > _img.file.size())
166 std::format("{}: sparse record {} at {:#x} overruns the file", _info.name, r, offset));
167 const std::size_t rec = _sink.add();
168 decodeSparseRecord(rec, _img.file.subspan(offset, size), id);
169 }
170 return {};
171 }
172
179 std::uint32_t _recordId(const WdcSection& sec, std::uint32_t r) const {
180 if (_img.idIsNoninline()) {
181 if (std::size_t{r} * 4 + 4 <= sec.idList.size()) {
182 std::uint32_t id = 0;
183 std::memcpy(&id, sec.idList.data() + std::size_t{r} * 4, 4);
184 return id;
185 }
186 return 0;
187 }
188 const auto recordBytes = sec.records.
189 subspan(std::size_t{r} * _img.header.recordSize, _img.header.recordSize);
190 return static_cast<std::uint32_t>(_img.fieldRaw(_img.header.idIndex, 0, 1, recordBytes, 0, _additional));
191 }
192
202 void _decodeRecord(std::size_t rec,
203 std::span<const std::byte> recordBytes,
204 std::uint64_t recordBlob,
205 std::uint32_t id) {
206 std::size_t f = 0, colidx = 0;
207 for (const Column& col : _info.schema) {
208 if (col.noninline) {
209 if (col.isId) _sink.setInt(rec, colidx, 0, static_cast<std::int64_t>(id));
210 }
211 else {
212 for (std::uint16_t e = 0; e < col.arrayLen; ++e)
213 decodeInlineElement(rec, f, col, e, colidx, recordBytes, recordBlob, id);
214 ++f;
215 }
216 ++colidx;
217 }
218 }
219
230 void decodeInlineElement(std::size_t rec,
231 std::size_t f,
232 const Column& col,
233 std::uint16_t e,
234 std::size_t colidx,
235 std::span<const std::byte> recordBytes,
236 std::uint64_t recordBlob,
237 std::uint32_t id) {
238 const std::uint64_t raw = _img.fieldRaw(f, e, col.arrayLen, recordBytes, id, _additional);
239 switch (col.type) {
240 case ColumnType::Int: {
241 const std::uint64_t v = signedFit(raw, _img.elemBitWidth(f, col.arrayLen),
242 _img.fieldIsSigned(f) || col.isSigned);
243 _sink.setInt(rec, colidx, e, static_cast<std::int64_t>(v));
244 break;
245 }
247 _sink.setFloat(rec, colidx, e, std::bit_cast<float>(static_cast<std::uint32_t>(raw)));
248 break;
249 case ColumnType::String: {
250 // A string ARRAY stores one reference per element, each relative
251 // to its OWN 4-byte slot, so the element base advances by e*4.
252 const std::size_t elemByte = std::size_t{_img.fieldStorage[f].fieldOffsetBits} / 8 + std::size_t{e} * 4;
253 _sink.setString(rec, colidx, e, _resolveString(recordBlob + elemByte, static_cast<std::uint32_t>(raw)));
254 break;
255 }
257 break;
258 // WDC (Cata+) has no localized-column layout; schemas never carry one.
259 }
260 }
261
274 std::string _resolveString(std::uint64_t refBlob, std::uint32_t raw) const {
275 if (_img.stringMode == StringRefMode::BlockRelative) {
276 const WdcSection& sec = _img.sections.front();
277 return readCString(_img.file, std::size_t{sec.stringBase} + raw,
278 std::size_t{sec.stringBase} + sec.strings.size());
279 }
280 // Sign-extend: a reference from a late section's field to an early
281 // section's string is a forward blob distance too (strings all sit
282 // after records), but keep the signed interpretation for safety.
283 const std::int64_t target = static_cast<std::int64_t>(refBlob) + static_cast<std::int32_t>(raw);
284 if (target < static_cast<std::int64_t>(_totalRecordBytes)) return {};
285 std::uint64_t t = static_cast<std::uint64_t>(target) - _totalRecordBytes;
286 for (std::size_t s = 0; s < _img.sections.size(); ++s) {
287 const WdcSection& sec = _img.sections[s];
288 if (t < sec.strings.size())
289 return readCString(_img.file, std::size_t{sec.stringBase} + t,
290 std::size_t{sec.stringBase} + sec.strings.size());
291 t -= sec.strings.size();
292 }
293 return {};
294 }
295
302 void decodeSparseRecord(std::size_t rec, std::span<const std::byte> bytes, std::uint32_t id) {
303 std::size_t cursor = 0, colidx = 0;
304 for (const Column& col : _info.schema) {
305 if (col.noninline) {
306 if (col.isId) _sink.setInt(rec, colidx, 0, static_cast<std::int64_t>(id));
307 ++colidx;
308 continue;
309 }
310 for (std::uint16_t e = 0; e < col.arrayLen; ++e) {
311 switch (col.type) {
312 case ColumnType::Int:
313 _sink.setInt(rec, colidx, e, db::detail::readInt(bytes, cursor, col.bits / 8u, col.isSigned));
314 break;
315 case ColumnType::Float: {
316 std::uint32_t v = 0;
317 if (cursor + 4 <= bytes.size()) std::memcpy(&v, bytes.data() + cursor, 4);
318 cursor += 4;
319 _sink.setFloat(rec, colidx, e, std::bit_cast<float>(v));
320 break;
321 }
322 case ColumnType::String: {
323 std::string s = readCString(bytes, cursor, bytes.size());
324 cursor += s.size() + 1;
325 _sink.setString(rec, colidx, e, s);
326 break;
327 }
329 break;
330 }
331 }
332 ++colidx;
333 }
334 }
335
342 void applyRelationship(const WdcSection& sec, std::size_t first) {
343 if (_relationCol == std::numeric_limits<std::size_t>::max() || sec.relationship.size() < 12) return;
344 std::uint32_t num = 0;
345 std::memcpy(&num, sec.relationship.data(), 4);
346 const bool byId = _img.magic != Wdc1Magic && _img.magic != Wdc3Magic && (_img.header.flags &
347 WdcFlagSecondary) != 0;
348 for (std::uint32_t e = 0; e < num; ++e) {
349 const std::size_t at = 12 + std::size_t{e} * 8;
350 if (at + 8 > sec.relationship.size()) break;
351 std::uint32_t foreign = 0, key = 0;
352 std::memcpy(&foreign, sec.relationship.data() + at, 4);
353 std::memcpy(&key, sec.relationship.data() + at + 4, 4);
354 const std::size_t rec = byId ? _sink.findById(key) : first + key;
355 if (rec < _sink.size())
356 _sink.setInt(rec, _relationCol, 0, static_cast<std::int64_t>(foreign));
357 }
358 }
359
363 void expandCopies(const WdcSection& sec) {
364 for (std::size_t c = 0; c + 8 <= sec.copyTable.size(); c += 8) {
365 std::uint32_t newId = 0, srcId = 0;
366 std::memcpy(&newId, sec.copyTable.data() + c, 4);
367 std::memcpy(&srcId, sec.copyTable.data() + c + 4, 4);
368 if (const std::size_t src = _sink.findById(srcId); src != _sink.size()) _sink.cloneWithId(src, newId);
369 }
370 }
371
376 std::size_t _indexOf(const WdcSection& sec) const {
377 return static_cast<std::size_t>(&sec - _img.sections.data());
378 }
379
380 const WdcImage& _img;
381 const TableInfo& _info;
382 RecordSink& _sink;
383 std::vector<std::uint32_t> _additional;
384 std::size_t _relationCol = std::numeric_limits<std::size_t>::max();
385 std::vector<std::uint64_t> _recordsBefore;
386 std::uint64_t _totalRecordBytes = 0;
387 };
388 }
389
390 Result<void> readWdc(const TableInfo& info, std::span<const std::byte> data, RecordSink& sink, TableState& state) {
391 if (info.version < builds::Cata)
392 return makeError(ErrorCode::TableMagicUnknown, std::format("{}: a {}.{} client does not use the WDC formats",
393 info.name, info.version.major, info.version.minor));
394
395 auto parsed = WdcImage::parse(data);
396 if (!parsed) return std::unexpected{parsed.error()};
397 const WdcImage& img = *parsed;
398
399 const std::size_t inlineColumns = db::detail::inlineColumnCount(info.schema);
400 if (img.header.fieldCount != inlineColumns)
402 std::format(
403 "{}: the file stores {} inline fields but the generated schema has {} "
404 "(layout_hash {:#010x})", info.name, img.header.fieldCount, inlineColumns,
405 img.header.layoutHash));
406
407 sink.clear();
408 state.reset();
409 state.sourceMagic = img.magic;
410 state.wdcTableHash = img.header.tableHash;
411 state.wdcLayoutHash = img.header.layoutHash;
412 state.wdcLocale = img.header.locale;
413 state.wdcKinds.assign(inlineColumns, static_cast<std::uint8_t>(WdcCompression::None));
414 for (std::size_t f = 0; f < inlineColumns && f < img.fieldStorage.size(); ++f)
415 state.wdcKinds[f] = static_cast<std::uint8_t>(img.fieldStorage[f].storageType);
416 if (img.magic == Wdc5Magic) {
417 const auto* pb = reinterpret_cast<const std::byte*>(&img.wdc5);
418 state.wdc5Prefix.assign(pb, pb + sizeof img.wdc5);
419 }
420
421 if (auto r = Decoder{img, info, sink}.run(state); !r) return r;
422
423 // While any section stays undecryptable the original bytes are the only
424 // faithful serialization — keep them for the verbatim re-emit.
425 if (!state.encrypted.empty()) state.wdcOriginal.assign(data.begin(), data.end());
426 return {};
427 }
428}
The decode target: the codecs build the record vector through this, one field at a time.
Definition codec.hpp:110
virtual void clear()=0
Named ClientVersion constants for the exact client builds format features appeared (or vanished) at —...
Shared codec internals for the fixed-stride formats (WDBC, WDB2): the string pool that reproduces byt...
constexpr ClientVersion Cata
Cataclysm (any 4.x client).
std::int64_t readInt(std::span< const std::byte > image, std::size_t &pos, std::size_t bytes, bool isSigned)
Read a little-endian integer of bytes bytes at pos, sign-extended when isSigned, advancing pos.
bool allZero(std::span< const std::byte > bytes)
Whether every byte of bytes is zero (an empty span counts as zero).
Definition schema.hpp:154
std::size_t inlineColumnCount(std::span< const Column > schema)
The number of INLINE columns (everything not $noninline$).
Definition codec.hpp:168
@ None
Stored inline at a byte offset, fieldSizeBits wide.
Definition binary.hpp:153
constexpr std::uint32_t Wdc3Magic
The WDC3 magic as memcpy'd off the file front (the bytes "WDC3").
Definition binary.hpp:35
Result< void > readWdc(const TableInfo &info, std::span< const std::byte > data, RecordSink &sink, TableState &state)
Decode a WDC-family image onto the schema: every unencrypted section's records (inline fields via the...
Definition read.cpp:390
@ WdcFlagSecondary
Secondary-key data; in WDC4+ also reorders the sparse id list ahead of the relationship block.
Definition binary.hpp:54
constexpr std::uint32_t Wdc1Magic
The WDC1 magic as memcpy'd off the file front (the bytes "WDC1").
Definition binary.hpp:32
constexpr std::uint32_t Wdc5Magic
The WDC5 magic as memcpy'd off the file front (the bytes "WDC5").
Definition binary.hpp:41
@ BlockRelative
WDC1: offset from the string block's first byte.
Definition image.hpp:36
@ Int
An integer column; Column::bits / Column::isSigned give the shape.
Definition schema.hpp:35
@ Float
A 32-bit IEEE float column.
Definition schema.hpp:36
@ String
A string-block reference column (u32 offset on disk).
Definition schema.hpp:37
@ LocString
A pre-Cataclysm localized string column (see LocString).
Definition schema.hpp:38
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
@ SchemaMismatch
A client-database record layout disagrees with the generated WoWDBDefs schema.
Definition error.hpp:46
@ TableMagicUnknown
A client-database magic wowlib does not support for the requested version.
Definition error.hpp:45
@ TableTruncated
A client-database header, record block or satellite block overruns the file.
Definition error.hpp:44
Schema reflection over generated client-database record structs: the column list, record stride and f...
std::uint16_t major
Expansion number, e.g.
std::uint16_t minor
Minor version within the expansion.
The per-table identity a codec needs beyond the record data: the client version (format selection,...
Definition codec.hpp:58
ClientVersion version
The record's client version.
Definition codec.hpp:59
std::span< const Column > schema
The record schema (schemaOf<Record>()).
Definition codec.hpp:61
std::string_view name
The WoWDBDefs table name.
Definition codec.hpp:60
The preserved decode state a table carries between read() and write() so a WDBC/WDB2 write is byte-pe...
Definition codec.hpp:68
std::vector< std::uint8_t > wdcKinds
Original per-inline-column compression (WdcCompression).
Definition codec.hpp:87
void reset()
Reset the per-read state before decoding a fresh image.
Definition codec.hpp:91
std::vector< std::byte > wdcOriginal
Raw image kept when the file has encrypted sections.
Definition codec.hpp:86
std::uint32_t wdcTableHash
Preserved WDC3 header tableHash.
Definition codec.hpp:83
std::uint32_t wdcLocale
Preserved WDC3 header locale.
Definition codec.hpp:85
std::vector< EncryptedSection > encrypted
Encrypted WDC sections skipped on read.
Definition codec.hpp:82
std::uint32_t sourceMagic
The magic read() sniffed; 0 for a fresh table.
Definition codec.hpp:69
std::uint32_t wdcLayoutHash
Preserved WDC3 header layoutHash.
Definition codec.hpp:84
std::vector< std::byte > wdc5Prefix
Preserved WDC5 {version, schema string} header prefix.
Definition codec.hpp:88
std::uint32_t tableHash
SStrHash of the uppercased table name.
Definition binary.hpp:99
std::uint32_t layoutHash
Structure hash; matches a DBD LAYOUT.
Definition binary.hpp:100
std::uint32_t fieldCount
Stored columns; a non-inline id is not counted.
Definition binary.hpp:96
A parsed WDC-family file: the (normalized) header, shared tables, and located sections.
Definition image.hpp:61
std::vector< WdcFieldStorage > fieldStorage
Definition image.hpp:68
std::uint32_t magic
The flavor actually parsed.
Definition image.hpp:62
Wdc5HeaderPrefix wdc5
Valid when magic == Wdc5Magic.
Definition image.hpp:64
Wdc3Header header
WDC1 header fields are mapped onto this.
Definition image.hpp:63
static Result< WdcImage > parse(std::span< const std::byte > data)
Parse and structurally validate data as a WDC-family file, sniffing the flavor off the magic.
Definition image.cpp:28
The WDC-family codec: the non-templated entry points Table<Record> dispatches to for every column-com...