UNPKG

@rhyster/wow-casc-dbc

Version:

Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.

120 lines (95 loc) 4.59 kB
import assert from 'node:assert'; import crypto from 'node:crypto'; 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 CHECKSUM_SIZE_OFFSET = 7; const NUM_ELEMENTS_OFFSET = 8; const CHECKSUM_OFFSET = 12; const CHECKSUM_TRIES = [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ]; interface ArchiveIndex { key: string, size: number, offset: number, } const tryArchiveIndexChecksumSize = (buffer: Buffer, cKey: string): number => { 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: Buffer, cKey: string): Map<string, ArchiveIndex> => { 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(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.createHash('md5').update(footer).digest('hex'); assert(hash === cKey, `Invalid footer hash in ${cKey}: expected ${cKey}, got ${hash}`); const footerHash = crypto.createHash('md5').update(footerCheckBuffer).digest('hex').slice(0, checksumSize * 2); assert(footerHash === footerChecksum, `Invalid footer checksum in ${cKey}: expected ${footerChecksum}, got ${footerHash}`); const tocHash = crypto.createHash('md5').update(toc).digest('hex').slice(0, checksumSize * 2); assert(tocHash === tocChecksum, `Invalid toc checksum in ${cKey}: expected ${tocChecksum}, got ${tocHash}`); const result = new Map<string, ArchiveIndex>(); 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.createHash('md5').update(buffer.subarray(i * blockSize, (i + 1) * blockSize)).digest('hex').slice(0, checksumSize * 2); assert(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(result.size === numElements, `Invalid number of elements: ${result.size.toString()} != ${numElements.toString()} in ${cKey}`); return result; }; export default parseArchiveIndex; export type { ArchiveIndex };