@rhyster/wow-casc-dbc
Version:
Fetch World of Warcraft data files from CASC and parse DBC/DB2 files.
221 lines (173 loc) • 8.48 kB
text/typescript
import assert from 'node:assert';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
import Salsa20 from './salsa20.ts';
interface Block {
compressedSize: number,
decompressedSize: number,
hash: string,
}
interface MissingKeyBlock {
offset: number,
size: number,
blockIndex: number,
keyName: string,
}
const BLTE_MAGIC = 0x424c5445;
const ENC_TYPE_SALSA20 = 0x53;
const EMPTY_HASH = '00000000000000000000000000000000';
export default class BLTEReader {
public buffer: Buffer;
public readonly blte: Buffer;
public readonly blocks: Block[] = [];
public readonly keys: Map<string, Uint8Array>;
private processedBlock = 0;
private processedOffset = 0;
constructor(buffer: Buffer, eKey: string, keys = new Map<string, Uint8Array>()) {
this.blte = buffer;
this.buffer = Buffer.alloc(0);
this.keys = keys;
const size = buffer.byteLength;
assert(size >= 8, `[BLTE]: Invalid size: ${size.toString()} < 8`);
const magic = buffer.readUInt32BE(0);
assert(magic === BLTE_MAGIC, `[BLTE]: Invalid magic: ${magic.toString(16).padStart(8, '0')}`);
const headerSize = buffer.readUInt32BE(4);
if (headerSize === 0) {
const blteHash = crypto.createHash('md5').update(buffer).digest('hex');
assert(blteHash === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash}`);
this.blocks.push({
compressedSize: size - 8,
decompressedSize: size - 9,
hash: EMPTY_HASH,
});
this.processedOffset = 8;
return;
}
const blteHash = crypto.createHash('md5').update(buffer.subarray(0, headerSize)).digest('hex');
assert(blteHash === eKey, `[BLTE]: Invalid hash: expected ${eKey}, got ${blteHash}`);
assert(size >= 12, `[BLTE]: Invalid size: ${size.toString()} < 12`);
const flag = buffer.readUInt8(8);
const numBlocks = buffer.readIntBE(9, 3);
assert(numBlocks > 0, `[BLTE]: Invalid number of blocks: ${numBlocks.toString()}`);
assert(flag === 0x0f, `[BLTE]: Invalid flag: ${flag.toString(16).padStart(2, '0')}`);
const blockHeaderSize = numBlocks * 24;
assert(headerSize === blockHeaderSize + 12, `[BLTE]: Invalid header size: header size ${headerSize.toString()} != block header size ${blockHeaderSize.toString()} + 12`);
assert(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;
}
private processBlock(buffer: Buffer, index: number, allowMissingKey: false): Buffer;
private processBlock(buffer: Buffer, index: number, allowMissingKey: true): Buffer | string;
private processBlock(buffer: Buffer, index: number, allowMissingKey: boolean): Buffer | string {
const flag = buffer.readUInt8(0);
switch (flag) {
case 0x45: { // Encrypted
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(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) {
// eslint-disable-next-line no-bitwise
iv[i] = ivBuffer.readUInt8(i) ^ ((index >>> (8 * i)) & 0xff);
} else {
iv[i] = 0x00;
}
}
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 0x46: // Frame (Recursive)
throw new Error(`[BLTE]: Frame (Recursive) block not supported at block ${index.toString()}`);
case 0x4e: // Frame (Normal)
return buffer.subarray(1);
case 0x5a: // Compressed
return zlib.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?: number): undefined;
processBytes(allowMissingKey: true, size?: number): MissingKeyBlock[];
processBytes(allowMissingKey = false, size = Infinity): MissingKeyBlock[] | undefined {
const missingKeyBlocks: MissingKeyBlock[] = [];
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.createHash('md5').update(blockBuffer).digest('hex');
assert(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(
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(
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 : undefined;
}
}
export type { MissingKeyBlock };