/** * Low-level SWF file parsing: decompression, tag reading, symbol class. */ import { inflateSync } from "node:zlib"; import LZMA from "lzma"; // ── SWF Tag Types ────────────────────────────────────────────────── export const TAG_END = 0; export const TAG_DEFINE_BITS_LOSSLESS = 20; export const TAG_DEFINE_BITS_JPEG2 = 21; export const TAG_DEFINE_BITS_JPEG3 = 35; export const TAG_DEFINE_BITS_LOSSLESS2 = 36; export const TAG_SYMBOL_CLASS = 76; export const TAG_DEFINE_BINARY_DATA = 87; // ── Interfaces ───────────────────────────────────────────────────── export interface SwfTag { type: number; data: Buffer; } export interface SymbolClassResult { /** charID → first symbol name (canonical name) */ classNames: Map; /** symbol name → charID (all symbols including aliases) */ symbols: Map; } // ── SWF Header Parsing ───────────────────────────────────────────── export function decompressSwf(buffer: Buffer): Buffer { const sig = buffer.toString("ascii", 0, 3); if (sig === "FWS") { return buffer; } if (sig === "CWS") { const header = Buffer.alloc(8); buffer.copy(header, 0, 0, 8); header.write("FWS", 0, 3, "ascii"); const compressed = buffer.subarray(8); const decompressed = inflateSync(compressed); return Buffer.concat([header, decompressed]); } if (sig === "ZWS") { const fileLength = buffer.readUInt32LE(4); const compressedSize = buffer.readUInt32LE(8); const lzmaData = buffer.subarray(12, 12 + compressedSize); const props = lzmaData.subarray(0, 5); const compressedPayload = lzmaData.subarray(5); const uncompressedSize = fileLength - 8; const lzmaHeader = Buffer.alloc(13); props.copy(lzmaHeader, 0); lzmaHeader.writeUInt32LE(uncompressedSize, 5); lzmaHeader.writeUInt32LE(0, 9); const lzmaStream = Buffer.concat([lzmaHeader, compressedPayload]); const decompressed = Buffer.from(LZMA.decompress(lzmaStream) as number[]); const header = Buffer.alloc(8); buffer.copy(header, 0, 0, 8); header.write("FWS", 0, 3, "ascii"); return Buffer.concat([header, decompressed]); } throw new Error(`Unknown SWF signature: ${sig}`); } function readRect(buf: Buffer, offset: number): number { const nbits = (buf[offset] >> 3) & 0x1f; const totalBits = 5 + nbits * 4; const totalBytes = Math.ceil(totalBits / 8); return offset + totalBytes; } // ── SWF Tag Reader ───────────────────────────────────────────────── export function parseSwfTags(swfBuffer: Buffer): SwfTag[] { const tags: SwfTag[] = []; let offset = 8; offset = readRect(swfBuffer, offset); offset += 4; while (offset < swfBuffer.length) { if (offset + 2 > swfBuffer.length) break; const tagCodeAndLength = swfBuffer.readUInt16LE(offset); offset += 2; const tagType = (tagCodeAndLength >> 6) & 0x3ff; let tagLength = tagCodeAndLength & 0x3f; if (tagLength === 0x3f) { if (offset + 4 > swfBuffer.length) break; tagLength = swfBuffer.readUInt32LE(offset); offset += 4; } if (tagType === TAG_END) break; if (offset + tagLength > swfBuffer.length) break; const tagData = swfBuffer.subarray(offset, offset + tagLength); tags.push({ type: tagType, data: Buffer.from(tagData) }); offset += tagLength; } return tags; } // ── SymbolClass Parser ───────────────────────────────────────────── export function parseSymbolClass(data: Buffer): SymbolClassResult { const classNames = new Map(); const symbols = new Map(); let offset = 0; const numSymbols = data.readUInt16LE(offset); offset += 2; for (let i = 0; i < numSymbols; i++) { if (offset + 2 > data.length) break; const charId = data.readUInt16LE(offset); offset += 2; const strStart = offset; while (offset < data.length && data[offset] !== 0) offset++; const name = data.toString("utf-8", strStart, offset); offset++; symbols.set(name, charId); if (!classNames.has(charId)) { classNames.set(charId, name); } } return { classNames, symbols }; } // ── DefineBinaryData Parser ──────────────────────────────────────── export function parseBinaryData(data: Buffer): { id: number; content: Buffer } { const id = data.readUInt16LE(0); const content = data.subarray(6); return { id, content }; }