wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
image.cpp
Go to the documentation of this file.
1
4
6
7#include <cstring>
8#include <utility>
9
10namespace wowlib::db::wdc {
11 namespace {
16 std::unexpected<Error> fail(std::string msg) {
17 return std::unexpected(Error{ErrorCode::TableTruncated, std::move(msg)});
18 }
19
22 void takeRaw(std::span<const std::byte> data, std::size_t& pos, void* out, std::size_t n) {
23 std::memcpy(out, data.data() + pos, n);
24 pos += n;
25 }
26 }
27
28 Result<WdcImage> WdcImage::parse(std::span<const std::byte> data) {
29 std::uint32_t magic = 0;
30 if (data.size() >= sizeof magic) std::memcpy(&magic, data.data(), sizeof magic);
31 if (magic == Wdc1Magic) return _parseWdc1(data);
32 if (magic == Wdc3Magic || magic == Wdc4Magic || magic == Wdc5Magic) return _parseWdc3(data, magic);
33 return makeError(ErrorCode::TableMagicUnknown, "WDC: the file magic is not a supported WDC flavor");
34 }
35
36 Result<WdcImage> WdcImage::_parseWdc1(std::span<const std::byte> data) {
37 WdcImage img;
38 img.file = data;
40
41 Wdc1Header h1;
42 if (data.size() < sizeof h1) return fail("WDC1: file smaller than its header");
43 std::memcpy(&h1, data.data(), sizeof h1);
44 img.magic = h1.magic;
45
46 // Map the WDC1 header onto the family-normalized Wdc3Header. Everything
47 // the codec reads downstream lives there; copyTableSize and
48 // offsetMapOffset only matter inside this function.
49 img.header.magic = h1.magic;
54 img.header.tableHash = h1.tableHash;
56 img.header.minId = h1.minId;
57 img.header.maxId = h1.maxId;
58 img.header.locale = h1.locale;
59 img.header.flags = h1.flags;
60 img.header.idIndex = h1.idIndex;
67 img.header.sectionCount = 1;
68
69 std::size_t pos = sizeof h1;
70 const std::size_t fieldStructBytes = std::size_t{h1.fieldCount} * sizeof(WdcFieldStructure);
71 if (data.size() < pos + fieldStructBytes) return fail("WDC1: field structure table overruns the file");
72 img.fieldStructure.resize(h1.fieldCount);
73 if (h1.fieldCount) takeRaw(data, pos, img.fieldStructure.data(), fieldStructBytes);
74
75 WdcSection sec;
76 const bool sparse = (h1.flags & WdcFlagSparse) != 0;
77 sec.header.fileOffset = static_cast<std::uint32_t>(pos);
78 sec.header.recordCount = h1.recordCount;
79 sec.header.stringTableSize = h1.stringTableSize;
80 sec.header.idListSize = h1.idListSize;
81 sec.header.relationshipDataSize = h1.relationshipDataSize;
82 sec.header.copyTableCount = h1.copyTableSize / 8;
83
84 if (sparse) {
85 // The variable-length records run from here to the offset map; the map
86 // itself is DENSE over minId..maxId with zero entries for absent ids.
87 // It is filtered to the present entries below (owned by the image) so
88 // the decoder sees WDC3's compact map + explicit id list shape.
89 if (h1.offsetMapOffset < pos || h1.offsetMapOffset > data.size()) return fail(
90 "WDC1: sparse offset_map_offset out of range");
91 sec.records = data.subspan(pos, h1.offsetMapOffset - pos);
92 sec.stringBase = static_cast<std::uint32_t>(pos);
93 pos = h1.offsetMapOffset;
94
95 const std::size_t denseCount = h1.maxId >= h1.minId ? std::size_t{h1.maxId} - h1.minId + 1 : 0;
96 if (pos + denseCount * 6 > data.size()) return fail("WDC1: dense offset map overruns the file");
97 for (std::size_t i = 0; i < denseCount; ++i) {
98 std::uint32_t offset = 0;
99 std::memcpy(&offset, data.data() + pos + i * 6, 4);
100 if (offset == 0) continue; // absent id
101 const std::byte* entry = data.data() + pos + i * 6;
102 img.ownedOffsetMap.insert(img.ownedOffsetMap.end(), entry, entry + 6);
103 img.ownedOffsetMapIds.push_back(h1.minId + static_cast<std::uint32_t>(i));
104 }
105 pos += denseCount * 6;
106 sec.offsetMap = img.ownedOffsetMap;
107 sec.offsetMapIds = std::as_bytes(std::span{img.ownedOffsetMapIds});
108 sec.header.offsetMapIdCount = static_cast<std::uint32_t>(img.ownedOffsetMapIds.size());
109 }
110 else {
111 const std::size_t recBytes = std::size_t{h1.recordCount} * h1.recordSize;
112 if (pos + recBytes + h1.stringTableSize > data.size()) return fail("WDC1: records+strings overrun the file");
113 sec.records = data.subspan(pos, recBytes);
114 sec.stringBase = static_cast<std::uint32_t>(pos + recBytes);
115 sec.strings = data.subspan(pos + recBytes, h1.stringTableSize);
116 pos += recBytes + h1.stringTableSize;
117 }
118
119 // WDC1 trailing block order: id list, copy table, field storage info,
120 // pallet data, common data, relationship map.
121 if (pos + h1.idListSize + h1.copyTableSize + h1.fieldStorageInfoSize + h1.palletDataSize + h1.
122 commonDataSize + h1.relationshipDataSize > data.size()) return fail("WDC1: trailing blocks overrun the file");
123 sec.idList = data.subspan(pos, h1.idListSize);
124 pos += h1.idListSize;
125 sec.copyTable = data.subspan(pos, h1.copyTableSize);
126 pos += h1.copyTableSize;
127
128 const std::size_t storageCount = h1.fieldStorageInfoSize / sizeof(WdcFieldStorage);
129 img.fieldStorage.resize(storageCount);
130 if (storageCount) takeRaw(data, pos, img.fieldStorage.data(), h1.fieldStorageInfoSize);
131 // Normalize WDC1's signed spelling: Bitpacked whose flags word (val3)
132 // carries 0x01 means sign-extend — the kind WDC2+ calls BitpackedSigned.
133 for (WdcFieldStorage& fs : img.fieldStorage)
134 if (fs.storageType == WdcCompression::Bitpacked && (fs.val3 & 1u)) fs.storageType =
136
137 img.palletData = data.subspan(pos, h1.palletDataSize);
138 pos += h1.palletDataSize;
139 img.commonData = data.subspan(pos, h1.commonDataSize);
140 pos += h1.commonDataSize;
141 sec.relationship = data.subspan(pos, h1.relationshipDataSize);
142
143 img.sections.push_back(std::move(sec));
144 return img;
145 }
146
147 Result<WdcImage> WdcImage::_parseWdc3(std::span<const std::byte> data, std::uint32_t magic) {
148 WdcImage img;
149 img.magic = magic;
150 img.file = data;
151 img.stringMode = StringRefMode::FieldRelative;
152
153 // WDC5 splices {versionNum, schemaString[128]} between the magic and
154 // the rest of the WDC3-shaped header; WDC3/WDC4 go straight on.
155 std::size_t pos = sizeof(std::uint32_t);
156 const std::size_t header_rest = sizeof(Wdc3Header) - sizeof(std::uint32_t);
157 const std::size_t prefix = magic == Wdc5Magic ? sizeof(Wdc5HeaderPrefix) : 0;
158 if (data.size() < pos + prefix + header_rest) return fail("WDC: file smaller than its header");
159 if (prefix) takeRaw(data, pos, &img.wdc5, sizeof img.wdc5);
160 img.header.magic = magic;
161 takeRaw(data, pos, &img.header.recordCount, header_rest);
162 const Wdc3Header& h = img.header;
163
164 const std::size_t sectionsBytes = std::size_t{h.sectionCount} * sizeof(Wdc3SectionHeader);
165 const std::size_t fieldStructBytes = std::size_t{h.fieldCount} * sizeof(WdcFieldStructure);
166 if (data.size() < pos + sectionsBytes + fieldStructBytes + h.fieldStorageInfoSize + h.palletDataSize + h.
167 commonDataSize) return fail("WDC: header tables overrun the file");
168
169 std::vector<Wdc3SectionHeader> sectionHeaders(h.sectionCount);
170 if (h.sectionCount) takeRaw(data, pos, sectionHeaders.data(), sectionsBytes);
171
172 img.fieldStructure.resize(h.fieldCount);
173 if (h.fieldCount) takeRaw(data, pos, img.fieldStructure.data(), fieldStructBytes);
174
175 const std::size_t storageCount = h.fieldStorageInfoSize / sizeof(WdcFieldStorage);
176 img.fieldStorage.resize(storageCount);
177 if (storageCount) takeRaw(data, pos, img.fieldStorage.data(), h.fieldStorageInfoSize);
178
179 img.palletData = data.subspan(pos, h.palletDataSize);
180 pos += h.palletDataSize;
181 img.commonData = data.subspan(pos, h.commonDataSize);
182 pos += h.commonDataSize;
183
184 // WDC4/WDC5 follow the common data with an encrypted_status block per
185 // encrypted section: {int32 count, uint32 ids[count]} — the ids hidden
186 // behind that section's TACT key, listed even when the storage delivered
187 // the section decrypted.
188 std::vector<std::vector<std::uint32_t>> encryptedIds(h.sectionCount);
189 if (magic != Wdc3Magic)
190 for (std::uint32_t s = 0; s < h.sectionCount; ++s) {
191 if (sectionHeaders[s].tactKeyHash == 0) continue;
192 std::uint32_t count = 0;
193 if (pos + 4 > data.size()) return fail("WDC: encrypted_status overruns the file");
194 takeRaw(data, pos, &count, 4);
195 if (pos + std::size_t{count} * 4 > data.size()) return fail("WDC: encrypted_status id list overruns the file");
196 encryptedIds[s].resize(count);
197 if (count) takeRaw(data, pos, encryptedIds[s].data(), std::size_t{count} * 4);
198 }
199
200 const bool sparse = (h.flags & WdcFlagSparse) != 0;
201 img.sections.reserve(h.sectionCount);
202 for (std::uint32_t s = 0; s < h.sectionCount; ++s) {
203 const Wdc3SectionHeader& sh = sectionHeaders[s];
204 WdcSection sec;
205 sec.header = sh;
206 sec.encrypted = sh.tactKeyHash != 0;
207 sec.encryptedIds = std::move(encryptedIds[s]);
208
209 std::size_t p = sh.fileOffset;
210 if (p > data.size()) return fail("WDC: section file_offset past end");
211
212 if (sparse) {
213 if (sh.offsetRecordsEnd < sh.fileOffset || sh.offsetRecordsEnd > data.size()) return fail(
214 "WDC: sparse offset_records_end out of range");
215 sec.records = data.subspan(p, sh.offsetRecordsEnd - p);
216 sec.stringBase = sh.fileOffset;
217 p = sh.offsetRecordsEnd;
218 }
219 else {
220 const std::size_t recBytes = std::size_t{sh.recordCount} * h.recordSize;
221 if (p + recBytes + sh.stringTableSize > data.size()) return fail(
222 "WDC: section records+strings overrun the file");
223 sec.records = data.subspan(p, recBytes);
224 sec.stringBase = static_cast<std::uint32_t>(p + recBytes);
225 sec.strings = data.subspan(p + recBytes, sh.stringTableSize);
226 p += recBytes + sh.stringTableSize;
227 }
228
229 auto take = [&](std::size_t n, std::span<const std::byte>& out) -> bool {
230 if (p + n > data.size()) return false;
231 out = data.subspan(p, n);
232 p += n;
233 return true;
234 };
235
236 // The section tail. WDC3 orders it id list, copy table, offset map,
237 // offset-map ids, relationship. WDC4/WDC5 move the offset-map ids to
238 // the very end, after the relationship block — unless flag 0x02, which
239 // restores the WDC3 order.
240 const bool idsAfterRelationship = magic != Wdc3Magic && (h.flags & WdcFlagSecondary) == 0;
241 if (!take(sh.idListSize, sec.idList)) return fail("WDC: id_list overruns the file");
242 if (!take(std::size_t{sh.copyTableCount} * 8, sec.copyTable)) return fail("WDC: copy_table overruns the file");
243 if (!take(std::size_t{sh.offsetMapIdCount} * 6, sec.offsetMap)) return fail(
244 "WDC: offset_map overruns the file");
245 if (!idsAfterRelationship && !take(std::size_t{sh.offsetMapIdCount} * 4, sec.offsetMapIds)) return fail(
246 "WDC: offset_map id list overruns the file");
247 if (!take(sh.relationshipDataSize, sec.relationship)) return fail("WDC: relationship block overruns the file");
248 if (idsAfterRelationship && !take(std::size_t{sh.offsetMapIdCount} * 4, sec.offsetMapIds)) return fail(
249 "WDC: offset_map id list overruns the file");
250
251 img.sections.push_back(std::move(sec));
252 }
253 return img;
254 }
255
256 std::vector<std::uint32_t> WdcImage::fieldAdditionalOffsets() const {
257 std::vector<std::uint32_t> offsets(fieldStorage.size());
258 std::uint32_t pallet = 0, common = 0;
259 for (std::size_t i = 0; i < fieldStorage.size(); ++i) {
260 const WdcFieldStorage& fs = fieldStorage[i];
261 if (fs.storageType == WdcCompression::Pallet || fs.storageType == WdcCompression::PalletArray) {
262 offsets[i] = pallet;
263 pallet += fs.additionalDataSize;
264 }
265 else if (fs.storageType == WdcCompression::CommonData) {
266 offsets[i] = common;
267 common += fs.additionalDataSize;
268 }
269 }
270 return offsets;
271 }
272
273 std::uint64_t WdcImage::fieldRaw(std::size_t field,
274 std::uint32_t element,
275 std::uint32_t arrayCount,
276 std::span<const std::byte> recordBytes,
277 std::uint32_t id,
278 const std::vector<std::uint32_t>& additional) const {
279 if (field >= fieldStorage.size()) return 0;
280 const WdcFieldStorage& fs = fieldStorage[field];
281 const BitReader reader(recordBytes.data(), recordBytes.size());
282 const std::size_t elemBits = elemBitWidth(field, arrayCount);
283 switch (fs.storageType) {
287 // Inline storage: elements sit side by side, elemBits wide each,
288 // starting at the field's bit offset.
289 return reader.read(std::size_t{fs.fieldOffsetBits} + std::size_t{element} * elemBits, elemBits);
291 const std::uint32_t index = static_cast<std::uint32_t>(reader.read(fs.fieldOffsetBits, fs.fieldSizeBits));
292 return _palletValue(additional[field], index, 0, 1);
293 }
295 // One bitpacked index selects the whole array; the pallet stores
296 // arrayCount values per slot.
297 const std::uint32_t index = static_cast<std::uint32_t>(reader.read(fs.fieldOffsetBits, fs.fieldSizeBits));
298 return _palletValue(additional[field], index, element, arrayCount);
299 }
301 // Nothing in the record at all: the value is keyed on the record id,
302 // defaulting to val1.
303 return _commonValue(additional[field], fs.additionalDataSize, id, fs.val1);
304 }
305 return 0;
306 }
307
308 std::size_t WdcImage::elemBitWidth(std::size_t field, std::uint32_t arrayCount) const {
309 if (field >= fieldStorage.size()) return 0;
310 const WdcFieldStorage& fs = fieldStorage[field];
311 const std::uint32_t count = arrayCount ? arrayCount : 1;
312 switch (fs.storageType) {
316 return fs.fieldSizeBits / count;
317 default:
318 return 32;
319 }
320 }
321
322 std::uint32_t WdcImage::_palletValue(std::uint32_t base,
323 std::uint32_t index,
324 std::uint32_t element,
325 std::uint32_t arraySize) const {
326 const std::size_t stride = std::size_t{arraySize ? arraySize : 1} * 4;
327 const std::size_t at = base + std::size_t{index} * stride + std::size_t{element} * 4;
328 if (at + 4 > palletData.size()) return 0;
329 std::uint32_t v = 0;
330 std::memcpy(&v, palletData.data() + at, 4);
331 return v;
332 }
333
334 std::uint32_t WdcImage::_commonValue(std::uint32_t base,
335 std::uint32_t size,
336 std::uint32_t id,
337 std::uint32_t fallback) const {
338 if (base + size > commonData.size()) return fallback;
339 const std::byte* block = commonData.data() + base;
340 const std::size_t count = size / 8;
341 // Binary search the sorted {id, value} pairs.
342 std::size_t lo = 0, hi = count;
343 while (lo < hi) {
344 const std::size_t mid = (lo + hi) / 2;
345 std::uint32_t key = 0;
346 std::memcpy(&key, block + mid * 8, 4);
347 if (key == id) {
348 std::uint32_t v = 0;
349 std::memcpy(&v, block + mid * 8 + 4, 4);
350 return v;
351 }
352 if (key < id) lo = mid + 1;
353 else hi = mid;
354 }
355 return fallback;
356 }
357}
A bounds-guarded little-endian bit reader over a record's byte span.
std::uint64_t read(std::size_t bitOffset, std::size_t bits) const
Read bits (<= 64) starting at absolute bit offset bitOffset.
WdcImage — the structural parser of the WDC family (WDC1/WDC3/WDC4/WDC5) and the per-field value deco...
@ Bitpacked
Bit-addressed unsigned value (signed in WDC1 when the entry's flags word carries bit 0x01).
Definition binary.hpp:154
@ None
Stored inline at a byte offset, fieldSizeBits wide.
Definition binary.hpp:153
@ BitpackedSigned
Bit-addressed signed value, sign-extended (WDC2+ only; WDC1 spells it Bitpacked + flags 0x01).
Definition binary.hpp:159
@ CommonData
Sparse per-id override table; default when an id is absent.
Definition binary.hpp:156
@ PalletArray
Bit-addressed index into palletData (array per slot).
Definition binary.hpp:158
@ Pallet
Bit-addressed index into palletData (one value per slot).
Definition binary.hpp:157
constexpr std::uint32_t Wdc3Magic
The WDC3 magic as memcpy'd off the file front (the bytes "WDC3").
Definition binary.hpp:35
constexpr std::uint32_t Wdc4Magic
The WDC4 magic as memcpy'd off the file front (the bytes "WDC4").
Definition binary.hpp:38
@ WdcFlagSparse
Offset-map layout + inline null-terminated strings.
Definition binary.hpp:53
@ 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
@ FieldRelative
WDC2+: offset from the referencing field's own position.
Definition image.hpp:37
@ BlockRelative
WDC1: offset from the string block's first byte.
Definition image.hpp:36
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
@ 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
The 84-byte WDC1 top header.
Definition binary.hpp:63
std::uint16_t idIndex
Column index of the inline id (when not flag 0x04).
Definition binary.hpp:76
std::uint32_t lookupColumnCount
Definition binary.hpp:79
std::uint32_t copyTableSize
Copy-table bytes ({newId, srcId} pairs).
Definition binary.hpp:74
std::uint32_t fieldStorageInfoSize
Bytes of the field_storage_info table (24 each).
Definition binary.hpp:83
std::uint32_t totalFieldCount
Equal to fieldCount in every observed file.
Definition binary.hpp:77
std::uint32_t relationshipDataSize
Definition binary.hpp:86
std::uint32_t palletDataSize
Definition binary.hpp:85
std::uint32_t recordSize
Fixed record stride in bytes (non-sparse).
Definition binary.hpp:67
std::uint32_t tableHash
SStrHash of the uppercased table name.
Definition binary.hpp:69
std::uint32_t layoutHash
Structure hash; matches a DBD LAYOUT.
Definition binary.hpp:70
std::uint32_t fieldCount
Stored columns; a non-inline id is not counted.
Definition binary.hpp:66
std::uint32_t commonDataSize
Definition binary.hpp:84
std::uint16_t flags
See WdcFlags.
Definition binary.hpp:75
std::uint32_t bitpackedDataOffset
First bit-packed field's byte offset in a record.
Definition binary.hpp:78
std::uint32_t offsetMapOffset
Absolute offset of the sparse offset map ({uint32 offset, uint16 size}[maxId - minId + 1]).
Definition binary.hpp:80
std::uint32_t stringTableSize
Definition binary.hpp:68
std::uint32_t idListSize
idList bytes (uint32 each) when flag 0x04.
Definition binary.hpp:82
std::uint32_t recordCount
Definition binary.hpp:65
std::uint32_t lookupColumnCount
Definition binary.hpp:108
std::uint32_t totalFieldCount
Columns incl.
Definition binary.hpp:106
std::uint32_t recordSize
Fixed record stride in bytes (non-sparse).
Definition binary.hpp:97
std::uint32_t recordCount
Total records across sections (unencrypted).
Definition binary.hpp:95
std::uint32_t palletDataSize
Definition binary.hpp:111
std::uint32_t bitpackedDataOffset
First bit-packed field's byte offset in a record.
Definition binary.hpp:107
std::uint32_t sectionCount
Definition binary.hpp:112
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::uint16_t flags
See WdcFlags.
Definition binary.hpp:104
std::uint16_t idIndex
Column index of the inline id (when not flag 0x04).
Definition binary.hpp:105
std::uint32_t stringTableSize
Section 0 string bytes (informational).
Definition binary.hpp:98
std::uint32_t fieldCount
Stored columns; a non-inline id is not counted.
Definition binary.hpp:96
std::uint32_t commonDataSize
Definition binary.hpp:110
std::uint32_t fieldStorageInfoSize
Bytes of the field_storage_info table (24 each).
Definition binary.hpp:109
The 24-byte field_storage_info entry: how one column is stored.
Definition binary.hpp:167
A parsed WDC-family file: the (normalized) header, shared tables, and located sections.
Definition image.hpp:61
std::span< const std::byte > file
Definition image.hpp:66
std::size_t elemBitWidth(std::size_t field, std::uint32_t arrayCount) const
The bit width of one element of inline field field: for the inline kinds, the field's total width div...
Definition image.cpp:308
std::vector< WdcFieldStorage > fieldStorage
Definition image.hpp:68
std::uint32_t magic
The flavor actually parsed.
Definition image.hpp:62
std::span< const std::byte > commonData
Definition image.hpp:70
std::uint64_t fieldRaw(std::size_t field, std::uint32_t element, std::uint32_t arrayCount, std::span< const std::byte > recordBytes, std::uint32_t id, const std::vector< std::uint32_t > &additional) const
Decode array element element of inline field field for one record.
Definition image.cpp:273
Wdc3Header header
WDC1 header fields are mapped onto this.
Definition image.hpp:63
std::vector< std::uint32_t > fieldAdditionalOffsets() const
The byte offset into palletData / commonData where each field's additional data begins (accumulated i...
Definition image.cpp:256
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
StringRefMode stringMode
Definition image.hpp:65
std::vector< WdcFieldStructure > fieldStructure
Definition image.hpp:67
std::span< const std::byte > palletData
Definition image.hpp:69
std::vector< WdcSection > sections
Definition image.hpp:71
std::vector< std::byte > ownedOffsetMap
Owned backing for WDC1's normalized sparse map (filtered entries + the ids the dense map only implied...
Definition image.hpp:74
std::vector< std::uint32_t > ownedOffsetMapIds
Definition image.hpp:75