import type { FileHandle } from "node:fs/promises"; import { createHash } from "node:crypto"; import { setImmediate } from "node:timers/promises"; import type { DesktopFile, DesktopFact } from "../domain/desktopApplication.js"; import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import { observeIl2cppMetadataHeaderPrefix } from "../domain/il2cpp/il2cppMetadataHeaderPrefix.js"; import { parsePeRecord, peRvaToFileOffset } from "../domain/binaryTarget.js"; import { readManagedMetadataLayout } from "../dotnet/ManagedMetadataLayout.js"; import { readAssemblyReferenceName } from "../dotnet/ManagedMetadataInventoryRows.js"; import { ManagedReaderFailure } from "../dotnet/ManagedReaderFailure.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; import { abortIfNeeded } from "./ArtifactHash.js"; import { hasMachoMagic } from "./apple/MachoLoadCommandReader.js"; export interface DesktopReadBudget { bytes: number; readonly maximum: number; retainedBytes: number; readonly maximumRetainedBytes: number; } /** A resource refusal is separate from malformed format declarations. */ class DesktopProbeLimit extends ArtifactReaderFailure { constructor() { super("limit", "Directory probe byte budget exhausted", { boundary: "capacity", }); } } /** Inspect selected ranges on an already admitted descriptor; never hash payloads. */ export const inspectDesktopFile = async ( file: FileHandle, row: DesktopFile, budget: DesktopReadBudget, signal?: AbortSignal, unityManagedDirectory: string | null = null, ): Promise => { const size = row.bytes; if (size === null) throw new ArtifactReaderFailure( "limit", "File extent exceeds exact host offsets", { boundary: "file-size" }, ); const read = async (offset: number, bytes: number): Promise => { abortIfNeeded(signal); if ( !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(bytes) || bytes < 0 || offset > size || bytes > size - offset ) throw new ArtifactReaderFailure( "format", "Declared byte range lies outside the file", ); if (bytes > budget.maximum - budget.bytes) throw new DesktopProbeLimit(); const result = Buffer.alloc(bytes); let consumed = 0; while (consumed < bytes) { abortIfNeeded(signal); const part = await file.read( result, consumed, bytes - consumed, offset + consumed, ); budget.bytes += part.bytesRead; if (part.bytesRead === 0) throw new ArtifactReaderFailure( "integrity", "File ended before its observed extent", ); consumed += part.bytesRead; } abortIfNeeded(signal); row.ranges.push({ offset, bytes, sha256: createHash("sha256").update(result).digest("hex"), }); return result; }; const fact = (observation: DesktopFact) => { const remaining = budget.maximumRetainedBytes - budget.retainedBytes; const reservation = 512 + jsonValueByteLength(observation.value, (remaining - 512) / 12) * 12; if (reservation > budget.maximumRetainedBytes - budget.retainedBytes) throw new ArtifactReaderFailure( "limit", "Directory facts reached the retained metadata budget", { boundary: "memory" }, ); budget.retainedBytes += reservation; row.facts.push(observation); }; const prefix = await read(0, Math.min(size, 64)); const metadataPrefix = observeIl2cppMetadataHeaderPrefix(prefix); if (metadataPrefix.kind === "il2cpp") fact({ kind: "il2cpp-metadata-header", offset: 0, value: { magic: "0xfab11baf", raw_version: metadataPrefix.rawVersion, }, }); if (prefix.length >= 20 && prefix.toString("latin1", 0, 4) === "GDPC") fact({ kind: "godot-pck-header", offset: 0, value: { pack_version: prefix.readUInt32LE(4), engine_version: [ prefix.readUInt32LE(8), prefix.readUInt32LE(12), prefix.readUInt32LE(16), ], placement: "standalone", }, }); if ( prefix.length >= 16 && prefix.toString("latin1", 0, 4) === "FORM" && prefix.toString("latin1", 8, 12) === "GEN8" ) { const containerBytes = prefix.readUInt32LE(4); const chunkBytes = prefix.readUInt32LE(12); if ( containerBytes >= 8 && containerBytes <= size - 8 && chunkBytes <= containerBytes - 8 ) fact({ kind: "gamemaker-form-header", offset: 0, value: { container: "FORM", first_chunk: "GEN8", }, }); } if ( prefix.length >= 16 && prefix.toString("latin1", 0, 16) === "-==--==--==--==-" ) fact({ kind: "unreal-container-convention", offset: 0, value: { format: "IoStore TOC" }, }); const native = prefix.length >= 4 && (prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) || hasMachoMagic(prefix)); if (native) fact({ kind: "native-image-header", offset: 0, value: { format: prefix[0] === 0x7f ? "elf" : "mach-o", signature: prefix.subarray(0, 4).toString("hex"), }, }); const observe = async (operation: () => Promise) => { try { await operation(); } catch (cause: unknown) { if ( !(cause instanceof ArtifactReaderFailure) || (cause.reason !== "format" && cause.reason !== "limit") ) throw cause; if (!row.issues.includes(cause.message)) row.issues.push(cause.message); } }; if (prefix.length === 64 && prefix.readUInt16LE(0) === 0x5a4d) await observe(() => inspectPeFile(prefix, row, read, fact, unityManagedDirectory, signal), ); if ( (native || (prefix.length >= 2 && prefix.readUInt16LE(0) === 0x5a4d)) && size >= 12 ) await observe(() => inspectPckTrailer(size, read, fact)); }; type DesktopByteRead = (offset: number, bytes: number) => Promise; type RecordDesktopFact = (observation: DesktopFact) => void; const inspectPeFile = async ( prefix: Buffer, row: DesktopFile, read: DesktopByteRead, fact: RecordDesktopFact, unityManagedDirectory: string | null, signal?: AbortSignal, ): Promise => { const peOffset = prefix.readUInt32LE(0x3c); const coff = await read(peOffset, 24); if (coff.toString("latin1", 0, 4) !== "PE\0\0") return; const optionalSize = coff.readUInt16LE(20); if (optionalSize > 4096) throw new ArtifactReaderFailure( "format", "PE optional header exceeds the supported format bound", ); const optional = await read(peOffset + 24, optionalSize); const parsed = parsePeRecord(Buffer.concat([coff, optional]), undefined); if (!parsed.ok) { row.issues.push(parsed.error); return; } if (parsed.value.format !== "pe") return; fact({ kind: "native-image-header", offset: peOffset, value: { format: "pe", machine: coff.readUInt16LE(4) }, }); if (!parsed.value.managed) return; const directories = optional.readUInt16LE(0) === 0x20b ? 112 : 96; const cliRva = optional.readUInt32LE(directories + 14 * 8); const cliSize = optional.readUInt32LE(directories + 14 * 8 + 4); fact({ kind: "pe-cli-directory", offset: peOffset + 24 + directories + 14 * 8, value: { rva: cliRva, bytes: cliSize }, }); if (unityManagedDirectory === null) return; const sectionCount = coff.readUInt16LE(6); const sections = await read(peOffset + 24 + optionalSize, sectionCount * 40); const mapped = (rva: number, bytes: number) => peRvaToFileOffset( rva, sections, sectionCount, optional.readUInt32LE(60), bytes, ); const cliOffset = mapped(cliRva, 16); if (cliOffset === undefined || cliSize < 72) throw new ArtifactReaderFailure( "format", "CLI metadata directory cannot be mapped", ); const cli = await read(cliOffset, 16); if (cli.readUInt32LE(0) < 72) throw new ArtifactReaderFailure( "format", "CLI header declares a truncated runtime header", ); const metadataBytes = cli.readUInt32LE(12); const metadataOffset = mapped(cli.readUInt32LE(8), metadataBytes); if (metadataOffset === undefined) throw new ArtifactReaderFailure( "format", "CLI metadata range cannot be mapped", ); const bytes = await read(metadataOffset, metadataBytes); try { const layout = readManagedMetadataLayout(bytes, 0, bytes.length); const refs = layout.table(35); for (let index = 1; index <= (refs?.rowCount ?? 0); index++) { // Yield between bounded batches so metadata names cannot monopolize MCP. if ((index - 1) % 256 === 0) { await setImmediate(); abortIfNeeded(signal); } const reference = readAssemblyReferenceName( bytes, layout, index, layout.strings.size, ); if ( reference.name !== "UnityEngine" && !reference.name.startsWith("UnityEngine.") ) continue; fact({ kind: "unity-assembly-reference", offset: metadataOffset + reference.row_offset, value: { name: reference.name, token: reference.token, managed_directory: unityManagedDirectory, }, }); return; } } catch (cause: unknown) { if (!(cause instanceof ManagedReaderFailure)) throw cause; throw new ArtifactReaderFailure( cause.issue.code === "resource-limit" ? "limit" : "format", cause.message, { cause, ...(cause.issue.code === "resource-limit" ? { boundary: "memory" as const } : {}), }, ); } }; const inspectPckTrailer = async ( size: number, read: DesktopByteRead, fact: RecordDesktopFact, ): Promise => { const trailer = await read(size - 12, 12); if (trailer.toString("latin1", 8, 12) === "GDPC") { const packSize = trailer.readBigUInt64LE(0); if (packSize < 20n || packSize > BigInt(size - 12)) throw new ArtifactReaderFailure( "format", "Embedded PCK trailer declares an invalid pack extent", ); const offset = size - 12 - Number(packSize); const header = await read(offset, 20); if (header.toString("latin1", 0, 4) !== "GDPC") throw new ArtifactReaderFailure( "format", "Embedded PCK trailer does not locate a pack header", ); fact({ kind: "godot-pck-header", offset, value: { pack_version: header.readUInt32LE(4), engine_version: [ header.readUInt32LE(8), header.readUInt32LE(12), header.readUInt32LE(16), ], placement: "embedded-trailer", }, }); } };