@rhyster/wow-casc-dbc
Version:
Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.
1 lines • 211 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../src/adb.ts","../src/salsa20.ts","../src/blte.ts","../src/store.ts","../src/fetcher.ts","../src/jenkins96.ts","../src/parsers/archiveIndex.ts","../src/parsers/config.ts","../src/parsers/encodingFile.ts","../src/parsers/installFile.ts","../src/parsers/productConfig.ts","../src/parsers/rootFile.ts","../src/utils.ts","../src/wdc.ts","../src/client.ts","../src/dbd.ts"],"sourcesContent":["import assert from 'node:assert';\n\nconst ADB_MAGIC = 0x58465448;\n\nexport interface HotfixEntry {\n regionID: number,\n pushID: number,\n uniqueID: number,\n tableHash: number,\n recordID: number,\n dataSize: number,\n recordState: number,\n data: Buffer,\n}\n\nexport default class ADBReader {\n public build: number;\n\n public entries: HotfixEntry[] = [];\n\n public tableEntries = new Map<number, HotfixEntry[]>();\n\n constructor(buffer: Buffer) {\n const magic = buffer.readUInt32BE(0);\n assert(magic === ADB_MAGIC, `[ADB]: Invalid magic: ${magic.toString(16).padStart(8, '0')}`);\n\n const version = buffer.readUInt32LE(4);\n assert(version === 9, `[ADB]: Invalid version: ${version.toString()}`);\n\n const build = buffer.readUInt32LE(8);\n this.build = build;\n\n let pointer = 44;\n while (pointer < buffer.byteLength) {\n const offset = pointer;\n\n const entryMagic = buffer.readUInt32BE(offset);\n assert(entryMagic === ADB_MAGIC, `[ADB]: Invalid entry magic: ${magic.toString(16).padStart(8, '0')}`);\n\n const regionID = buffer.readInt32LE(offset + 4);\n const pushID = buffer.readInt32LE(offset + 8);\n const uniqueID = buffer.readUInt32LE(offset + 12);\n const tableHash = buffer.readUInt32LE(offset + 16);\n const recordID = buffer.readUInt32LE(offset + 20);\n const dataSize = buffer.readUInt32LE(offset + 24);\n const recordState = buffer.readUInt32LE(offset + 28);\n\n const data = buffer.subarray(offset + 32, offset + 32 + dataSize);\n\n const entry: HotfixEntry = {\n regionID,\n pushID,\n uniqueID,\n tableHash,\n recordID,\n dataSize,\n recordState,\n data,\n };\n this.entries.push(entry);\n\n if (!this.tableEntries.has(tableHash)) {\n this.tableEntries.set(tableHash, []);\n }\n this.tableEntries.get(tableHash)?.push(entry);\n\n pointer += 32 + dataSize;\n }\n }\n}\n","/* eslint-disable no-bitwise */\n\nimport assert from 'node:assert';\n\nexport default class Salsa20 {\n private readonly fixed: Uint32Array;\n\n private readonly key: Uint32Array;\n\n private readonly nonce: Uint32Array;\n\n private counter = new Uint32Array([0, 0]);\n\n private state = new Uint32Array(16);\n\n private block = new Uint8Array(64);\n\n private position = 0;\n\n constructor(key: Uint8Array, nonce: Uint8Array) {\n assert(key.length === 32 || key.length === 16, 'Salsa20 requires 128-bit or 256-bit key');\n assert(nonce.length === 8, 'Salsa20 requires 64-bit nonce');\n\n this.key = new Uint32Array(8);\n const keyView = new DataView(key.buffer);\n if (key.length === 32) {\n for (let i = 0; i < 8; i += 1) {\n this.key[i] = keyView.getUint32(i * 4, true);\n }\n this.fixed = new Uint32Array([\n 0x61707865,\n 0x3320646e,\n 0x79622d32,\n 0x6b206574,\n ]);\n } else {\n for (let i = 0; i < 4; i += 1) {\n const word = keyView.getUint32(i * 4, true);\n this.key[i] = word;\n this.key[i + 4] = word;\n }\n this.fixed = new Uint32Array([\n 0x61707865,\n 0x3120646e,\n 0x79622d36,\n 0x6b206574,\n ]);\n }\n\n this.nonce = new Uint32Array(2);\n const nonceView = new DataView(nonce.buffer);\n for (let i = 0; i < 2; i += 1) {\n this.nonce[i] = nonceView.getUint32(i * 4, true);\n }\n\n this.generateBlock();\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n private QR(a: number, b: number, c: number, d: number) {\n let t: number;\n\n t = (this.state[a] + this.state[d]) & 0xffffffff;\n this.state[b] ^= (t << 7) | (t >>> 25);\n\n t = (this.state[b] + this.state[a]) & 0xffffffff;\n this.state[c] ^= (t << 9) | (t >>> 23);\n\n t = (this.state[c] + this.state[b]) & 0xffffffff;\n this.state[d] ^= (t << 13) | (t >>> 19);\n\n t = (this.state[d] + this.state[c]) & 0xffffffff;\n this.state[a] ^= (t << 18) | (t >>> 14);\n }\n\n private generateBlock() {\n const init = new Uint32Array([\n this.fixed[0],\n this.key[0],\n this.key[1],\n this.key[2],\n\n this.key[3],\n this.fixed[1],\n this.nonce[0],\n this.nonce[1],\n\n this.counter[0],\n this.counter[1],\n this.fixed[2],\n this.key[4],\n\n this.key[5],\n this.key[6],\n this.key[7],\n this.fixed[3],\n ]);\n this.state = new Uint32Array(init);\n\n for (let i = 0; i < 20; i += 2) {\n // Odd round\n this.QR(0, 4, 8, 12);\n this.QR(5, 9, 13, 1);\n this.QR(10, 14, 2, 6);\n this.QR(15, 3, 7, 11);\n // Even round\n this.QR(0, 1, 2, 3);\n this.QR(5, 6, 7, 4);\n this.QR(10, 11, 8, 9);\n this.QR(15, 12, 13, 14);\n }\n\n for (let i = 0; i < 16; i += 1) {\n const word = (this.state[i] + init[i]) & 0xffffffff;\n this.block[i * 4] = word & 0xff;\n this.block[i * 4 + 1] = (word >>> 8) & 0xff;\n this.block[i * 4 + 2] = (word >>> 16) & 0xff;\n this.block[i * 4 + 3] = (word >>> 24) & 0xff;\n }\n\n this.counter[0] = (this.counter[0] + 1) & 0xffffffff;\n if (this.counter[0] === 0) {\n this.counter[1] = (this.counter[1] + 1) & 0xffffffff;\n }\n }\n\n process(input: Uint8Array): Uint8Array {\n const { length } = input;\n const result = new Uint8Array(length);\n\n for (let i = 0; i < length; i += 1) {\n if (this.position === 64) {\n this.generateBlock();\n this.position = 0;\n }\n\n result[i] = input[i] ^ this.block[this.position];\n this.position += 1;\n }\n\n return result;\n }\n}\n","import assert from 'node:assert';\nimport crypto from 'node:crypto';\nimport zlib from 'node:zlib';\n\nimport Salsa20 from './salsa20.ts';\n\ninterface Block {\n compressedSize: number,\n decompressedSize: number,\n hash: string,\n}\n\ninterface MissingKeyBlock {\n offset: number,\n size: number,\n blockIndex: number,\n keyName: string,\n}\n\nconst BLTE_MAGIC = 0x424c5445;\nconst ENC_TYPE_SALSA20 = 0x53;\nconst EMPTY_HASH = '00000000000000000000000000000000';\n\nexport default class BLTEReader {\n public buffer: Buffer;\n\n public readonly blte: Buffer;\n\n public readonly blocks: Block[] = [];\n\n public readonly keys: Map<string, Uint8Array>;\n\n private processedBlock = 0;\n\n private processedOffset = 0;\n\n constructor(buffer: Buffer, eKey: string, keys = new Map<string, Uint8Array>()) {\n this.blte = buffer;\n this.buffer = Buffer.alloc(0);\n this.keys = keys;\n\n const size = buffer.byteLength;\n assert(size >= 8, `[BLTE]: Invalid size: ${size.toString()} < 8`);\n\n const magic = buffer.readUInt32BE(0);\n assert(magic === BLTE_MAGIC, `[BLTE]: Invalid magic: ${magic.toString(16).padStart(8, '0')}`);\n\n const headerSize = buffer.readUInt32BE(4);\n if (headerSize === 0) {\n const blteHash = crypto.createHash('md5').update(buffer).digest('hex');\n assert(blteHash === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash}`);\n\n this.blocks.push({\n compressedSize: size - 8,\n decompressedSize: size - 9,\n hash: EMPTY_HASH,\n });\n this.processedOffset = 8;\n\n return;\n }\n\n const blteHash = crypto.createHash('md5').update(buffer.subarray(0, headerSize)).digest('hex');\n assert(blteHash === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash}`);\n\n assert(size >= 12, `[BLTE]: Invalid size: ${size.toString()} < 12`);\n\n const flag = buffer.readUInt8(8);\n const numBlocks = buffer.readIntBE(9, 3);\n\n assert(numBlocks > 0, `[BLTE]: Invalid number of blocks: ${numBlocks.toString()}`);\n assert(flag === 0x0f, `[BLTE]: Invalid flag: ${flag.toString(16).padStart(2, '0')}`);\n\n const blockHeaderSize = numBlocks * 24;\n assert(headerSize === blockHeaderSize + 12, `[BLTE]: Invalid header size: header size ${headerSize.toString()} != block header size ${blockHeaderSize.toString()} + 12`);\n\n assert(size >= headerSize, `[BLTE]: Invalid size: ${size.toString()} < ${headerSize.toString()}`);\n\n for (let i = 0; i < numBlocks; i += 1) {\n const offset = 12 + i * 24;\n const compressedSize = buffer.readUInt32BE(offset);\n const decompressedSize = buffer.readUInt32BE(offset + 4);\n const hash = buffer.toString('hex', offset + 8, offset + 24);\n\n this.blocks.push({\n compressedSize,\n decompressedSize,\n hash,\n });\n }\n\n this.processedOffset = headerSize;\n }\n\n private processBlock(buffer: Buffer, index: number, allowMissingKey: false): Buffer;\n private processBlock(buffer: Buffer, index: number, allowMissingKey: true): Buffer | string;\n private processBlock(buffer: Buffer, index: number, allowMissingKey: boolean): Buffer | string {\n const flag = buffer.readUInt8(0);\n switch (flag) {\n case 0x45: { // Encrypted\n let offset = 1;\n\n const keyNameLength = buffer.readUInt8(offset);\n offset += 1;\n\n const keyNameBE = buffer.toString('hex', offset, offset + keyNameLength);\n offset += keyNameLength;\n\n const ivLength = buffer.readUInt8(offset);\n offset += 1;\n\n const ivBuffer = buffer.subarray(offset, offset + ivLength);\n offset += ivLength;\n\n const encryptType = buffer.readUInt8(offset);\n offset += 1;\n\n assert(encryptType === ENC_TYPE_SALSA20, `[BLTE]: Invalid encrypt type: ${encryptType.toString(16).padStart(2, '0')} at block ${index.toString()}`);\n\n const keyName = [...keyNameBE.matchAll(/.{2}/g)].map((v) => v[0]).reverse().join('').toLowerCase();\n const key = this.keys.get(keyName);\n if (!key) {\n if (allowMissingKey) {\n return keyName;\n }\n throw new Error(`[BLTE]: Missing key: ${keyName} at block ${index.toString()}`);\n }\n\n const iv = new Uint8Array(8);\n for (let i = 0; i < 8; i += 1) {\n if (i < ivLength) {\n // eslint-disable-next-line no-bitwise\n iv[i] = ivBuffer.readUInt8(i) ^ ((index >>> (8 * i)) & 0xff);\n } else {\n iv[i] = 0x00;\n }\n }\n\n const handler = new Salsa20(key, iv);\n const decrypted = handler.process(buffer.subarray(offset));\n\n if (allowMissingKey) {\n return this.processBlock(Buffer.from(decrypted.buffer), index, true);\n }\n return this.processBlock(Buffer.from(decrypted.buffer), index, false);\n }\n case 0x46: // Frame (Recursive)\n throw new Error(`[BLTE]: Frame (Recursive) block not supported at block ${index.toString()}`);\n case 0x4e: // Frame (Normal)\n return buffer.subarray(1);\n case 0x5a: // Compressed\n return zlib.inflateSync(buffer.subarray(1));\n default:\n throw new Error(`[BLTE]: Invalid block flag: ${flag.toString(16).padStart(2, '0')} at block ${index.toString()}`);\n }\n }\n\n processBytes(allowMissingKey?: false, size?: number): undefined;\n processBytes(allowMissingKey: true, size?: number): MissingKeyBlock[];\n processBytes(allowMissingKey = false, size = Infinity): MissingKeyBlock[] | undefined {\n const missingKeyBlocks: MissingKeyBlock[] = [];\n\n while (\n this.processedBlock < this.blocks.length\n && size > this.buffer.byteLength\n ) {\n const blockIndex = this.processedBlock;\n const block = this.blocks[blockIndex];\n\n const blockBuffer = this.blte.subarray(\n this.processedOffset,\n this.processedOffset + block.compressedSize,\n );\n if (block.hash !== EMPTY_HASH) {\n const blockHash = crypto.createHash('md5').update(blockBuffer).digest('hex');\n assert(blockHash === block.hash, `[BLTE]: Invalid block hash: expected ${block.hash}, got ${blockHash}`);\n }\n\n if (allowMissingKey) {\n const buffer = this.processBlock(blockBuffer, blockIndex, allowMissingKey);\n if (typeof buffer === 'string') {\n missingKeyBlocks.push({\n offset: this.buffer.byteLength,\n size: block.decompressedSize,\n blockIndex,\n keyName: buffer,\n });\n\n this.buffer = Buffer.concat([\n this.buffer,\n Buffer.alloc(block.decompressedSize),\n ]);\n } else {\n assert(\n buffer.byteLength === block.decompressedSize,\n `[BLTE]: Invalid decompressed size: expected ${block.decompressedSize.toString()}, got ${buffer.byteLength.toString()}`,\n );\n\n this.buffer = Buffer.concat([this.buffer, buffer]);\n }\n } else {\n const buffer = this.processBlock(blockBuffer, blockIndex, allowMissingKey);\n\n assert(\n buffer.byteLength === block.decompressedSize,\n `[BLTE]: Invalid decompressed size: expected ${block.decompressedSize.toString()}, got ${buffer.byteLength.toString()}`,\n );\n\n this.buffer = Buffer.concat([this.buffer, buffer]);\n }\n\n this.processedBlock += 1;\n this.processedOffset += block.compressedSize;\n }\n\n return allowMissingKey ? missingKeyBlocks : undefined;\n }\n}\n\nexport type { MissingKeyBlock };\n","import fs from 'node:fs/promises';\n\nexport default class Store<K extends string | number | symbol, V> {\n private data: Record<K, V | undefined>;\n\n private dataFile: string;\n\n private promise: Promise<void>;\n\n constructor(dataFile: string) {\n this.dataFile = dataFile;\n this.data = {} as Record<K, V | undefined>;\n\n this.promise = new Promise((resolve) => {\n fs\n .readFile(dataFile, 'utf-8')\n .then((file) => {\n this.data = JSON.parse(file) as Record<K, V | undefined>;\n resolve();\n })\n .catch(() => {\n resolve();\n });\n });\n }\n\n public async get(key: K): Promise<V | undefined> {\n await this.promise;\n return this.data[key];\n }\n\n public async set(key: K, value: V): Promise<void> {\n await this.promise;\n this.data[key] = value;\n await fs.writeFile(this.dataFile, JSON.stringify(this.data), 'utf-8');\n }\n}\n","import crypto from 'node:crypto';\nimport fs from 'node:fs/promises';\nimport http from 'node:http';\nimport path from 'node:path';\n\nimport cliProgress from 'cli-progress';\n\nimport Store from './store.ts';\n\nconst USER_AGENT = 'node-wow-casc-dbc';\n\nconst CACHE_ROOT = path.resolve('cache');\nconst CACHE_DIRS = {\n build: 'builds',\n indexes: 'indices',\n data: 'data',\n dbd: 'dbd',\n};\n\nconst CACHE_INTEGRITY_FILE = path.resolve(CACHE_ROOT, 'integrity.json');\n\nconst cacheIntegrity = new Store<string, string>(CACHE_INTEGRITY_FILE);\n\nconst formatCDNKey = (key: string): string => `${key.substring(0, 2)}/${key.substring(2, 4)}/${key}`;\n\nconst requestData = async (\n url: string,\n {\n partialOffset,\n partialLength,\n showProgress,\n }: {\n partialOffset?: number,\n partialLength?: number,\n showProgress?: boolean,\n } = {},\n): Promise<Buffer> => new Promise((resolve, reject) => {\n const options = {\n headers: {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n 'User-Agent': USER_AGENT,\n // eslint-disable-next-line @typescript-eslint/naming-convention\n Range: partialOffset !== undefined && partialLength !== undefined\n ? `bytes=${partialOffset.toString()}-${(partialOffset + partialLength - 1).toString()}`\n : 'bytes=0-',\n },\n };\n\n http.get(url, options, (res) => {\n if (res.statusCode === 301 || res.statusCode === 302) {\n if (res.headers.location !== undefined) {\n requestData(res.headers.location, { partialOffset, partialLength, showProgress })\n .then(resolve)\n .catch((err: unknown) => {\n throw err;\n });\n } else {\n reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode.toString()}`));\n }\n return;\n }\n\n if (res.statusCode === undefined || res.statusCode < 200 || res.statusCode > 302) {\n reject(new Error(`Failed to request ${url}, Status Code: ${res.statusCode?.toString() ?? 'undefined'}`));\n return;\n }\n\n const lengthText = res.headers['content-length'];\n const length = lengthText !== undefined ? parseInt(lengthText, 10) : 0;\n const bar = showProgress === true && !Number.isNaN(length) && length >= 10485760\n ? new cliProgress.SingleBar({ etaBuffer: 10240 }, cliProgress.Presets.shades_classic)\n : undefined;\n bar?.start(length, 0);\n\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => {\n bar?.increment(chunk.length);\n chunks.push(chunk);\n });\n res.on('end', () => {\n bar?.stop();\n resolve(Buffer.concat(chunks));\n });\n res.on('error', (err) => {\n bar?.stop();\n reject(err);\n });\n })\n .on('error', reject)\n .end();\n});\n\nconst downloadFile = (\n prefixes: string[],\n type: 'data' | 'config',\n key: string,\n {\n partialOffset,\n partialLength,\n showProgress,\n showAttemptFail,\n }: {\n partialOffset?: number,\n partialLength?: number,\n showProgress?: boolean,\n showAttemptFail?: boolean,\n } = {},\n): Promise<Buffer> => {\n const urls = prefixes.map((prefix) => `${prefix}/${type}/${formatCDNKey(key)}`);\n\n return urls\n .reduce(\n (prev, url, index) => prev\n .catch((err: unknown) => {\n if (showAttemptFail === true && index > 0 && err instanceof Error) {\n console.warn(`${new Date().toISOString()} [WARN]:`, err.message);\n }\n return requestData(url, { partialOffset, partialLength, showProgress });\n }),\n Promise.reject<Buffer>(new Error('')),\n );\n};\n\nconst getFileCache = async (file: string): Promise<Buffer | undefined> => {\n const integrity = await cacheIntegrity.get(file);\n if (integrity !== undefined) {\n try {\n const buffer = await fs.readFile(path.resolve(CACHE_ROOT, file));\n const hash = crypto.createHash('sha256').update(buffer).digest('hex');\n if (hash === integrity) {\n return buffer;\n }\n } catch {\n // ignore\n }\n }\n return undefined;\n};\n\nexport const getDataFile = async (\n prefixes: string[],\n key: string,\n type: keyof typeof CACHE_DIRS,\n buildCKey: string,\n {\n name,\n partialOffset,\n partialLength,\n showProgress,\n showAttemptFail,\n }: {\n name?: string,\n partialOffset?: number,\n partialLength?: number,\n showProgress?: boolean,\n showAttemptFail?: boolean,\n } = {},\n): Promise<Buffer> => {\n const dir = type === 'build'\n ? path.join(CACHE_DIRS[type], buildCKey)\n : CACHE_DIRS[type];\n const file = name !== undefined ? path.join(dir, name) : path.join(dir, key);\n const cacheBuffer = await getFileCache(file);\n\n if (cacheBuffer) {\n if (name === undefined && partialOffset !== undefined && partialLength !== undefined) {\n return cacheBuffer.subarray(partialOffset, partialOffset + partialLength);\n }\n return cacheBuffer;\n }\n\n const downloadBuffer = await downloadFile(prefixes, 'data', key, {\n partialOffset, partialLength, showProgress, showAttemptFail,\n });\n if ((partialOffset === undefined && partialLength === undefined) || name !== undefined) {\n await fs.mkdir(path.resolve(CACHE_ROOT, dir), { recursive: true });\n await fs.writeFile(path.resolve(CACHE_ROOT, file), downloadBuffer);\n\n const hash = crypto.createHash('sha256').update(downloadBuffer).digest('hex');\n await cacheIntegrity.set(file, hash);\n }\n\n return downloadBuffer;\n};\n\nexport const getConfigFile = async (\n prefixes: string[],\n key: string,\n {\n showProgress, showAttemptFail,\n }: {\n showProgress?: boolean, showAttemptFail?: boolean,\n } = {},\n): Promise<string> => {\n const downloadBuffer = await downloadFile(prefixes, 'config', key, { showProgress, showAttemptFail });\n return downloadBuffer.toString('utf-8');\n};\n\nexport const getProductVersions = async (\n region: string,\n product: string,\n): Promise<string> => {\n const url = `http://${region}.patch.battle.net:1119/${product}/versions`;\n const headers = new Headers();\n headers.set('User-Agent', USER_AGENT);\n\n const res = await fetch(url, { headers });\n\n return res.text();\n};\n\nexport const getProductCDNs = async (\n region: string,\n product: string,\n): Promise<string> => {\n const url = `http://${region}.patch.battle.net:1119/${product}/cdns`;\n const headers = new Headers();\n headers.set('User-Agent', USER_AGENT);\n\n const res = await fetch(url, { headers });\n\n return res.text();\n};\n","/* eslint-disable no-bitwise */\n\nconst hashlittle2 = (key: string, pc = 0, pb = 0): [number, number] => {\n const { length } = key;\n let offset = 0;\n\n let a = 0xdeadbeef + length + pc | 0;\n let b = 0xdeadbeef + length + pc | 0;\n let c = 0xdeadbeef + length + pc + pb | 0;\n\n while (length - offset > 12) {\n a += key.charCodeAt(offset + 0);\n a += key.charCodeAt(offset + 1) << 8;\n a += key.charCodeAt(offset + 2) << 16;\n a += key.charCodeAt(offset + 3) << 24;\n\n b += key.charCodeAt(offset + 4);\n b += key.charCodeAt(offset + 5) << 8;\n b += key.charCodeAt(offset + 6) << 16;\n b += key.charCodeAt(offset + 7) << 24;\n\n c += key.charCodeAt(offset + 8);\n c += key.charCodeAt(offset + 9) << 8;\n c += key.charCodeAt(offset + 10) << 16;\n c += key.charCodeAt(offset + 11) << 24;\n\n // mix(a, b, c);\n a -= c; a ^= (c << 4) | (c >>> 28); c = c + b | 0;\n b -= a; b ^= (a << 6) | (a >>> 26); a = a + c | 0;\n c -= b; c ^= (b << 8) | (b >>> 24); b = b + a | 0;\n a -= c; a ^= (c << 16) | (c >>> 16); c = c + b | 0;\n b -= a; b ^= (a << 19) | (a >>> 13); a = a + c | 0;\n c -= b; c ^= (b << 4) | (b >>> 28); b = b + a | 0;\n\n offset += 12;\n }\n\n if (length - offset > 0) {\n // zero length strings require no mixing\n // eslint-disable-next-line default-case\n switch (length - offset) {\n case 12: c += key.charCodeAt(offset + 11) << 24; // falls through\n case 11: c += key.charCodeAt(offset + 10) << 16; // falls through\n case 10: c += key.charCodeAt(offset + 9) << 8; // falls through\n case 9: c += key.charCodeAt(offset + 8); // falls through\n case 8: b += key.charCodeAt(offset + 7) << 24; // falls through\n case 7: b += key.charCodeAt(offset + 6) << 16; // falls through\n case 6: b += key.charCodeAt(offset + 5) << 8; // falls through\n case 5: b += key.charCodeAt(offset + 4); // falls through\n case 4: a += key.charCodeAt(offset + 3) << 24; // falls through\n case 3: a += key.charCodeAt(offset + 2) << 16; // falls through\n case 2: a += key.charCodeAt(offset + 1) << 8; // falls through\n case 1: a += key.charCodeAt(offset + 0);\n }\n\n // final(a, b, c);\n c ^= b; c -= (b << 14) | (b >>> 18);\n a ^= c; a -= (c << 11) | (c >>> 21);\n b ^= a; b -= (a << 25) | (a >>> 7);\n c ^= b; c -= (b << 16) | (b >>> 16);\n a ^= c; a -= (c << 4) | (c >>> 28);\n b ^= a; b -= (a << 14) | (a >>> 18);\n c ^= b; c -= (b << 24) | (b >>> 8);\n }\n\n return [c >>> 0, b >>> 0];\n};\n\nconst getNameHash = (name: string): string => {\n const normalized = name.replace(/\\//g, '\\\\').toUpperCase();\n const [pc, pb] = hashlittle2(normalized);\n return `${pc.toString(16).padStart(8, '0')}${pb.toString(16).padStart(8, '0')}`;\n};\n\nexport default getNameHash;\n","import assert from 'node:assert';\nimport crypto from 'node:crypto';\n\nconst VERSION_SUB_OFFSET = -12;\nconst CHECKSUM_SIZE_SUB_OFFSET = -5;\n\nconst BLOCK_SIZE_OFFSET = 3;\nconst OFFSET_BYTES_OFFSET = 4;\nconst SIZE_BYTES_OFFSET = 5;\nconst KEY_SIZE_OFFSET = 6;\n// const CHECKSUM_SIZE_OFFSET = 7;\nconst NUM_ELEMENTS_OFFSET = 8;\nconst CHECKSUM_OFFSET = 12;\n\nconst CHECKSUM_TRIES = [\n 10,\n 9,\n 8,\n 7,\n 6,\n 5,\n 4,\n 3,\n 2,\n 1,\n 0,\n];\n\ninterface ArchiveIndex {\n key: string,\n size: number,\n offset: number,\n}\n\nconst tryArchiveIndexChecksumSize = (buffer: Buffer, cKey: string): number => {\n const res = CHECKSUM_TRIES.filter(\n (index) => (\n buffer.readUInt8(buffer.byteLength - index + CHECKSUM_SIZE_SUB_OFFSET) === index\n && buffer.readUInt8(buffer.byteLength - index + VERSION_SUB_OFFSET) === 1\n ),\n );\n\n if (res.length === 1) {\n return res[0];\n }\n\n throw new Error(`Invalid checksum size: ${res.join(', ')} in ${cKey}`);\n};\n\nconst parseArchiveIndex = (buffer: Buffer, cKey: string): Map<string, ArchiveIndex> => {\n const checksumSize = tryArchiveIndexChecksumSize(buffer, cKey);\n\n const versionOffset = buffer.byteLength - checksumSize + VERSION_SUB_OFFSET;\n const footerOffset = versionOffset - checksumSize;\n\n const tocChecksum = buffer.toString('hex', footerOffset, versionOffset);\n const version = buffer.readUInt8(versionOffset);\n const blockSizeKB = buffer.readUInt8(versionOffset + BLOCK_SIZE_OFFSET);\n const offsetBytes = buffer.readUInt8(versionOffset + OFFSET_BYTES_OFFSET);\n const sizeBytes = buffer.readUInt8(versionOffset + SIZE_BYTES_OFFSET);\n const keySize = buffer.readUInt8(versionOffset + KEY_SIZE_OFFSET);\n const numElements = buffer.readUInt32LE(versionOffset + NUM_ELEMENTS_OFFSET);\n const footerChecksum = buffer.toString('hex', versionOffset + CHECKSUM_OFFSET);\n\n assert(version === 1, `Invalid archive index version: ${version.toString()} in ${cKey}`);\n\n const entrySize = keySize + offsetBytes + sizeBytes;\n const blockSize = blockSizeKB * 1024;\n const numBlocks = footerOffset / (blockSize + keySize + checksumSize);\n const tocSize = (keySize + checksumSize) * numBlocks;\n const toc = buffer.subarray(footerOffset - tocSize, footerOffset);\n const footer = buffer.subarray(footerOffset);\n const footerCheckBuffer = Buffer.concat([\n buffer.subarray(versionOffset, buffer.byteLength - checksumSize),\n Buffer.alloc(checksumSize),\n ]);\n\n const hash = crypto.createHash('md5').update(footer).digest('hex');\n assert(hash === cKey, `Invalid footer hash in ${cKey}: expected ${cKey}, got ${hash}`);\n\n const footerHash = crypto.createHash('md5').update(footerCheckBuffer).digest('hex').slice(0, checksumSize * 2);\n assert(footerHash === footerChecksum, `Invalid footer checksum in ${cKey}: expected ${footerChecksum}, got ${footerHash}`);\n\n const tocHash = crypto.createHash('md5').update(toc).digest('hex').slice(0, checksumSize * 2);\n assert(tocHash === tocChecksum, `Invalid toc checksum in ${cKey}: expected ${tocChecksum}, got ${tocHash}`);\n\n const result = new Map<string, ArchiveIndex>();\n for (let i = 0; i < numBlocks; i += 1) {\n const lastEkey = toc.toString('hex', i * keySize, (i + 1) * keySize);\n const blockChecksum = toc.toString('hex', numBlocks * keySize + i * checksumSize, numBlocks * keySize + (i + 1) * checksumSize);\n const blockOffset = i * blockSize;\n\n const blockHash = crypto.createHash('md5').update(buffer.subarray(i * blockSize, (i + 1) * blockSize)).digest('hex').slice(0, checksumSize * 2);\n assert(blockChecksum === blockHash, `Invalid block hash in ${cKey} at ${i.toString()}: expected ${blockChecksum}, got ${blockHash}`);\n\n let length = 0;\n while (length < blockSize) {\n const entryOffset = blockOffset + length * entrySize;\n const eKey = buffer.toString('hex', entryOffset, entryOffset + keySize);\n const size = buffer.readUIntBE(entryOffset + keySize, sizeBytes);\n const offset = buffer.readUIntBE(entryOffset + keySize + sizeBytes, offsetBytes);\n\n result.set(eKey, { key: cKey, size, offset });\n length += 1;\n\n if (eKey === lastEkey) {\n break;\n }\n }\n }\n\n assert(result.size === numElements, `Invalid number of elements: ${result.size.toString()} != ${numElements.toString()} in ${cKey}`);\n\n return result;\n};\n\nexport default parseArchiveIndex;\n\nexport type { ArchiveIndex };\n","import assert from 'node:assert';\n\nconst normalizeKey = (key: string): string => key\n .split('-')\n .map((part, index) => (\n index === 0\n ? part\n : `${part.charAt(0).toUpperCase()}${part.slice(1)}`\n ))\n .join('');\n\nconst parseConfig = (text: string): Record<string, string> => {\n const entries: Record<string, string> = {};\n\n text\n .split(/\\r?\\n/)\n .filter((line) => line.trim().length !== 0 && !line.startsWith('#'))\n .forEach((line) => {\n const match = /([^\\s]+)\\s?=\\s?(.*)/.exec(line);\n assert(match !== null, 'Invalid token encountered parsing CDN config');\n\n const [key, value] = match.slice(1);\n entries[normalizeKey(key)] = value;\n });\n\n return entries;\n};\n\ninterface CDNConfig {\n archives: string,\n archivesIndexSize: string,\n archiveGroup: string,\n patchArchives: string,\n patchArchivesIndexSize: string,\n patchArchiveGroup: string,\n fileIndex: string,\n fileIndexSize: string,\n patchFileIndex: string,\n patchFileIndexSize: string,\n}\n\nconst parseCDNConfig = (\n text: string,\n): CDNConfig => parseConfig(text) as unknown as CDNConfig;\n\ninterface BuildConfig {\n root: string,\n install: string,\n installSize: string,\n download: string,\n downloadSize: string,\n size: string,\n sizeSize: string,\n encoding: string,\n encodingSize: string,\n patchIndex: string,\n patchIndexSize: string,\n patch: string,\n patchSize: string,\n patchConfig: string,\n buildName: string,\n buildUid: string,\n buildProduct: string,\n buildPlaybuildInstaller: string,\n buildPartialPriority: string,\n vfsRoot: string,\n vfsRootSize: string,\n [key: `vfs${number}` | `vfs${number}Size`]: string,\n}\n\nconst parseBuildConfig = (\n text: string,\n): BuildConfig => parseConfig(text) as unknown as BuildConfig;\n\nexport { parseCDNConfig, parseBuildConfig };\n","import assert from 'node:assert';\nimport crypto from 'node:crypto';\n\nimport BLTEReader from '../blte.ts';\n\nconst ENC_MAGIC = 0x454e;\n\nconst MAGIC_OFFSET = 0;\nconst VERSION_OFFSET = 2;\nconst HASH_SIZE_CKEY_OFFSET = 3;\nconst HASH_SIZE_EKEY_OFFSET = 4;\nconst CKEY_PAGE_SIZE_OFFSET = 5;\nconst EKEY_PAGE_SIZE_OFFSET = 7;\nconst CKEY_PAGE_COUNT_OFFSET = 9;\nconst EKEY_PAGE_COUNT_OFFSET = 13;\n// const UNK11_OFFSET = 17;\nconst SPEC_BLOCK_SIZE_OFFSET = 18;\nconst SPEC_BLOCK_OFFSET = 22;\n\ninterface EncodingData {\n eSpec: string[],\n cKey2FileSize: Map<string, number>,\n cKey2EKey: Map<string, string | string[]>,\n eKey2ESpecIndex: Map<string, number>,\n eKey2FileSize: Map<string, number>,\n}\n\nconst parseEncodingFile = (inputBuffer: Buffer, eKey: string, cKey: string): EncodingData => {\n const reader = new BLTEReader(inputBuffer, eKey);\n reader.processBytes();\n\n const { buffer } = reader;\n\n const encodingHash = crypto.createHash('md5').update(buffer).digest('hex');\n assert(encodingHash === cKey, `Invalid encoding hash: expected ${cKey}, got ${encodingHash}`);\n\n const magic = buffer.readUInt16BE(MAGIC_OFFSET);\n assert(magic === ENC_MAGIC, `Invalid encoding magic: ${magic.toString(16).padStart(4, '0')}`);\n\n const version = buffer.readUInt8(VERSION_OFFSET);\n const hashSizeCKey = buffer.readUInt8(HASH_SIZE_CKEY_OFFSET);\n const hashSizeEKey = buffer.readUInt8(HASH_SIZE_EKEY_OFFSET);\n const cKeyPageSizeKB = buffer.readUInt16BE(CKEY_PAGE_SIZE_OFFSET);\n const eKeyPageSizeKB = buffer.readUInt16BE(EKEY_PAGE_SIZE_OFFSET);\n const cKeyPageCount = buffer.readUInt32BE(CKEY_PAGE_COUNT_OFFSET);\n const eKeyPageCount = buffer.readUInt32BE(EKEY_PAGE_COUNT_OFFSET);\n const specBlockSize = buffer.readUInt32BE(SPEC_BLOCK_SIZE_OFFSET);\n\n assert(version === 1, `Invalid encoding version: ${version.toString()}`);\n\n const eSpec: string[] = [];\n let eSpecStringStart = SPEC_BLOCK_OFFSET;\n for (\n let i = SPEC_BLOCK_OFFSET;\n i < SPEC_BLOCK_OFFSET + specBlockSize;\n i += 1\n ) {\n if (buffer[i] === 0x00) {\n eSpec.push(buffer.toString('ascii', eSpecStringStart, i));\n eSpecStringStart = i + 1;\n }\n }\n\n const cKey2FileSize = new Map<string, number>();\n const cKey2EKey = new Map<string, string | string[]>();\n const cKeyPageIndexOffset = SPEC_BLOCK_OFFSET + specBlockSize;\n const cKeyPageIndexEntrySize = hashSizeCKey + 0x10;\n const cKeyPageOffset = cKeyPageIndexOffset + cKeyPageIndexEntrySize * cKeyPageCount;\n const cKeyPageSize = cKeyPageSizeKB * 1024;\n for (let i = 0; i < cKeyPageCount; i += 1) {\n const indexOffset = cKeyPageIndexOffset + i * cKeyPageIndexEntrySize;\n const pageOffset = cKeyPageOffset + i * cKeyPageSize;\n\n const firstCKey = buffer.toString('hex', indexOffset, indexOffset + hashSizeCKey);\n const pageChecksum = buffer.toString('hex', indexOffset + hashSizeCKey, indexOffset + hashSizeCKey + 0x10);\n\n const pageBuffer = buffer.subarray(pageOffset, pageOffset + cKeyPageSize);\n const pageHash = crypto.createHash('md5').update(pageBuffer).digest('hex');\n assert(pageHash === pageChecksum, `Invalid ckey page ${i.toString()} checksum: expected ${pageChecksum}, got ${pageHash}`);\n\n const pageFirstCKey = pageBuffer.toString('hex', 6, 6 + hashSizeCKey);\n assert(pageFirstCKey === firstCKey, `Invalid ckey page ${i.toString()} first ckey: expected ${firstCKey}, got ${pageFirstCKey}`);\n\n let pagePointer = 0;\n while (pagePointer < cKeyPageSize) {\n const keyCount = pageBuffer.readUInt8(pagePointer);\n pagePointer += 1;\n if (keyCount === 0x00) {\n break;\n }\n\n const fileSize = pageBuffer.readUIntBE(pagePointer, 5);\n pagePointer += 5;\n\n const fileCKey = pageBuffer.toString('hex', pagePointer, pagePointer + hashSizeCKey);\n pagePointer += hashSizeCKey;\n\n cKey2FileSize.set(fileCKey, fileSize);\n\n if (keyCount === 1) {\n const fileEKey = pageBuffer.toString('hex', pagePointer, pagePointer + hashSizeEKey);\n cKey2EKey.set(fileCKey, fileEKey);\n pagePointer += hashSizeEKey;\n } else {\n const fileEKeys: string[] = [];\n for (let j = 0; j < keyCount; j += 1) {\n const fileEKey = pageBuffer.toString('hex', pagePointer, pagePointer + hashSizeEKey);\n fileEKeys.push(fileEKey);\n pagePointer += hashSizeEKey;\n }\n cKey2EKey.set(fileCKey, fileEKeys);\n }\n }\n }\n\n const eKey2ESpecIndex = new Map<string, number>();\n const eKey2FileSize = new Map<string, number>();\n const eKeyPageIndexOffset = cKeyPageOffset + cKeyPageSize * cKeyPageCount;\n const eKeyPageIndexEntrySize = hashSizeEKey + 0x10;\n const eKeyPageOffset = eKeyPageIndexOffset + eKeyPageIndexEntrySize * eKeyPageCount;\n const eKeyPageSize = eKeyPageSizeKB * 1024;\n const eKeyPageEntrySize = hashSizeEKey + 0x04 + 0x05;\n for (let i = 0; i < eKeyPageCount; i += 1) {\n const indexOffset = eKeyPageIndexOffset + i * eKeyPageIndexEntrySize;\n const pageOffset = eKeyPageOffset + i * eKeyPageSize;\n\n const firstEKey = buffer.toString('hex', indexOffset, indexOffset + hashSizeEKey);\n const pageChecksum = buffer.toString('hex', indexOffset + hashSizeEKey, indexOffset + hashSizeEKey + 0x10);\n\n const pageBuffer = buffer.subarray(pageOffset, pageOffset + eKeyPageSize);\n const pageHash = crypto.createHash('md5').update(pageBuffer).digest('hex');\n assert(pageHash === pageChecksum, `Invalid ekey page ${i.toString()} checksum: expected ${pageChecksum}, got ${pageHash}`);\n\n const pageFirstEKey = pageBuffer.toString('hex', 0, hashSizeEKey);\n assert(pageFirstEKey === firstEKey, `Invalid ekey page ${i.toString()} first ekey: expected ${firstEKey}, got ${pageFirstEKey}`);\n\n let pagePointer = 0;\n while (pagePointer + eKeyPageEntrySize <= eKeyPageSize) {\n const fileEKey = pageBuffer.toString('hex', pagePointer, pagePointer + hashSizeEKey);\n pagePointer += hashSizeEKey;\n\n const eSpecIndex = pageBuffer.readUInt32BE(pagePointer);\n pagePointer += 4;\n eKey2ESpecIndex.set(fileEKey, eSpecIndex);\n\n const fileSize = pageBuffer.readUIntBE(pagePointer, 5);\n pagePointer += 5;\n eKey2FileSize.set(fileEKey, fileSize);\n }\n }\n\n return {\n eSpec, cKey2FileSize, cKey2EKey, eKey2ESpecIndex, eKey2FileSize,\n };\n};\n\nexport default parseEncodingFile;\n\nexport type { EncodingData };\n","import assert from 'node:assert';\nimport crypto from 'node:crypto';\n\nimport BLTEReader from '../blte.ts';\n\nconst INSTALL_MAGIC = 0x494e;\n\nconst MAGIC_OFFSET = 0;\nconst VERSION_OFFSET = 2;\nconst HASH_SIZE_OFFSET = 3;\nconst NUM_TAGS_OFFSET = 4;\nconst NUM_ENTRIES_OFFSET = 6;\nconst TAGS_OFFSET = 10;\n\ninterface InstallTag {\n name: string,\n type: number,\n files: boolean[],\n}\n\ninterface InstallFile {\n name: string,\n hash: string,\n size: number,\n tags: InstallTag[],\n}\n\ninterface InstallData {\n tags: InstallTag[],\n files: InstallFile[],\n}\n\nconst parseInstallFile = (inputBuffer: Buffer, eKey: string, cKey: string): InstallData => {\n const reader = new BLTEReader(inputBuffer, eKey);\n reader.processBytes();\n\n const { buffer } = reader;\n\n const installHash = crypto.createHash('md5').update(buffer).digest('hex');\n assert(installHash === cKey, `Invalid root hash: expected ${cKey}, got ${installHash}`);\n\n const magic = buffer.readUInt16BE(MAGIC_OFFSET);\n assert(magic === INSTALL_MAGIC, `Invalid install magic: ${magic.toString(16).padStart(4, '0')}`);\n\n const version = buffer.readUInt8(VERSION_OFFSET);\n const hashSize = buffer.readUInt8(HASH_SIZE_OFFSET);\n const numTags = buffer.readUInt16BE(NUM_TAGS_OFFSET);\n const numEntries = buffer.readUInt32BE(NUM_ENTRIES_OFFSET);\n\n assert(version === 1, `Invalid install version: ${version.toString()}`);\n\n let pointer = TAGS_OFFSET;\n\n const tags: InstallTag[] = [];\n for (let i = 0; i < numTags; i += 1) {\n const startOffset = pointer;\n while (buffer[pointer] !== 0x00) {\n pointer += 1;\n }\n\n const name = buffer.toString('utf-8', startOffset, pointer);\n pointer += 1;\n\n const type = buffer.readUInt16BE(pointer);\n pointer += 2;\n\n const files = [];\n const finalOffset = pointer + Math.ceil(numEntries / 8);\n while (pointer < finalOffset) {\n const byte = buffer.readUInt8(pointer);\n pointer += 1;\n\n for (let j = 7; j >= 0; j -= 1) {\n // eslint-disable-next-line no-bitwise\n files.push((byte & (1 << j)) > 0);\n }\n }\n\n tags.push({ name, type, files });\n }\n\n const files: InstallFile[] = [];\n for (let i = 0; i < numEntries; i += 1) {\n const startOffset = pointer;\n while (buffer[pointer] !== 0x00) {\n pointer += 1;\n }\n\n const name = buffer.toString('utf-8', startOffset, pointer);\n pointer += 1;\n\n const hash = buffer.toString('hex', pointer, pointer + hashSize);\n pointer += hashSize;\n\n const size = buffer.readUInt32BE(pointer);\n pointer += 4;\n\n const fileTags = tags.filter((tag) => tag.files[i]);\n\n files.push({\n name,\n hash,\n size,\n tags: fileTags,\n });\n }\n\n return { tags, files };\n};\n\nexport default parseInstallFile;\n\nexport type { InstallFile, InstallData };\n","/* eslint-disable @typescript-eslint/naming-convention */\n\nconst parseProductConfig = (text: string): Record<string, string>[] => {\n const lines = text.split(/\\r?\\n/);\n\n // First line contains field definitions.\n // Example: Name!STRING:0|Path!STRING:0|Hosts!STRING:0|Servers!STRING:0|ConfigPath!STRING:0\n // Whitespace is replaced so that a field like 'Install Key' becomes 'InstallKey'.\n // This just improves coding readability when accessing the fields later on.\n const headers = lines[0]\n .split('|')\n .map((header) => header.split('!')[0].replace(' ', ''));\n\n const entries = lines\n .filter((line, index) => index > 0 && line.trim().length !== 0 && !line.startsWith('#'))\n .map((line) => {\n const node: Record<string, string> = {};\n const entryFields = line.split('|');\n for (let i = 0, n = entryFields.length; i < n; i += 1) {\n node[headers[i]] = entryFields[i];\n }\n\n return node;\n });\n\n return entries;\n};\n\ninterface Version {\n Region: string,\n BuildConfig: string,\n CDNConfig: string,\n KeyRing: string,\n BuildId: string,\n VersionsName: string,\n ProductConfig: string,\n}\n\nconst parseProductVersions = (\n text: string,\n): Version[] => parseProductConfig(text) as unknown as Version[];\n\ninterface CDN {\n Name: string,\n Path: string,\n Hosts: string,\n Servers: string,\n ConfigPath: string,\n}\n\nconst parseProductCDNs = (\n text: string,\n): CDN[] => parseProductConfig(text) as unknown as CDN[];\n\nexport { parseProductVersions, parseProductCDNs };\n\nexport type { Version };\n","/* eslint-disable @typescript-eslint/naming-convention */\n\nimport assert from 'node:assert';\nimport crypto from 'node:crypto';\n\nimport BLTEReader from '../blte.ts';\n\nconst MFST_MAGIC = 0x4d465354;\n\nconst ContentFlags = {\n Install: 0x4,\n LoadOnWindows: 0x8,\n LoadOnMacOS: 0x10,\n x86_32: 0x20,\n x86_64: 0x40,\n LowViolence: 0x80,\n DoNotLoad: 0x100,\n UpdatePlugin: 0x800,\n ARM64: 0x8000,\n Encrypted: 0x8000000,\n NoNameHash: 0x10000000,\n UncommonResolution: 0x20000000,\n Bundle: 0x40000000,\n NoCompression: 0x80000000,\n} as const;\n\nconst LocaleFlags = {\n enUS: 0x2,\n koKR: 0x4,\n frFR: 0x10,\n deDE: 0x20,\n zhCN: 0x40,\n esES: 0x80,\n zhTW: 0x100,\n enGB: 0x200,\n // enCN: 0x400,\n // enTW: 0x800,\n esMX: 0x1000,\n ruRU: 0x2000,\n ptBR: 0x4000,\n itIT: 0x8000,\n ptPT: 0x10000,\n} as const;\n\ninterface FileInfo {\n cKey: string,\n contentFlags: number,\n localeFlags: number,\n}\n\ninterface RootData {\n fileDataID2CKey: Map<number, FileInfo[]>,\n nameHash2FileDataID: Map<string, number>,\n}\n\nconst parseRootFile = (inputBuffer: Buffer, eKey: string, cKey: string): RootData => {\n const reader = new BLTEReader(inputBuffer, eKey);\n reader.processBytes();\n\n const { buffer } = reader;\n\n const rootHash = crypto.createHash('md5').update(buffer).digest('hex');\n assert(rootHash === cKey, `Invalid root hash: expected ${cKey}, got ${rootHash}`);\n\n const fileDataID2CKey = new Map<number, FileInfo[]>();\n const nameHash2FileDataID = new Map<string, number>();\n\n const magic = buffer.readUInt32LE(0);\n if (magic === MFST_MAGIC) {\n // post 8.2.0\n const firstEntry = buffer.readUInt32LE(4);\n const newFormat = firstEntry < 100; // post 10.1.7\n\n const headerSize = newFormat ? firstEntry : 12;\n const version = newFormat ? buffer.readUInt32LE(8) : 0;\n const totalFileCount = newFormat ? buffer.readUInt32LE(12) : firstEntry;\n const namedFileCount = newFormat ? buffer.readUInt32LE(16) : buffer.readUInt32LE(8);\n\n assert(version >= 0 && version <= 2, `Invalid root version: ${version.toString()}`);\n\n const allowNonNamedFiles = totalFileCount !== namedFileCount;\n\n let pointer = headerSize;\n while (pointer < buffer.byteLength) {\n const numRecords = buffer.readUInt32LE(pointer);\n\n let contentFlags = 0;\n let localeFlags = 0;\n if (version >= 2) {\n localeFlags = buffer.readUInt32LE(pointer + 4);\n\n const contentFlags1 = buffer.readUInt32LE(pointer + 8);\n const contentFlags2 = buffer.readUInt32LE(pointer + 12);\n const contentFlags3 = buffer.readUInt8(pointer + 16);\n // eslint-disable-next-line no-bitwise\n contentFlags = (contentFlags1 | contentFlags2 | (contentFlags3 << 17));\n\n pointer += 17;\n } else {\n contentFlags = buffer.readUInt32LE(pointer + 4);\n localeFlags = buffer.readUInt32LE(pointer + 8);\n pointer += 12;\n }\n\n const fileDataIDs = [];\n let currFileDataID = -1;\n for (let i = 0; i < numRecords; i += 1) {\n currFileDataID += buffer.readUInt32LE(pointer) + 1;\n fileDataIDs.push(currFileDataID);\n pointer += 4;\n }\n\n for (let i = 0; i < numRecords; i += 1) {\n const fileDataID = fileDataIDs[i];\n const fileCKey = buffer.toString('hex', pointer, pointer + 16);\n pointer += 16;\n\n if (fileDataID2CKey.has(fileDataID)) {\n fileDataID2CKey.get(fileDataID)?.push({\n cKey: fileCKey,\n contentFlags,\n localeFlags,\n });\n } else {\n fileDataID2CKey.set(fileDataID, [\n { cKey: fileCKey, contentFlags, localeFlags },\n ]);\n }\n }\n\n // eslint-disable-next-line no-bitwise\n if (!(allowNonNamedFiles && (contentFlags & ContentFlags.NoNameHash))) {\n for (let i = 0; i < numRecords; i += 1) {\n const fileDataID = fileD