UNPKG

ts-ritofile

Version:

TypeScript library for reading and writing League of Legends game file formats

227 lines (193 loc) 6.47 kB
/** * RST (String Table) format implementation * 1:1 port of Python pyritofile RST class */ import * as fs from 'fs'; import { BinStream } from '../stream/bin-stream'; import { JsonSerializable } from '../core/json-encoder'; import { xxh3_64_intdigest } from '../hash/hash-utils'; export class RSTError extends Error { constructor(message: string) { super(message); this.name = 'RSTError'; } } export class RST implements JsonSerializable { public entries: Record<string, string> = {}; public version: number = 5; public hash_bits: number = 38; public has_trenc: boolean = false; public count: number = 0; public hashtable: Record<string, string> = {}; constructor( entries: Record<string, string> = {}, version: number = 5, hash_bits: number = 38, has_trenc: boolean = false, count: number = 0, hashtable: Record<string, string> = {} ) { this.entries = entries; this.version = version; this.hash_bits = hash_bits; this.has_trenc = has_trenc; this.count = count; this.hashtable = hashtable; } /** * JSON serialization matching Python __json__ method */ toJSON(): any { return { entries: this.entries, version: this.version, hash_bits: this.hash_bits, has_trenc: this.has_trenc, count: this.count, hashtable: this.hashtable }; } /** * Create stream from path or raw data - matches Python stream method */ private stream(path: string, mode: string, raw?: Buffer | boolean): BinStream { if (raw !== undefined) { if (raw === true) { return BinStream.create(); } else if (Buffer.isBuffer(raw)) { return BinStream.fromBuffer(raw, mode.includes('w')); } } if (mode.includes('w')) { return BinStream.create(); } else { return BinStream.fromFile(path); } } /** * Read RST file - matches Python read method exactly * Credits: https://github.com/CommunityDragon/CDTB/blob/master/cdtb/rstfile.py */ read(path: string, raw?: Buffer): void { const stream = this.stream(path, 'rb', raw); try { // Read magic and version const [magic] = stream.read_s(3); const [version] = stream.read_u8(); if (magic !== 'RST') { throw new RSTError(`pyRitoFile: invalid magic for RST file "${magic}"`); } if (version !== 5) { throw new RSTError(`pyRitoFile: unsupported RST version "${version}"`); } this.version = version; const hashMask = (1n << BigInt(this.hash_bits)) - 1n; const [count] = stream.read_ul(); this.count = count; const entries: Array<[number, bigint]> = []; // Read entry headers for (let i = 0; i < this.count; i++) { const [v] = stream.read_ull(); const offset = Number(v >> BigInt(this.hash_bits)); const bHash = v & hashMask; entries.push([offset, bHash]); } // Read remaining data const data = stream.raw().subarray(stream.tell()); // Parse strings for (const [offset, bHash] of entries) { const end = data.indexOf(0, offset); const stringData = data.subarray(offset, end === -1 ? undefined : end); const text = stringData.toString('utf-8'); this.entries[bHash.toString()] = text; } } finally { stream.close(); } } /** * Write RST file - matches Python write method exactly */ write(path: string, raw?: boolean): Buffer | void { const stream = this.stream(path, 'wb', raw); try { // Write magic and version stream.write_s('RST'); stream.write_u8(this.version); stream.write_ul(Object.keys(this.entries).length); const entriesOrder: Array<[bigint, string]> = []; // Process entries and compute hashes for (const [bHashStr, text] of Object.entries(this.entries)) { let hashInt: bigint; try { hashInt = BigInt(bHashStr); } catch { hashInt = xxh3_64_intdigest(bHashStr) & ((1n << BigInt(this.hash_bits)) - 1n); } entriesOrder.push([hashInt, text]); } // Build data block const dataBlock: Buffer[] = []; let currentOffset = 0; const entriesWithOffsets: Array<[bigint, number]> = []; for (const [hashInt, text] of entriesOrder) { const textBytes = Buffer.concat([Buffer.from(text, 'utf-8'), Buffer.from([0])]); entriesWithOffsets.push([hashInt, currentOffset]); dataBlock.push(textBytes); currentOffset += textBytes.length; } // Write entry headers const hashMask = (1n << BigInt(this.hash_bits)) - 1n; for (const [hashInt, offset] of entriesWithOffsets) { const combined = (BigInt(offset) << BigInt(this.hash_bits)) | (hashInt & hashMask); stream.write_ull(combined); } // Write data block const finalDataBlock = Buffer.concat(dataBlock); stream.writeBuffer(finalDataBlock); if (raw) { return stream.raw(); } else { fs.writeFileSync(path, stream.raw()); } } finally { stream.close(); } } /** * Read JSON data - matches Python read_json method */ readJson(path: string, raw?: string): void { if (raw) { this.entries = JSON.parse(raw); } else { const data = fs.readFileSync(path, 'utf-8'); this.entries = JSON.parse(data); } } /** * Write JSON data - matches Python write_json method */ writeJson(path: string, raw?: boolean): string | void { const result = JSON.stringify(this.entries, null, 4); if (raw) { return result; } else { fs.writeFileSync(path, result, 'utf-8'); } } /** * Un-hash entries using hashtable - matches Python un_hash method */ unHash(): void { const newEntries: Record<string, string> = {}; for (const [h, value] of Object.entries(this.entries)) { if (h in this.hashtable) { newEntries[this.hashtable[h]] = value; } else { newEntries[h] = value; } } this.entries = newEntries; } }