/** Binary reader/writer utilities for subtitle format parsing */ export class BinaryReader { private buf: Buffer; private _offset = 0; constructor(buf: Buffer) { this.buf = buf; } get offset(): number { return this._offset; } get remaining(): number { return this.buf.length - this._offset; } get eof(): boolean { return this._offset >= this.buf.length; } seek(offset: number): void { this._offset = offset; } skip(bytes: number): void { this._offset += bytes; } readUInt8(): number { const v = this.buf[this._offset]; this._offset += 1; return v; } readInt8(): number { const v = this.buf.readInt8(this._offset); this._offset += 1; return v; } readUInt16LE(): number { const v = this.buf.readUInt16LE(this._offset); this._offset += 2; return v; } readUInt32LE(): number { const v = this.buf.readUInt32LE(this._offset); this._offset += 4; return v; } readInt32LE(): number { const v = this.buf.readInt32LE(this._offset); this._offset += 4; return v; } readBytes(count: number): Buffer { const data = this.buf.subarray(this._offset, this._offset + count); this._offset += count; return data; } /** Read a PMW tag: [TAG:4][SIZE:4][DATA:N] */ readTag(): { tag: number; size: number; data: Buffer } { const tag = this.readUInt32LE(); const size = this.readUInt32LE(); const data = this.readBytes(size); return { tag, size, data }; } /** Read a PMW multi-string (BOM-aware: UTF-16LE or Latin1) */ readMultiString(data: Buffer): string { if (data.length === 0) return ""; if (data.length >= 2) { const bom = data.readUInt16LE(0); if (bom === 0xFEFF || bom === 0xFFFE) { return data.subarray(2).toString("utf16le"); } } return data.toString("latin1"); } /** Validate a PMW tag prefix */ static isValidPMWTag(tag: number): boolean { return (tag & 0xFFFF0000) === 0xFA500000; } /** Scan forward to find the next valid PMW tag */ recoverToNextTag(): { tag: number; size: number; data: Buffer } | null { while (this._offset + 8 <= this.buf.length) { const candidate = this.buf.readUInt32LE(this._offset); if (BinaryReader.isValidPMWTag(candidate)) { return this.readTag(); } this._offset += 1; } return null; } readString(length: number, encoding: BufferEncoding = "latin1"): string { const data = this.readBytes(length); return data.toString(encoding).replace(/\0+$/, ""); } } export class BinaryWriter { private chunks: Buffer[] = []; writeUInt8(v: number): void { const buf = Buffer.alloc(1); buf[0] = v; this.chunks.push(buf); } writeUInt16LE(v: number): void { const buf = Buffer.alloc(2); buf.writeUInt16LE(v); this.chunks.push(buf); } writeUInt32LE(v: number): void { const buf = Buffer.alloc(4); buf.writeUInt32LE(v); this.chunks.push(buf); } writeInt32LE(v: number): void { const buf = Buffer.alloc(4); buf.writeInt32LE(v); this.chunks.push(buf); } writeBytes(data: Buffer): void { this.chunks.push(data); } /** Write a PMW tag: [TAG:4][SIZE:4][DATA:N] */ writeTag(tag: number, data: Buffer): void { this.writeUInt32LE(tag); this.writeUInt32LE(data.length); this.writeBytes(data); } /** Write a PMW DWORD tag */ writeTagDWORD(tag: number, value: number): void { const buf = Buffer.alloc(4); buf.writeUInt32LE(value); this.writeTag(tag, buf); } /** Write a PMW multi-string (with UTF-16LE BOM) */ writeTagMultiString(tag: number, str: string): void { if (!str) { this.writeTag(tag, Buffer.alloc(0)); return; } // Always write as UTF-16LE with BOM const bom = Buffer.from([0xFF, 0xFE]); const strBuf = Buffer.from(str, "utf16le"); const data = Buffer.concat([bom, strBuf]); this.writeTag(tag, data); } writeString(str: string, length: number, encoding: BufferEncoding = "latin1"): void { const buf = Buffer.alloc(length, 0); buf.write(str, 0, length, encoding); this.chunks.push(buf); } toBuffer(): Buffer { return Buffer.concat(this.chunks); } }