ts-ritofile
Version:
TypeScript library for reading and writing League of Legends game file formats
227 lines (193 loc) • 6.47 kB
text/typescript
/**
* 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;
}
}