import type { BinaryArchitecture } from "./binaryTargetTypes.js"; import { err, ok, type Result } from "./result.js"; import { mzWindowsHeaderOffset, parseDosMzHeader } from "./dosMz.js"; export type { BinaryArchitecture, BinaryTarget } from "./binaryTargetTypes.js"; /** Format and architecture facts recovered from an executable header. */ export type ExecutableMetadata = | { readonly format: "mach-o" | "elf" | "dos-mz" | "dos-com"; readonly architecture: BinaryArchitecture; readonly availableArchitectures: readonly BinaryArchitecture[]; } | { readonly format: "pe"; readonly architecture: BinaryArchitecture; readonly availableArchitectures: readonly BinaryArchitecture[]; readonly executableRole: | "application" | "shared-library" | "non-executable"; readonly managed: boolean; }; /** * Parse supported executable headers without I/O. The caller supplies the host * architecture used for deterministic FAT Mach-O slice selection. */ export const parseExecutableHeader = ( bytes: Buffer, hostArchitecture: NodeJS.Architecture, fileSize = bytes.length, ): Result => { if ( bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) ) return parseElf(bytes); if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) return parseMz(bytes, fileSize); if (bytes.length < 8) return err("truncated or unsupported binary header"); const magic = bytes.readUInt32BE(0); if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic)) return parseThinMachO(bytes, magic); if ([0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic)) return parseFatMachO(bytes, magic, hostArchitecture); return err("unsupported binary format"); }; const parseThinMachO = ( bytes: Buffer, magic: number, ): Result => { if (bytes.length < 8) return err("truncated Mach-O header"); const little = magic === 0xcefaedfe || magic === 0xcffaedfe; const architecture = machArchitecture( little ? bytes.readUInt32LE(4) : bytes.readUInt32BE(4), ); if (architecture === undefined) return err("unsupported Mach-O architecture"); return ok({ format: "mach-o", architecture, availableArchitectures: [architecture], }); }; const parseFatMachO = ( bytes: Buffer, magic: number, host: NodeJS.Architecture, ): Result => { const little = magic === 0xbebafeca || magic === 0xbfbafeca; const is64 = magic === 0xcafebabf || magic === 0xbfbafeca; if (bytes.length < 8) return err("truncated FAT header"); const read = (offset: number): number => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); const count = read(4); const entrySize = is64 ? 32 : 20; if (count === 0 || count > 128 || bytes.length < 8 + count * entrySize) return err("truncated or invalid FAT architecture table"); const architectures: BinaryArchitecture[] = []; for (let index = 0; index < count; index += 1) { const architecture = machArchitecture(read(8 + index * entrySize)); if (architecture !== undefined && !architectures.includes(architecture)) architectures.push(architecture); } const preferred = host === "arm64" ? "arm64" : host === "x64" ? "x86_64" : host === "ia32" ? "x86" : host === "arm" ? "arm" : undefined; if (preferred === undefined || !architectures.includes(preferred)) return err(`FAT binary has no host-compatible ${host} architecture`); return ok({ format: "mach-o", architecture: preferred, availableArchitectures: architectures, }); }; const parseElf = (bytes: Buffer): Result => { if (bytes.length < 20) return err("truncated ELF header"); if (bytes[4] !== 1 && bytes[4] !== 2) return err("unsupported ELF class"); const little = bytes[5] === 1; if (!little && bytes[5] !== 2) return err("unsupported ELF endianness"); const machine = little ? bytes.readUInt16LE(18) : bytes.readUInt16BE(18); const architecture = elfArchitecture(machine); if (architecture === undefined) return err("unsupported ELF architecture"); return ok({ format: "elf", architecture, availableArchitectures: [architecture], }); }; const parseMz = ( bytes: Buffer, fileSize: number, ): Result => { const offset = mzWindowsHeaderOffset(bytes); if (offset !== null) { if (offset < 64 || offset > bytes.length - 24) return err("invalid or truncated Windows executable header in MZ image"); const signature = bytes.toString("ascii", offset, offset + 2); if (["NE", "LE", "LX"].includes(signature)) return err(`unsupported ${signature} executable in MZ image`); return parsePeRecord(bytes.subarray(offset)); } const parsed = parseDosMzHeader(bytes, fileSize); return parsed.ok ? ok({ format: "dos-mz", architecture: "x86", availableArchitectures: ["x86"], }) : parsed; }; /** Parse a bounded PE signature, COFF record, and optional header without I/O. */ export const parsePeRecord = ( record: Buffer, ): Result => { if ( record.length < 24 || record.toString("binary", 0, 4) !== "PE\u0000\u0000" ) return err("invalid or truncated PE header"); const architecture = peArchitecture(record.readUInt16LE(4)); if (architecture === undefined) return err("unsupported PE architecture"); const optionalHeaderSize = record.readUInt16LE(20); if (optionalHeaderSize > 4096 || record.length < 24 + optionalHeaderSize) return err("invalid or truncated PE optional header"); const optionalHeader = record.subarray(24, 24 + optionalHeaderSize); const managed = peManagedStatus(optionalHeader, architecture); if (!managed.ok) return managed; const characteristics = record.readUInt16LE(22); return ok({ format: "pe", architecture, availableArchitectures: [architecture], executableRole: (characteristics & 0x0002) === 0 ? "non-executable" : (characteristics & 0x2000) === 0 ? "application" : "shared-library", managed: managed.value, }); }; const peManagedStatus = ( optionalHeader: Buffer, architecture: BinaryArchitecture, ): Result => { if (optionalHeader.length < 2) return err("truncated PE optional header"); const magic = optionalHeader.readUInt16LE(0); const expectedMagic = architecture === "x86_64" || architecture === "arm64" ? 0x20b : 0x10b; if (magic !== expectedMagic) return err("PE optional-header magic does not match its architecture"); const directoryOffset = magic === 0x20b ? 112 : 96; const directoryCountOffset = directoryOffset - 4; if (optionalHeader.length < directoryCountOffset + 4) return err("truncated PE optional header"); const directoryCount = optionalHeader.readUInt32LE(directoryCountOffset); const requiredBytes = directoryOffset + directoryCount * 8; if ( !Number.isSafeInteger(requiredBytes) || requiredBytes > optionalHeader.length ) return err("truncated PE data-directory table"); if (directoryCount <= 14) return ok(false); const cliDirectory = directoryOffset + 14 * 8; return ok( optionalHeader.readUInt32LE(cliDirectory) !== 0 || optionalHeader.readUInt32LE(cliDirectory + 4) !== 0, ); }; const machArchitecture = (cpu: number): BinaryArchitecture | undefined => { switch (cpu) { case 7: return "x86"; case 0x01000007: return "x86_64"; case 12: return "arm"; case 0x0100000c: return "arm64"; } return undefined; }; const elfArchitecture = (machine: number): BinaryArchitecture | undefined => { switch (machine) { case 3: return "x86"; case 62: return "x86_64"; case 40: return "arm"; case 183: return "arm64"; } return undefined; }; const peArchitecture = (machine: number): BinaryArchitecture | undefined => { switch (machine) { case 0x14c: return "x86"; case 0x8664: return "x86_64"; case 0x1c0: case 0x1c2: case 0x1c4: return "arm"; case 0xaa64: return "arm64"; } return undefined; };