@rhyster/wow-casc-dbc
Version:
Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.
1,324 lines (1,307 loc) • 86.3 kB
JavaScript
'use strict';
const assert = require('node:assert');
const crypto = require('node:crypto');
const async = require('async');
const cliProgress = require('cli-progress');
const zlib = require('node:zlib');
const fs = require('node:fs/promises');
const https = require('node:https');
const path = require('node:path');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const assert__default = /*#__PURE__*/_interopDefaultCompat(assert);
const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
const cliProgress__default = /*#__PURE__*/_interopDefaultCompat(cliProgress);
const zlib__default = /*#__PURE__*/_interopDefaultCompat(zlib);
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
const https__default = /*#__PURE__*/_interopDefaultCompat(https);
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const ADB_MAGIC = 1481004104;
class ADBReader {
build;
entries = [];
tableEntries = /* @__PURE__ */ new Map();
constructor(buffer) {
const magic = buffer.readUInt32BE(0);
assert__default(magic === ADB_MAGIC, `[ADB]: Invalid magic: ${magic.toString(16).padStart(8, "0")}`);
const version = buffer.readUInt32LE(4);
assert__default(version === 9, `[ADB]: Invalid version: ${version.toString()}`);
const build = buffer.readUInt32LE(8);
this.build = build;
let pointer = 44;
while (pointer < buffer.byteLength) {
const offset = pointer;
const entryMagic = buffer.readUInt32BE(offset);
assert__default(entryMagic === ADB_MAGIC, `[ADB]: Invalid entry magic: ${magic.toString(16).padStart(8, "0")}`);
const regionID = buffer.readInt32LE(offset + 4);
const pushID = buffer.readInt32LE(offset + 8);
const uniqueID = buffer.readUInt32LE(offset + 12);
const tableHash = buffer.readUInt32LE(offset + 16);
const recordID = buffer.readUInt32LE(offset + 20);
const dataSize = buffer.readUInt32LE(offset + 24);
const recordState = buffer.readUInt32LE(offset + 28);
const data = buffer.subarray(offset + 32, offset + 32 + dataSize);
const entry = {
regionID,
pushID,
uniqueID,
tableHash,
recordID,
dataSize,
recordState,
data
};
this.entries.push(entry);
if (!this.tableEntries.has(tableHash)) {
this.tableEntries.set(tableHash, []);
}
this.tableEntries.get(tableHash)?.push(entry);
pointer += 32 + dataSize;
}
}
}
class Salsa20 {
fixed;
key;
nonce;
counter = new Uint32Array([0, 0]);
state = new Uint32Array(16);
block = new Uint8Array(64);
position = 0;
constructor(key, nonce) {
assert__default(key.length === 32 || key.length === 16, "Salsa20 requires 128-bit or 256-bit key");
assert__default(nonce.length === 8, "Salsa20 requires 64-bit nonce");
this.key = new Uint32Array(8);
const keyView = new DataView(key.buffer);
if (key.length === 32) {
for (let i = 0; i < 8; i += 1) {
this.key[i] = keyView.getUint32(i * 4, true);
}
this.fixed = new Uint32Array([
1634760805,
857760878,
2036477234,
1797285236
]);
} else {
for (let i = 0; i < 4; i += 1) {
const word = keyView.getUint32(i * 4, true);
this.key[i] = word;
this.key[i + 4] = word;
}
this.fixed = new Uint32Array([
1634760805,
824206446,
2036477238,
1797285236
]);
}
this.nonce = new Uint32Array(2);
const nonceView = new DataView(nonce.buffer);
for (let i = 0; i < 2; i += 1) {
this.nonce[i] = nonceView.getUint32(i * 4, true);
}
this.generateBlock();
}
// eslint-disable-next-line @typescript-eslint/naming-convention
QR(a, b, c, d) {
let t;
t = this.state[a] + this.state[d] & 4294967295;
this.state[b] ^= t << 7 | t >>> 25;
t = this.state[b] + this.state[a] & 4294967295;
this.state[c] ^= t << 9 | t >>> 23;
t = this.state[c] + this.state[b] & 4294967295;
this.state[d] ^= t << 13 | t >>> 19;
t = this.state[d] + this.state[c] & 4294967295;
this.state[a] ^= t << 18 | t >>> 14;
}
generateBlock() {
const init = new Uint32Array([
this.fixed[0],
this.key[0],
this.key[1],
this.key[2],
this.key[3],
this.fixed[1],
this.nonce[0],
this.nonce[1],
this.counter[0],
this.counter[1],
this.fixed[2],
this.key[4],
this.key[5],
this.key[6],
this.key[7],
this.fixed[3]
]);
this.state = new Uint32Array(init);
for (let i = 0; i < 20; i += 2) {
this.QR(0, 4, 8, 12);
this.QR(5, 9, 13, 1);
this.QR(10, 14, 2, 6);
this.QR(15, 3, 7, 11);
this.QR(0, 1, 2, 3);
this.QR(5, 6, 7, 4);
this.QR(10, 11, 8, 9);
this.QR(15, 12, 13, 14);
}
for (let i = 0; i < 16; i += 1) {
const word = this.state[i] + init[i] & 4294967295;
this.block[i * 4] = word & 255;
this.block[i * 4 + 1] = word >>> 8 & 255;
this.block[i * 4 + 2] = word >>> 16 & 255;
this.block[i * 4 + 3] = word >>> 24 & 255;
}
this.counter[0] = this.counter[0] + 1 & 4294967295;
if (this.counter[0] === 0) {
this.counter[1] = this.counter[1] + 1 & 4294967295;
}
}
process(input) {
const { length } = input;
const result = new Uint8Array(length);
for (let i = 0; i < length; i += 1) {
if (this.position === 64) {
this.generateBlock();
this.position = 0;
}
result[i] = input[i] ^ this.block[this.position];
this.position += 1;
}
return result;
}
}
const BLTE_MAGIC = 1112298565;
const ENC_TYPE_SALSA20 = 83;
const EMPTY_HASH = "00000000000000000000000000000000";
class BLTEReader {
buffer;
blte;
blocks = [];
keys;
processedBlock = 0;
processedOffset = 0;
constructor(buffer, eKey, keys = /* @__PURE__ */ new Map()) {
this.blte = buffer;
this.buffer = Buffer.alloc(0);
this.keys = keys;
const size = buffer.byteLength;
assert__default(size >= 8, `[BLTE]: Invalid size: ${size.toString()} < 8`);
const magic = buffer.readUInt32BE(0);
assert__default(magic === BLTE_MAGIC, `[BLTE]: Invalid magic: ${magic.toString(16).padStart(8, "0")}`);
const headerSize = buffer.readUInt32BE(4);
if (headerSize === 0) {
const blteHash2 = crypto__default.createHash("md5").update(buffer).digest("hex");
assert__default(blteHash2 === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash2}`);
this.blocks.push({
compressedSize: size - 8,
decompressedSize: size - 9,
hash: EMPTY_HASH
});
this.processedOffset = 8;
return;
}
const blteHash = crypto__default.createHash("md5").update(buffer.subarray(0, headerSize)).digest("hex");
assert__default(blteHash === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash}`);
assert__default(size >= 12, `[BLTE]: Invalid size: ${size.toString()} < 12`);
const flag = buffer.readUInt8(8);
const numBlocks = buffer.readIntBE(9, 3);
assert__default(numBlocks > 0, `[BLTE]: Invalid number of blocks: ${numBlocks.toString()}`);
assert__default(flag === 15, `[BLTE]: Invalid flag: ${flag.toString(16).padStart(2, "0")}`);
const blockHeaderSize = numBlocks * 24;
assert__default(headerSize === blockHeaderSize + 12, `[BLTE]: Invalid header size: header size ${headerSize.toString()} != block header size ${blockHeaderSize.toString()} + 12`);
assert__default(size >= headerSize, `[BLTE]: Invalid size: ${size.toString()} < ${headerSize.toString()}`);
for (let i = 0; i < numBlocks; i += 1) {
const offset = 12 + i * 24;
const compressedSize = buffer.readUInt32BE(offset);
const decompressedSize = buffer.readUInt32BE(offset + 4);
const hash = buffer.toString("hex", offset + 8, offset + 24);
this.blocks.push({
compressedSize,
decompressedSize,
hash
});
}
this.processedOffset = headerSize;
}
processBlock(buffer, index, allowMissingKey) {
const flag = buffer.readUInt8(0);
switch (flag) {
case 69: {
let offset = 1;
const keyNameLength = buffer.readUInt8(offset);
offset += 1;
const keyNameBE = buffer.toString("hex", offset, offset + keyNameLength);
offset += keyNameLength;
const ivLength = buffer.readUInt8(offset);
offset += 1;
const ivBuffer = buffer.subarray(offset, offset + ivLength);
offset += ivLength;
const encryptType = buffer.readUInt8(offset);
offset += 1;
assert__default(encryptType === ENC_TYPE_SALSA20, `[BLTE]: Invalid encrypt type: ${encryptType.toString(16).padStart(2, "0")} at block ${index.toString()}`);
const keyName = [...keyNameBE.matchAll(/.{2}/g)].map((v) => v[0]).reverse().join("").toLowerCase();
const key = this.keys.get(keyName);
if (!key) {
if (allowMissingKey) {
return keyName;
}
throw new Error(`[BLTE]: Missing key: ${keyName} at block ${index.toString()}`);
}
const iv = new Uint8Array(8);
for (let i = 0; i < 8; i += 1) {
if (i < ivLength) {
iv[i] = ivBuffer.readUInt8(i) ^ index >>> 8 * i & 255;
} else {
iv[i] = 0;
}
}
const handler = new Salsa20(key, iv);
const decrypted = handler.process(buffer.subarray(offset));
if (allowMissingKey) {
return this.processBlock(Buffer.from(decrypted.buffer), index, true);
}
return this.processBlock(Buffer.from(decrypted.buffer), index, false);
}
case 70:
throw new Error(`[BLTE]: Frame (Recursive) block not supported at block ${index.toString()}`);
case 78:
return buffer.subarray(1);
case 90:
return zlib__default.inflateSync(buffer.subarray(1));
default:
throw new Error(`[BLTE]: Invalid block flag: ${flag.toString(16).padStart(2, "0")} at block ${index.toString()}`);
}
}
processBytes(allowMissingKey = false, size = Infinity) {
const missingKeyBlocks = [];
while (this.processedBlock < this.blocks.length && size > this.buffer.byteLength) {
const blockIndex = this.processedBlock;
const block = this.blocks[blockIndex];
const blockBuffer = this.blte.subarray(
this.processedOffset,
this.processedOffset + block.compressedSize
);
if (block.hash !== EMPTY_HASH) {
const blockHash = crypto__default.createHash("md5").update(blockBuffer).digest("hex");
assert__default(blockHash === block.hash, `[BLTE]: Invalid block hash: expected ${block.hash}, got ${blockHash}`);
}
if (allowMissingKey) {
const buffer = this.processBlock(blockBuffer, blockIndex, allowMissingKey);
if (typeof buffer === "string") {
missingKeyBlocks.push({
offset: this.buffer.byteLength,
size: block.decompressedSize,
blockIndex,
keyName: buffer
});
this.buffer = Buffer.concat([
this.buffer,
Buffer.alloc(block.decompressedSize)
]);
} else {
assert__default(
buffer.byteLength === block.decompressedSize,
`[BLTE]: Invalid decompressed size: expected ${block.decompressedSize.toString()}, got ${buffer.byteLength.toString()}`
);
this.buffer = Buffer.concat([this.buffer, buffer]);
}
} else {
const buffer = this.processBlock(blockBuffer, blockIndex, allowMissingKey);
assert__default(
buffer.byteLength === block.decompressedSize,
`[BLTE]: Invalid decompressed size: expected ${block.decompressedSize.toString()}, got ${buffer.byteLength.toString()}`
);
this.buffer = Buffer.concat([this.buffer, buffer]);
}
this.processedBlock += 1;
this.processedOffset += block.compressedSize;
}
return allowMissingKey ? missingKeyBlocks : void 0;
}
}
class Store {
data;
dataFile;
promise;
constructor(dataFile) {
this.dataFile = dataFile;
this.data = {};
this.promise = new Promise((resolve) => {
fs__default.readFile(dataFile, "utf-8").then((file) => {
this.data = JSON.parse(file);
resolve();
}).catch(() => {
resolve();
});
});
}
async get(key) {
await this.promise;
return this.data[key];
}
async set(key, value) {
await this.promise;
this.data[key] = value;
await fs__default.writeFile(this.dataFile, JSON.stringify(this.data), "utf-8");
}
}
const USER_AGENT = "node-wow-casc-dbc";
const CACHE_ROOT = path__default.resolve("cache");
const CACHE_DIRS = {
build: "builds",
indexes: "indices",
data: "data",
dbd: "dbd"
};
const CACHE_INTEGRITY_FILE = path__default.resolve(CACHE_ROOT, "integrity.json");
const cacheIntegrity = new Store(CACHE_INTEGRITY_FILE);
const formatCDNKey = (key) => `${key.substring(0, 2)}/${key.substring(2, 4)}/${key}`;
const requestData = async (url, {
partialOffset,
partialLength,
showProgress
} = {}) => new Promise((resolve, reject) => {
const options = {
headers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"User-Agent": USER_AGENT,
// eslint-disable-next-line @typescript-eslint/naming-convention
Range: partialOffset !== void 0 && partialLength !== void 0 ? `bytes=${partialOffset.toString()}-${(partialOffset + partialLength - 1).toString()}` : "bytes=0-"
}
};
https__default.get(url, options, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location !== void 0) {
requestData(res.headers.location, { partialOffset, partialLength, showProgress }).then(resolve).catch((err) => {
throw err;
});
} else {
reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode.toString()}`));
}
return;
}
if (res.statusCode === void 0 || res.statusCode < 200 || res.statusCode > 302) {
reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode?.toString() ?? "undefined"}`));
return;
}
const lengthText = res.headers["content-length"];
const length = lengthText !== void 0 ? parseInt(lengthText, 10) : 0;
const bar = showProgress === true && !Number.isNaN(length) && length >= 10485760 ? new cliProgress__default.SingleBar({ etaBuffer: 10240 }, cliProgress__default.Presets.shades_classic) : void 0;
bar?.start(length, 0);
const chunks = [];
res.on("data", (chunk) => {
bar?.increment(chunk.length);
chunks.push(chunk);
});
res.on("end", () => {
bar?.stop();
resolve(Buffer.concat(chunks));
});
res.on("error", (err) => {
bar?.stop();
reject(err);
});
}).on("error", reject).end();
});
const downloadFile = (prefixes, type, key, {
partialOffset,
partialLength,
showProgress,
showAttemptFail
} = {}) => {
const urls = prefixes.map((prefix) => `${prefix}/${type}/${formatCDNKey(key)}`);
return urls.reduce(
(prev, url, index) => prev.catch((err) => {
if (showAttemptFail === true && index > 0 && err instanceof Error) {
console.warn(`${(/* @__PURE__ */ new Date()).toISOString()} [WARN]:`, err.message);
}
return requestData(url, { partialOffset, partialLength, showProgress });
}),
Promise.reject(new Error(""))
);
};
const getFileCache = async (file) => {
const integrity = await cacheIntegrity.get(file);
if (integrity !== void 0) {
try {
const buffer = await fs__default.readFile(path__default.resolve(CACHE_ROOT, file));
const hash = crypto__default.createHash("sha256").update(buffer).digest("hex");
if (hash === integrity) {
return buffer;
}
} catch {
}
}
return void 0;
};
const getDataFile = async (prefixes, key, type, buildCKey, {
name,
partialOffset,
partialLength,
showProgress,
showAttemptFail
} = {}) => {
const dir = type === "build" ? path__default.join(CACHE_DIRS[type], buildCKey) : CACHE_DIRS[type];
const file = name !== void 0 ? path__default.join(dir, name) : path__default.join(dir, key);
const cacheBuffer = await getFileCache(file);
if (cacheBuffer) {
if (name === void 0 && partialOffset !== void 0 && partialLength !== void 0) {
return cacheBuffer.subarray(partialOffset, partialOffset + partialLength);
}
return cacheBuffer;
}
const downloadBuffer = await downloadFile(prefixes, "data", key, {
partialOffset,
partialLength,
showProgress,
showAttemptFail
});
if (partialOffset === void 0 && partialLength === void 0 || name !== void 0) {
await fs__default.mkdir(path__default.resolve(CACHE_ROOT, dir), { recursive: true });
await fs__default.writeFile(path__default.resolve(CACHE_ROOT, file), downloadBuffer);
const hash = crypto__default.createHash("sha256").update(downloadBuffer).digest("hex");
await cacheIntegrity.set(file, hash);
}
return downloadBuffer;
};
const getConfigFile = async (prefixes, key, {
showProgress,
showAttemptFail
} = {}) => {
const downloadBuffer = await downloadFile(prefixes, "config", key, { showProgress, showAttemptFail });
return downloadBuffer.toString("utf-8");
};
const getProductVersions = async (region, product) => {
const url = region !== "cn" ? `https://${region}.version.battle.net/v2/products/${product}/versions` : `https://cn.version.battlenet.com.cn/v2/products/${product}/versions`;
const headers = new Headers();
headers.set("User-Agent", USER_AGENT);
const res = await fetch(url, { headers });
return res.text();
};
const getProductCDNs = async (region, product) => {
const url = region !== "cn" ? `https://${region}.version.battle.net/v2/products/${product}/cdns` : `https://cn.version.battlenet.com.cn/v2/products/${product}/cdns`;
const headers = new Headers();
headers.set("User-Agent", USER_AGENT);
const res = await fetch(url, { headers });
return res.text();
};
const hashlittle2 = (key, pc = 0, pb = 0) => {
const { length } = key;
let offset = 0;
let a = 3735928559 + length + pc | 0;
let b = 3735928559 + length + pc | 0;
let c = 3735928559 + length + pc + pb | 0;
while (length - offset > 12) {
a += key.charCodeAt(offset + 0);
a += key.charCodeAt(offset + 1) << 8;
a += key.charCodeAt(offset + 2) << 16;
a += key.charCodeAt(offset + 3) << 24;
b += key.charCodeAt(offset + 4);
b += key.charCodeAt(offset + 5) << 8;
b += key.charCodeAt(offset + 6) << 16;
b += key.charCodeAt(offset + 7) << 24;
c += key.charCodeAt(offset + 8);
c += key.charCodeAt(offset + 9) << 8;
c += key.charCodeAt(offset + 10) << 16;
c += key.charCodeAt(offset + 11) << 24;
a -= c;
a ^= c << 4 | c >>> 28;
c = c + b | 0;
b -= a;
b ^= a << 6 | a >>> 26;
a = a + c | 0;
c -= b;
c ^= b << 8 | b >>> 24;
b = b + a | 0;
a -= c;
a ^= c << 16 | c >>> 16;
c = c + b | 0;
b -= a;
b ^= a << 19 | a >>> 13;
a = a + c | 0;
c -= b;
c ^= b << 4 | b >>> 28;
b = b + a | 0;
offset += 12;
}
if (length - offset > 0) {
switch (length - offset) {
case 12:
c += key.charCodeAt(offset + 11) << 24;
// falls through
case 11:
c += key.charCodeAt(offset + 10) << 16;
// falls through
case 10:
c += key.charCodeAt(offset + 9) << 8;
// falls through
case 9:
c += key.charCodeAt(offset + 8);
// falls through
case 8:
b += key.charCodeAt(offset + 7) << 24;
// falls through
case 7:
b += key.charCodeAt(offset + 6) << 16;
// falls through
case 6:
b += key.charCodeAt(offset + 5) << 8;
// falls through
case 5:
b += key.charCodeAt(offset + 4);
// falls through
case 4:
a += key.charCodeAt(offset + 3) << 24;
// falls through
case 3:
a += key.charCodeAt(offset + 2) << 16;
// falls through
case 2:
a += key.charCodeAt(offset + 1) << 8;
// falls through
case 1:
a += key.charCodeAt(offset + 0);
}
c ^= b;
c -= b << 14 | b >>> 18;
a ^= c;
a -= c << 11 | c >>> 21;
b ^= a;
b -= a << 25 | a >>> 7;
c ^= b;
c -= b << 16 | b >>> 16;
a ^= c;
a -= c << 4 | c >>> 28;
b ^= a;
b -= a << 14 | a >>> 18;
c ^= b;
c -= b << 24 | b >>> 8;
}
return [c >>> 0, b >>> 0];
};
const getNameHash = (name) => {
const normalized = name.replace(/\//g, "\\").toUpperCase();
const [pc, pb] = hashlittle2(normalized);
return `${pc.toString(16).padStart(8, "0")}${pb.toString(16).padStart(8, "0")}`;
};
const VERSION_SUB_OFFSET = -12;
const CHECKSUM_SIZE_SUB_OFFSET = -5;
const BLOCK_SIZE_OFFSET = 3;
const OFFSET_BYTES_OFFSET = 4;
const SIZE_BYTES_OFFSET = 5;
const KEY_SIZE_OFFSET = 6;
const NUM_ELEMENTS_OFFSET = 8;
const CHECKSUM_OFFSET = 12;
const CHECKSUM_TRIES = [
10,
9,
8,
7,
6,
5,
4,
3,
2,
1,
0
];
const tryArchiveIndexChecksumSize = (buffer, cKey) => {
const res = CHECKSUM_TRIES.filter(
(index) => buffer.readUInt8(buffer.byteLength - index + CHECKSUM_SIZE_SUB_OFFSET) === index && buffer.readUInt8(buffer.byteLength - index + VERSION_SUB_OFFSET) === 1
);
if (res.length === 1) {
return res[0];
}
throw new Error(`Invalid checksum size: ${res.join(", ")} in ${cKey}`);
};
const parseArchiveIndex = (buffer, cKey) => {
const checksumSize = tryArchiveIndexChecksumSize(buffer, cKey);
const versionOffset = buffer.byteLength - checksumSize + VERSION_SUB_OFFSET;
const footerOffset = versionOffset - checksumSize;
const tocChecksum = buffer.toString("hex", footerOffset, versionOffset);
const version = buffer.readUInt8(versionOffset);
const blockSizeKB = buffer.readUInt8(versionOffset + BLOCK_SIZE_OFFSET);
const offsetBytes = buffer.readUInt8(versionOffset + OFFSET_BYTES_OFFSET);
const sizeBytes = buffer.readUInt8(versionOffset + SIZE_BYTES_OFFSET);
const keySize = buffer.readUInt8(versionOffset + KEY_SIZE_OFFSET);
const numElements = buffer.readUInt32LE(versionOffset + NUM_ELEMENTS_OFFSET);
const footerChecksum = buffer.toString("hex", versionOffset + CHECKSUM_OFFSET);
assert__default(version === 1, `Invalid archive index version: ${version.toString()} in ${cKey}`);
const entrySize = keySize + offsetBytes + sizeBytes;
const blockSize = blockSizeKB * 1024;
const numBlocks = footerOffset / (blockSize + keySize + checksumSize);
const tocSize = (keySize + checksumSize) * numBlocks;
const toc = buffer.subarray(footerOffset - tocSize, footerOffset);
const footer = buffer.subarray(footerOffset);
const footerCheckBuffer = Buffer.concat([
buffer.subarray(versionOffset, buffer.byteLength - checksumSize),
Buffer.alloc(checksumSize)
]);
const hash = crypto__default.createHash("md5").update(footer).digest("hex");
assert__default(hash === cKey, `Invalid footer hash in ${cKey}: expected ${cKey}, got ${hash}`);
const footerHash = crypto__default.createHash("md5").update(footerCheckBuffer).digest("hex").slice(0, checksumSize * 2);
assert__default(footerHash === footerChecksum, `Invalid footer checksum in ${cKey}: expected ${footerChecksum}, got ${footerHash}`);
const tocHash = crypto__default.createHash("md5").update(toc).digest("hex").slice(0, checksumSize * 2);
assert__default(tocHash === tocChecksum, `Invalid toc checksum in ${cKey}: expected ${tocChecksum}, got ${tocHash}`);
const result = /* @__PURE__ */ new Map();
for (let i = 0; i < numBlocks; i += 1) {
const lastEkey = toc.toString("hex", i * keySize, (i + 1) * keySize);
const blockChecksum = toc.toString("hex", numBlocks * keySize + i * checksumSize, numBlocks * keySize + (i + 1) * checksumSize);
const blockOffset = i * blockSize;
const blockHash = crypto__default.createHash("md5").update(buffer.subarray(i * blockSize, (i + 1) * blockSize)).digest("hex").slice(0, checksumSize * 2);
assert__default(blockChecksum === blockHash, `Invalid block hash in ${cKey} at ${i.toString()}: expected ${blockChecksum}, got ${blockHash}`);
let length = 0;
while (length < blockSize) {
const entryOffset = blockOffset + length * entrySize;
const eKey = buffer.toString("hex", entryOffset, entryOffset + keySize);
const size = buffer.readUIntBE(entryOffset + keySize, sizeBytes);
const offset = buffer.readUIntBE(entryOffset + keySize + sizeBytes, offsetBytes);
result.set(eKey, { key: cKey, size, offset });
length += 1;
if (eKey === lastEkey) {
break;
}
}
}
assert__default(result.size === numElements, `Invalid number of elements: ${result.size.toString()} != ${numElements.toString()} in ${cKey}`);
return result;
};
const normalizeKey = (key) => key.split("-").map((part, index) => index === 0 ? part : `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
const parseConfig = (text) => {
const entries = {};
text.split(/\r?\n/).filter((line) => line.trim().length !== 0 && !line.startsWith("#")).forEach((line) => {
const match = /([^\s]+)\s?=\s?(.*)/.exec(line);
assert__default(match !== null, "Invalid token encountered parsing CDN config");
const [key, value] = match.slice(1);
entries[normalizeKey(key)] = value;
});
return entries;
};
const parseCDNConfig = (text) => parseConfig(text);
const parseBuildConfig = (text) => parseConfig(text);
const ENC_MAGIC = 17742;
const MAGIC_OFFSET$1 = 0;
const VERSION_OFFSET$1 = 2;
const HASH_SIZE_CKEY_OFFSET = 3;
const HASH_SIZE_EKEY_OFFSET = 4;
const CKEY_PAGE_SIZE_OFFSET = 5;
const EKEY_PAGE_SIZE_OFFSET = 7;
const CKEY_PAGE_COUNT_OFFSET = 9;
const EKEY_PAGE_COUNT_OFFSET = 13;
const SPEC_BLOCK_SIZE_OFFSET = 18;
const SPEC_BLOCK_OFFSET = 22;
const parseEncodingFile = (inputBuffer, eKey, cKey) => {
const reader = new BLTEReader(inputBuffer, eKey);
reader.processBytes();
const { buffer } = reader;
const encodingHash = crypto__default.createHash("md5").update(buffer).digest("hex");
assert__default(encodingHash === cKey, `Invalid encoding hash: expected ${cKey}, got ${encodingHash}`);
const magic = buffer.readUInt16BE(MAGIC_OFFSET$1);
assert__default(magic === ENC_MAGIC, `Invalid encoding magic: ${magic.toString(16).padStart(4, "0")}`);
const version = buffer.readUInt8(VERSION_OFFSET$1);
const hashSizeCKey = buffer.readUInt8(HASH_SIZE_CKEY_OFFSET);
const hashSizeEKey = buffer.readUInt8(HASH_SIZE_EKEY_OFFSET);
const cKeyPageSizeKB = buffer.readUInt16BE(CKEY_PAGE_SIZE_OFFSET);
const eKeyPageSizeKB = buffer.readUInt16BE(EKEY_PAGE_SIZE_OFFSET);
const cKeyPageCount = buffer.readUInt32BE(CKEY_PAGE_COUNT_OFFSET);
const eKeyPageCount = buffer.readUInt32BE(EKEY_PAGE_COUNT_OFFSET);
const specBlockSize = buffer.readUInt32BE(SPEC_BLOCK_SIZE_OFFSET);
assert__default(version === 1, `Invalid encoding version: ${version.toString()}`);
const eSpec = [];
let eSpecStringStart = SPEC_BLOCK_OFFSET;
for (let i = SPEC_BLOCK_OFFSET; i < SPEC_BLOCK_OFFSET + specBlockSize; i += 1) {
if (buffer[i] === 0) {
eSpec.push(buffer.toString("ascii", eSpecStringStart, i));
eSpecStringStart = i + 1;
}
}
const cKey2FileSize = /* @__PURE__ */ new Map();
const cKey2EKey = /* @__PURE__ */ new Map();
const cKeyPageIndexOffset = SPEC_BLOCK_OFFSET + specBlockSize;
const cKeyPageIndexEntrySize = hashSizeCKey + 16;
const cKeyPageOffset = cKeyPageIndexOffset + cKeyPageIndexEntrySize * cKeyPageCount;
const cKeyPageSize = cKeyPageSizeKB * 1024;
for (let i = 0; i < cKeyPageCount; i += 1) {
const indexOffset = cKeyPageIndexOffset + i * cKeyPageIndexEntrySize;
const pageOffset = cKeyPageOffset + i * cKeyPageSize;
const firstCKey = buffer.toString("hex", indexOffset, indexOffset + hashSizeCKey);
const pageChecksum = buffer.toString("hex", indexOffset + hashSizeCKey, indexOffset + hashSizeCKey + 16);
const pageBuffer = buffer.subarray(pageOffset, pageOffset + cKeyPageSize);
const pageHash = crypto__default.createHash("md5").update(pageBuffer).digest("hex");
assert__default(pageHash === pageChecksum, `Invalid ckey page ${i.toString()} checksum: expected ${pageChecksum}, got ${pageHash}`);
const pageFirstCKey = pageBuffer.toString("hex", 6, 6 + hashSizeCKey);
assert__default(pageFirstCKey === firstCKey, `Invalid ckey page ${i.toString()} first ckey: expected ${firstCKey}, got ${pageFirstCKey}`);
let pagePointer = 0;
while (pagePointer < cKeyPageSize) {
const keyCount = pageBuffer.readUInt8(pagePointer);
pagePointer += 1;
if (keyCount === 0) {
break;
}
const fileSize = pageBuffer.readUIntBE(pagePointer, 5);
pagePointer += 5;
const fileCKey = pageBuffer.toString("hex", pagePointer, pagePointer + hashSizeCKey);
pagePointer += hashSizeCKey;
cKey2FileSize.set(fileCKey, fileSize);
if (keyCount === 1) {
const fileEKey = pageBuffer.toString("hex", pagePointer, pagePointer + hashSizeEKey);
cKey2EKey.set(fileCKey, fileEKey);
pagePointer += hashSizeEKey;
} else {
const fileEKeys = [];
for (let j = 0; j < keyCount; j += 1) {
const fileEKey = pageBuffer.toString("hex", pagePointer, pagePointer + hashSizeEKey);
fileEKeys.push(fileEKey);
pagePointer += hashSizeEKey;
}
cKey2EKey.set(fileCKey, fileEKeys);
}
}
}
const eKey2ESpecIndex = /* @__PURE__ */ new Map();
const eKey2FileSize = /* @__PURE__ */ new Map();
const eKeyPageIndexOffset = cKeyPageOffset + cKeyPageSize * cKeyPageCount;
const eKeyPageIndexEntrySize = hashSizeEKey + 16;
const eKeyPageOffset = eKeyPageIndexOffset + eKeyPageIndexEntrySize * eKeyPageCount;
const eKeyPageSize = eKeyPageSizeKB * 1024;
const eKeyPageEntrySize = hashSizeEKey + 4 + 5;
for (let i = 0; i < eKeyPageCount; i += 1) {
const indexOffset = eKeyPageIndexOffset + i * eKeyPageIndexEntrySize;
const pageOffset = eKeyPageOffset + i * eKeyPageSize;
const firstEKey = buffer.toString("hex", indexOffset, indexOffset + hashSizeEKey);
const pageChecksum = buffer.toString("hex", indexOffset + hashSizeEKey, indexOffset + hashSizeEKey + 16);
const pageBuffer = buffer.subarray(pageOffset, pageOffset + eKeyPageSize);
const pageHash = crypto__default.createHash("md5").update(pageBuffer).digest("hex");
assert__default(pageHash === pageChecksum, `Invalid ekey page ${i.toString()} checksum: expected ${pageChecksum}, got ${pageHash}`);
const pageFirstEKey = pageBuffer.toString("hex", 0, hashSizeEKey);
assert__default(pageFirstEKey === firstEKey, `Invalid ekey page ${i.toString()} first ekey: expected ${firstEKey}, got ${pageFirstEKey}`);
let pagePointer = 0;
while (pagePointer + eKeyPageEntrySize <= eKeyPageSize) {
const fileEKey = pageBuffer.toString("hex", pagePointer, pagePointer + hashSizeEKey);
pagePointer += hashSizeEKey;
const eSpecIndex = pageBuffer.readUInt32BE(pagePointer);
pagePointer += 4;
eKey2ESpecIndex.set(fileEKey, eSpecIndex);
const fileSize = pageBuffer.readUIntBE(pagePointer, 5);
pagePointer += 5;
eKey2FileSize.set(fileEKey, fileSize);
}
}
return {
eSpec,
cKey2FileSize,
cKey2EKey,
eKey2ESpecIndex,
eKey2FileSize
};
};
const INSTALL_MAGIC = 18766;
const MAGIC_OFFSET = 0;
const VERSION_OFFSET = 2;
const HASH_SIZE_OFFSET = 3;
const NUM_TAGS_OFFSET = 4;
const NUM_ENTRIES_OFFSET = 6;
const TAGS_OFFSET = 10;
const parseInstallFile = (inputBuffer, eKey, cKey) => {
const reader = new BLTEReader(inputBuffer, eKey);
reader.processBytes();
const { buffer } = reader;
const installHash = crypto__default.createHash("md5").update(buffer).digest("hex");
assert__default(installHash === cKey, `Invalid root hash: expected ${cKey}, got ${installHash}`);
const magic = buffer.readUInt16BE(MAGIC_OFFSET);
assert__default(magic === INSTALL_MAGIC, `Invalid install magic: ${magic.toString(16).padStart(4, "0")}`);
const version = buffer.readUInt8(VERSION_OFFSET);
const hashSize = buffer.readUInt8(HASH_SIZE_OFFSET);
const numTags = buffer.readUInt16BE(NUM_TAGS_OFFSET);
const numEntries = buffer.readUInt32BE(NUM_ENTRIES_OFFSET);
assert__default(version === 1, `Invalid install version: ${version.toString()}`);
let pointer = TAGS_OFFSET;
const tags = [];
for (let i = 0; i < numTags; i += 1) {
const startOffset = pointer;
while (buffer[pointer] !== 0) {
pointer += 1;
}
const name = buffer.toString("utf-8", startOffset, pointer);
pointer += 1;
const type = buffer.readUInt16BE(pointer);
pointer += 2;
const files2 = [];
const finalOffset = pointer + Math.ceil(numEntries / 8);
while (pointer < finalOffset) {
const byte = buffer.readUInt8(pointer);
pointer += 1;
for (let j = 7; j >= 0; j -= 1) {
files2.push((byte & 1 << j) > 0);
}
}
tags.push({ name, type, files: files2 });
}
const files = [];
for (let i = 0; i < numEntries; i += 1) {
const startOffset = pointer;
while (buffer[pointer] !== 0) {
pointer += 1;
}
const name = buffer.toString("utf-8", startOffset, pointer);
pointer += 1;
const hash = buffer.toString("hex", pointer, pointer + hashSize);
pointer += hashSize;
const size = buffer.readUInt32BE(pointer);
pointer += 4;
const fileTags = tags.filter((tag) => tag.files[i]);
files.push({
name,
hash,
size,
tags: fileTags
});
}
return { tags, files };
};
const parseProductConfig = (text) => {
const lines = text.split(/\r?\n/);
const headers = lines[0].split("|").map((header) => header.split("!")[0].replace(" ", ""));
const entries = lines.filter((line, index) => index > 0 && line.trim().length !== 0 && !line.startsWith("#")).map((line) => {
const node = {};
const entryFields = line.split("|");
for (let i = 0, n = entryFields.length; i < n; i += 1) {
node[headers[i]] = entryFields[i];
}
return node;
});
return entries;
};
const parseProductVersions = (text) => parseProductConfig(text);
const parseProductCDNs = (text) => parseProductConfig(text);
const MFST_MAGIC = 1296454484;
const ContentFlags = {
Install: 4,
LoadOnWindows: 8,
LoadOnMacOS: 16,
x86_32: 32,
x86_64: 64,
LowViolence: 128,
DoNotLoad: 256,
UpdatePlugin: 2048,
ARM64: 32768,
Encrypted: 134217728,
NoNameHash: 268435456,
UncommonResolution: 536870912,
Bundle: 1073741824,
NoCompression: 2147483648
};
const LocaleFlags = {
enUS: 2,
koKR: 4,
frFR: 16,
deDE: 32,
zhCN: 64,
esES: 128,
zhTW: 256,
enGB: 512,
// enCN: 0x400,
// enTW: 0x800,
esMX: 4096,
ruRU: 8192,
ptBR: 16384,
itIT: 32768,
ptPT: 65536
};
const parseRootFile = (inputBuffer, eKey, cKey) => {
const reader = new BLTEReader(inputBuffer, eKey);
reader.processBytes();
const { buffer } = reader;
const rootHash = crypto__default.createHash("md5").update(buffer).digest("hex");
assert__default(rootHash === cKey, `Invalid root hash: expected ${cKey}, got ${rootHash}`);
const fileDataID2CKey = /* @__PURE__ */ new Map();
const nameHash2FileDataID = /* @__PURE__ */ new Map();
const magic = buffer.readUInt32LE(0);
if (magic === MFST_MAGIC) {
const firstEntry = buffer.readUInt32LE(4);
const newFormat = firstEntry < 100;
const headerSize = newFormat ? firstEntry : 12;
const version = newFormat ? buffer.readUInt32LE(8) : 0;
const totalFileCount = newFormat ? buffer.readUInt32LE(12) : firstEntry;
const namedFileCount = newFormat ? buffer.readUInt32LE(16) : buffer.readUInt32LE(8);
assert__default(version >= 0 && version <= 2, `Invalid root version: ${version.toString()}`);
const allowNonNamedFiles = totalFileCount !== namedFileCount;
let pointer = headerSize;
while (pointer < buffer.byteLength) {
const numRecords = buffer.readUInt32LE(pointer);
let contentFlags;
let localeFlags;
if (version >= 2) {
localeFlags = buffer.readUInt32LE(pointer + 4);
const contentFlags1 = buffer.readUInt32LE(pointer + 8);
const contentFlags2 = buffer.readUInt32LE(pointer + 12);
const contentFlags3 = buffer.readUInt8(pointer + 16);
contentFlags = contentFlags1 | contentFlags2 | contentFlags3 << 17;
pointer += 17;
} else {
contentFlags = buffer.readUInt32LE(pointer + 4);
localeFlags = buffer.readUInt32LE(pointer + 8);
pointer += 12;
}
const fileDataIDs = [];
let currFileDataID = -1;
for (let i = 0; i < numRecords; i += 1) {
currFileDataID += buffer.readUInt32LE(pointer) + 1;
fileDataIDs.push(currFileDataID);
pointer += 4;
}
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const fileCKey = buffer.toString("hex", pointer, pointer + 16);
pointer += 16;
if (fileDataID2CKey.has(fileDataID)) {
fileDataID2CKey.get(fileDataID)?.push({
cKey: fileCKey,
contentFlags,
localeFlags
});
} else {
fileDataID2CKey.set(fileDataID, [
{ cKey: fileCKey, contentFlags, localeFlags }
]);
}
}
if (!(allowNonNamedFiles && contentFlags & ContentFlags.NoNameHash)) {
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const nameHash = buffer.readBigUInt64LE(pointer).toString(16).padStart(16, "0");
pointer += 8;
nameHash2FileDataID.set(nameHash, fileDataID);
}
}
}
} else {
let pointer = 0;
while (pointer < buffer.byteLength) {
const numRecords = buffer.readUInt32LE(pointer);
const contentFlags = buffer.readUInt32LE(pointer + 4);
const localeFlags = buffer.readUInt32LE(pointer + 8);
pointer += 12;
const fileDataIDs = [];
let currFileDataID = -1;
for (let i = 0; i < numRecords; i += 1) {
currFileDataID += buffer.readUInt32LE(pointer) + 1;
fileDataIDs.push(currFileDataID);
pointer += 4;
}
for (let i = 0; i < numRecords; i += 1) {
const fileDataID = fileDataIDs[i];
const fileCKey = buffer.toString("hex", pointer, pointer + 16);
const nameHash = buffer.toString("hex", pointer + 16, pointer + 24);
pointer += 24;
if (fileDataID2CKey.has(fileDataID)) {
fileDataID2CKey.get(fileDataID)?.push({
cKey: fileCKey,
contentFlags,
localeFlags
});
} else {
fileDataID2CKey.set(fileDataID, [
{ cKey: fileCKey, contentFlags, localeFlags }
]);
}
nameHash2FileDataID.set(nameHash, fileDataID);
}
}
}
return { fileDataID2CKey, nameHash2FileDataID };
};
const JEDEC = [
"B",
"KB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB"
];
const formatFileSize = (input) => {
if (Number.isNaN(input)) return "";
let size = input;
const isNegative = size < 0;
const result = [];
if (isNegative) size = -size;
let exponent = Math.floor(Math.log(size) / Math.log(1024));
if (exponent < 0) exponent = 0;
if (exponent > 8) exponent = 8;
if (size === 0) {
result[0] = 0;
result[1] = JEDEC[exponent];
} else {
const val = size / 2 ** (exponent * 10);
result[0] = Number(val.toFixed(exponent > 0 ? 2 : 0));
if (result[0] === 1024 && exponent < 8) {
result[0] = 1;
exponent += 1;
}
result[1] = JEDEC[exponent];
}
if (isNegative) result[0] = -result[0];
return result.join(" ");
};
const resolveCDNHost = async (hosts, path) => {
const latencies = await Promise.allSettled(
hosts.map(async (host) => {
const start = Date.now();
await fetch(`https://${host}/`);
const end = Date.now();
return {
host,
latency: end - start
};
})
);
const resolved = latencies.filter((result) => result.status === "fulfilled").map((result) => result.value).sort((a, b) => a.latency - b.latency);
return resolved.map((result) => `https://${result.host}/${path}`);
};
const WDC5_MAGIC = 1464091445;
const readBitpackedValue = (buffer, fieldOffsetBits, fieldSizeBits, signed = false) => {
const offsetBytes = fieldOffsetBits >>> 3;
const bitOffset = fieldOffsetBits & 7;
const sizeBytes = Math.ceil((fieldSizeBits + bitOffset) / 8);
let value = 0n;
for (let i = sizeBytes - 1; i >= 0; i -= 1) {
const byte = buffer.readUInt8(offsetBytes + i);
value = value << 8n | BigInt(byte);
}
const result = signed ? BigInt.asIntN(fieldSizeBits, value >> BigInt(bitOffset)) : BigInt.asUintN(fieldSizeBits, value >> BigInt(bitOffset));
return fieldSizeBits <= 32 ? Number(result) : result;
};
const isDataRangeAllZero = (buffer, offset, length) => {
const end = offset + length;
for (let pointer = offset; pointer < end; pointer += 1) {
if (buffer[pointer] !== 0) {
return false;
}
}
return true;
};
class WDCReader {
tableHash;
layoutHash;
locale;
isNormal;
hasRelationshipData;
fields;
fieldsInfo;
rows = /* @__PURE__ */ new Map();
relationships = /* @__PURE__ */ new Map();
copyTable = /* @__PURE__ */ new Map();
hotfixes = /* @__PURE__ */ new Map();
constructor(buffer, blocksOrOption, adbInput) {
const options = blocksOrOption === void 0 || Array.isArray(blocksOrOption) ? { blocks: blocksOrOption, adb: adbInput } : blocksOrOption;
const blocks = options.blocks ?? [];
const adb = options.adb ?? adbInput;
const detectIsZeroedByData = options.detectIsZeroedByData ?? false;
const mergedBlocks = [];
if (!detectIsZeroedByData) {
blocks.sort((a, b) => a.offset - b.offset).forEach(({ offset, size }) => {
const lastBlock = mergedBlocks[mergedBlocks.length - 1];
if (mergedBlocks.length > 0 && lastBlock.offset + lastBlock.size >= offset) {
lastBlock.size = Math.max(
lastBlock.offset + lastBlock.size,
offset + size
) - lastBlock.offset;
} else {
mergedBlocks.push({
offset,
size
});
}
});
}
const magic = buffer.readUInt32BE(0);
const version = buffer.readUInt32LE(4);
const fieldCount = buffer.readUInt32LE(140);
const recordSize = buffer.readUInt32LE(144);
const tableHash = buffer.readUInt32LE(152);
const layoutHash = buffer.readUInt32LE(156);
const locale = buffer.readUInt32LE(168);
const flags = buffer.readUInt16LE(172);
const idIndex = buffer.readUInt16LE(174);
const fieldStorageInfoSize = buffer.readUInt32LE(188);
const commonDataSize = buffer.readUInt32LE(192);
const palletDataSize = buffer.readUInt32LE(196);
const sectionCount = buffer.readUInt32LE(200);
assert__default(magic === WDC5_MAGIC, `Invalid WDC5 magic: ${magic.toString(16).padStart(8, "0")}`);
assert__default(version === 5, `Invalid WDC5 version: ${version.toString()}`);
this.tableHash = tableHash;
this.layoutHash = layoutHash;
this.locale = locale;
const isNormal = !(flags & 1);
const hasRelationshipData = !!(flags & 2);
this.isNormal = isNormal;
this.hasRelationshipData = hasRelationshipData;
const sectionHeaders = [];
const sectionHeadersOffset = 204;
for (let i = 0; i < sectionCount; i += 1) {
const sectionHeaderOffset = sectionHeadersOffset + i * 40;
sectionHeaders.push({
tactKeyHash: buffer.readBigUInt64LE(sectionHeaderOffset),
fileOffset: buffer.readUInt32LE(sectionHeaderOffset + 8),
recordCount: buffer.readUInt32LE(sectionHeaderOffset + 12),
stringTableSize: buffer.readUInt32LE(sectionHeaderOffset + 16),
offsetRecordsEnd: buffer.readUInt32LE(sectionHeaderOffset + 20),
idListSize: buffer.readUInt32LE(sectionHeaderOffset + 24),
relationshipDataSize: buffer.readUInt32LE(sectionHeaderOffset + 28),
offsetMapIDCount: buffer.readUInt32LE(sectionHeaderOffset + 32),
copyTableCount: buffer.readUInt32LE(sectionHeaderOffset + 36)
});
}
const fields = [];
const fieldsOffset = 204 + sectionCount * 40;
for (let i = 0; i < fieldCount; i += 1) {
const fieldOffset = fieldsOffset + i * 4;
fields.push({
size: buffer.readInt16LE(fieldOffset),
position: buffer.readUInt16LE(fieldOffset + 2)
});
}
this.fields = fields;
const fieldsInfo = [];
const fieldsInfoOffset = fieldsOffset + fieldCount * 4;
for (let i = 0; i < fieldStorageInfoSize / 24; i += 1) {
const fieldInfoOffset = fieldsInfoOffset + i * 24;
const fieldOffsetBits = buffer.readUInt16LE(fieldInfoOffset);
const fieldSizeBits = buffer.readUInt16LE(fieldInfoOffset + 2);
const additionalDataSize = buffer.readUInt32LE(fieldInfoOffset + 4);
const storageType = buffer.readUInt32LE(fieldInfoOffset + 8);
const arg1 = buffer.readUInt32LE(fieldInfoOffset + 12);
const arg2 = buffer.readUInt32LE(fieldInfoOffset + 16);
const arg3 = buffer.readUInt32LE(fieldInfoOffset + 20);
switch (storageType) {
case 0:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "none"
});
break;
case 1:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "bitpacked",
bitpackingOffsetBits: arg1,
bitpackingSizeBits: arg2,
flags: arg3
});
break;
case 2:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "commonData",
defaultValue: arg1
});
break;
case 3:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "bitpackedIndexed",
bitpackingOffsetBits: arg1,
bitpackingSizeBits: arg2
});
break;
case 4:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "bitpackedIndexedArray",
bitpackingOffsetBits: arg1,
bitpackingSizeBits: arg2,
arrayCount: arg3
});
break;
case 5:
fieldsInfo.push({
fieldOffsetBits,
fieldSizeBits,
additionalDataSize,
storageType: "bitpackedSigned",
bitpackingOffsetBits: arg1,
bitpackingSizeBits: arg2,
flags: arg3
});
break;
default:
throw new Error(`Unknown storage type: ${storageType.toString(16).padStart(8, "0")}`);
}
}
this.fieldsInfo = fieldsInfo;
const palletData = /* @__PURE__ */ new Map();
const palletDataOffset = fieldsInfoOffset + fieldStorageInfoSize;
let palletDataPointer = palletDataOffset;
for (let i = 0; i < fieldsInfo.length; i += 1) {
const fieldInfo = fieldsInfo[i];
if (fieldInfo.storageType === "bitpackedIndexed" || fieldInfo.storageType === "bitpackedIndexedArray") {
const data = [];
for (let j = 0; j < fieldInfo.additionalDataSize / 4; j += 1) {
data.push(buffer.readUInt32LE(palletDataPointer));
palletDataPointer += 4;
}
palletData.set(i, data);
}
}
assert__default(
palletDataPointer === palletDataOffset + palletDataSize,
`Invalid pallet data size: ${(palletDataPointer - palletDataOffset).toString()} != ${palletDataSize.toString()}`
);
const commonData = /* @__PURE__ */ new Map();
const commonDataOffset = palletDataPointer;
let commonDataPointer = commonDataOffset;
for (let i = 0; i < fieldsInfo.length; i += 1) {
const fieldInfo = fieldsInfo[i];
if (fieldInfo.storageType === "commonData") {
const map = /* @__PURE__ */ new Map();
for (let j = 0; j < fieldInfo.additionalDataSize / 8; j += 1) {
map.set(
buffer.readUInt32LE(commonDataPointer),
buffer.readUInt32LE(commonDataPointer + 4)
);
commonDataPointer += 8;
}
commonData.set(i, map);
}
}
assert__default(
commonDataPointer === commonDataOffset + commonDataSize,
`Invalid common data size: ${(commonDataPointer - commonDataOffset).toString()} != ${commonDataSize.toString()}`
);
const encryptedIDs = /* @__PURE__ */ new Map();
const encryptedRecordsOffset = commonDataPointer;
let encryptedRecordsPointer = encryptedRecordsOffset;
for (let i = 0; i < sectionHeaders.length; i += 1) {
const sectionHeader = sectionHeaders[i];
if (sectionHeader.tactKeyHash !== 0n) {
const count = buffer.readUInt32LE(encryptedRecordsPointer);
encryptedRecordsPointer += 4;
const data = [];
for (let j = 0; j < count; j += 1) {
data.push(buffer.readUInt32LE(encryptedRecordsPointer));
encryptedRecordsPointer += 4;
}
encryptedIDs.set(i, data);
}
}
const stringTable = /* @__PURE__ */ new Map();
let stringTableDelta = 0;
const sectionsOffset = encryptedRecordsPointer;
let sectionPointer = sectionsOffset;
const sections = sectionHeaders.map((sectionHeader) => {
assert__default(
sectionPointer === sectionHeader.fileOffset,
`Invalid section offset: ${sectionPointer.toString()} != ${sectionHeader.fileOffset.toStrin