import { regularFileCancellation, RegularFileCleanupFailure, RegularFileCancelled, withOwnedRegularFile, OwnedRegularFileOperationFailure, } from "../filesystem/OwnedRegularFile.js"; import { bindRegularFileCleanup, RetainedRegularFileCleanupFailure, retryRegularFileCleanup, } from "./RegularFileRead.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { constants, type Stats } from "node:fs"; import { createHash } from "node:crypto"; import { access, lstat, realpath, stat, type FileHandle, } from "node:fs/promises"; import { extname } from "node:path"; import { BinaryTargetError } from "../domain/configurationErrors.js"; import { sameRegularFileState } from "../filesystem/RegularFile.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { err, ok, type Result } from "../domain/result.js"; import { readMipsElfMetadata } from "./MipsElfMetadata.js"; import { validateElfDeclaredRanges } from "../domain/elfDeclaredRanges.js"; import { resolveAppBundleExecutable, type ResolvedAppBundle, } from "./AppBundleExecutable.js"; import { validateDosComLength, type ExecutableFormatHint, } from "../domain/dosCom.js"; import { admitRawBinary, rawBinarySelectionError, } from "../domain/rawBinary.js"; import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import { mzWindowsHeaderLayoutLimitation, observeMzWindowsHeader, parseMzHeaderEvidence, type DosMzHeader, type MzWindowsHeaderObservation, } from "../domain/dosMz.js"; import { createXmlPlistSignatureProbe, hasBinaryPlistSignature, MAX_XML_PLIST_PROBE_BYTES, } from "../domain/apple/appleArchiveFormat.js"; import { hasGodotPckHeader } from "../domain/godotPckFormat.js"; import { hasZipSignature, locateTrailingZipArchive, isZipPackageFormat, zipRootPackageFormat, } from "../domain/zipPackageFormat.js"; import { parseCodeViewData, claimsExecutableHeader, parseExecutableHeader, parsePeRecord, peDebugDirectoryEntry, peRvaToFileOffset, type ExecutableMetadata, } from "../domain/binaryTarget.js"; import { classifyFatContainerIdentity, readFatContainerHeader, readMachoHeaderPrefixFacts, type FatSliceHeaderEvidence, type MachoHeaderPrefixFacts, } from "../domain/apple/machoContainer.js"; import { isJvmClassFile, MAX_JVM_CLASS_IDENTITY_BYTES, JVM_CLASS_IDENTITY_LIMIT_REASON, } from "../domain/apple/jvmClassFile.js"; import { MachoLoadCommandCursor, readMachoLoadCommandTableHeader, type MachoLoadCommandTableHeader, } from "../domain/apple/machoLoadCommands.js"; import type { PeCodeViewRecord, UniversalMachOSlice, } from "../domain/binaryTargetTypes.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import type { JsonValue } from "../domain/jsonValue.js"; const XML_PLIST_PROBE_CHUNK_BYTES = 64 * 1024; /** Options for resolving a local binary target and its stable identity. */ export interface BinaryTargetResolverOptions { readonly cwd?: string; readonly hostArchitecture?: NodeJS.Architecture; readonly targetKind?: BinaryTarget["kind"]; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; readonly entryPoint?: string; readonly signal?: AbortSignal; readonly resources?: ArtifactResourceScope; } /** * Resolve and classify a readable local target before a provider is selected. * FAT Mach-O inputs select the host-compatible architecture, otherwise their * first declared supported slice, so setup remains non-interactive; unsupported * or ambiguous inputs are returned as typed errors. */ export const parseBinaryTarget = async ( input: string, options: BinaryTargetResolverOptions = {}, ): Promise< Result > => { const cwd = options.cwd ?? process.cwd(); const { signal } = options; const candidate = absoluteCallerPath(input, { cwd }); // Failures before canonicalization concern the selected path itself; later // failures concern the canonical path opened with symbolic links rejected. let stage: TargetFilesystemStage = "resolve"; const resolve = async (): Promise => { throwIfTargetResolutionCancelled(signal); await access(candidate, constants.R_OK); throwIfTargetResolutionCancelled(signal); const canonical = await realpath(candidate); stage = "read"; // Windows native admission must inspect the caller-selected reparse components. const sourcePath = process.platform === "win32" ? candidate : canonical; throwIfTargetResolutionCancelled(signal); const resolved = await resolveAppBundle(canonical); throwIfTargetResolutionCancelled(signal); if (!resolved.ok) return err(resolved.error); const { executable: path, infoPlist } = resolved.value; const bundle = infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist }; return withOwnedRegularFile( path, { symlinks: "reject", ...(signal === undefined ? {} : { signal }), }, async (handle) => { try { return await readOpenedBinaryTarget( handle, { path, sourcePath, ...bundle }, options, ); } catch (cause: unknown) { throw new OwnedRegularFileOperationFailure(cause); } }, ); }; try { const resolved = await (options.resources === undefined ? resolve() : options.resources.run(resolve)); return finalizedTargetResolution(resolved, signal); } catch (cause: unknown) { // A caller-selected abort value never transfers file ownership. if (signal?.aborted === true && cause === signal.reason) return err( new AnalysisCancelledError("resolve_binary_target", { cause }), ); if (cause instanceof RegularFileCleanupFailure) { const fileFailure = cause as RegularFileCleanupFailure; if (options.resources === undefined) return err( targetFileCleanupError( new RetainedRegularFileCleanupFailure(fileFailure), candidate, stage, ), ); const retried = await retryRegularFileCleanup( fileFailure, options.resources, signal, ); if (retried.failure !== undefined) return err(targetFileCleanupError(retried.failure, candidate, stage)); if (retried.outcome.kind === "failed") return targetResolutionFailure( fileFailure.operationFailure ?? retried.outcome.cause, candidate, stage, signal, ); return finalizedTargetResolution(retried.outcome.value, signal); } if (cause instanceof RegularFileCancelled) return finalizedTargetResolution( cause.outcome.value as TargetResolution, signal, ); if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) { const error = new BinaryTargetError( candidate, "A previous target file could not be closed", { cause, cleanup: cause.cleanup, partialObservation: cause.partialObservation, }, ); bindRegularFileCleanup(cause, error); return err(error); } return targetResolutionFailure(cause, candidate, stage, signal); } }; type TargetResolution = Result< BinaryTarget, BinaryTargetError | AnalysisCancelledError >; /** Classify and hash the one descriptor admitted by the lifetime owner. */ const readOpenedBinaryTarget = async ( handle: FileHandle, selected: { readonly path: string; readonly sourcePath: string; readonly bundleInfoPlist?: string; }, options: BinaryTargetResolverOptions, ): Promise => { const { path, sourcePath, bundleInfoPlist } = selected; const bundle = bundleInfoPlist === undefined ? {} : { bundleInfoPlist }; const hostArchitecture = options.hostArchitecture ?? process.arch; const { targetKind, formatHint, signal, architecture, entryPoint } = options; throwIfTargetResolutionCancelled(signal); const metadata = await handle.stat(); throwIfTargetResolutionCancelled(signal); if (formatHint === "raw-binary") { if (targetKind !== undefined && targetKind !== "executable") return err( new BinaryTargetError( path, "raw-binary format requires an executable target kind", { constraint: "load_selection_rejected" }, ), ); const admitted = admitRawBinary({ architecture, entryPoint, fileBytes: metadata.size, }); if (!admitted.ok) return err( new BinaryTargetError(path, admitted.error.reason, { constraint: admitted.error.constraint, }), ); return ok({ path, sourcePath, ...bundle, sha256: await sha256Handle(handle, path, metadata, signal), kind: "executable", format: "raw-binary", architecture: admitted.value.architecture, availableArchitectures: [admitted.value.architecture], entryPoint: admitted.value.entryPoint, entryPointSource: admitted.value.entryPointSource, }); } const pairing = rawBinarySelectionError({ ...(formatHint === undefined ? {} : { format: formatHint }), architecture, entryPoint, }); if (pairing !== undefined) return err( new BinaryTargetError(path, pairing, { constraint: "load_selection_rejected", }), ); if (formatHint === "dos-com") { if (targetKind !== undefined && targetKind !== "executable") return err( new BinaryTargetError( path, "DOS COM format requires an executable target kind", { constraint: "load_selection_rejected" }, ), ); const length = validateDosComLength(metadata.size); if (!length.ok) return err( new BinaryTargetError(path, length.error, { constraint: "file_format_rejected", }), ); return ok({ path, sourcePath, ...bundle, sha256: await sha256Handle(handle, path, metadata, signal), kind: "executable", format: "dos-com", architecture: "x86", availableArchitectures: ["x86"], }); } // Probe WASM content before suffix-only database routing; an explicit // database selection keeps the caller's chosen interpretation. const artifactFormat = targetKind === "database" ? undefined : await detectArtifactFormat(path, handle, signal); if ( targetKind === "database" || (targetKind === undefined && path.toLowerCase().endsWith(".hop")) ) return ok({ path, sourcePath, sha256: await sha256Handle(handle, path, metadata, signal), kind: "database", format: "analysis-database", }); if (artifactFormat !== undefined) { const identity = { path, sourcePath, sha256: await sha256Handle(handle, path, metadata, signal), }; return isArchiveFormat(artifactFormat) ? ok({ ...identity, kind: "archive", format: artifactFormat }) : ok({ ...identity, kind: "artifact", format: artifactFormat }); } const detected = await readExecutableMetadata( handle, hostArchitecture, signal, ); if (!detected.ok) { // Only bytes no other format claims may lead a located ZIP, so an // executable with an appended archive keeps its executable route. if ( !(await claimsLeadingExecutableHeader(handle, metadata.size, signal)) && (await locateTrailingZipArchive(metadata.size, (position, length) => readHandleRange(handle, position, length, signal), )) !== undefined ) return ok({ path, sourcePath, sha256: await sha256Handle(handle, path, metadata, signal), kind: "archive", format: zipRootPackageFormat(path), }); const reason = typeof detected.error === "string" ? detected.error : detected.error.reason; const diagnostics = typeof detected.error === "string" ? undefined : detected.error.diagnostics; return err( new BinaryTargetError(path, reason, { constraint: "file_format_rejected", ...(diagnostics === undefined ? {} : { diagnostics }), }), ); } return ok({ path, sourcePath, ...bundle, sha256: await sha256Handle(handle, path, metadata, signal), kind: "executable", ...detected.value, }); }; /** Preserve an earlier admission refusal over an interruption during final close. */ const finalizedTargetResolution = ( result: TargetResolution, signal?: AbortSignal, ): TargetResolution => { if (!result.ok || signal?.aborted !== true) return result; return err( new AnalysisCancelledError("resolve_binary_target", { cause: signal.reason, diagnostics: { completed_target: jsonValueSchema.parse(result.value) }, }), ); }; /** Descriptor cleanup retains the physical result and binds its exact release lease. */ const targetFileCleanupError = ( failure: RetainedRegularFileCleanupFailure, candidate: string, stage: TargetFilesystemStage, ): BinaryTargetError => { const cancellation = regularFileCancellation(failure); const completed = failure.outcome.kind === "completed" ? failure.outcome.value : undefined; const completedTarget = completed?.ok === true ? jsonValueSchema.parse(completed.value) : undefined; let primary: BinaryTargetError | AnalysisCancelledError | undefined; if (failure.outcome.kind === "failed") { const physical = failure.outcome.cause; primary = physical instanceof BinaryTargetError || physical instanceof AnalysisCancelledError ? physical : cancellation !== undefined ? new AnalysisCancelledError("resolve_binary_target", { cause: cancellation.reason, }) : (classifyTargetFilesystemFailure(candidate, physical, stage) ?? new BinaryTargetError( failure.path, "Target read failed before file cleanup", { cause: physical }, )); } else if (completed?.ok === false) primary = completed.error; else if (cancellation !== undefined) primary = new AnalysisCancelledError("resolve_binary_target", { cause: cancellation.reason, ...(completedTarget === undefined ? {} : { diagnostics: { completed_target: completedTarget } }), }); const error = new BinaryTargetError( failure.path, "The target file handle could not be closed", { cause: failure, cleanupPrimary: primary, cleanup: { reason: failure.cleanupCause instanceof Error ? failure.cleanupCause.message : String(failure.cleanupCause), resources: [failure.path], }, diagnostics: { ...(primary === undefined ? {} : { primary_error: projectAnalysisError(primary) }), ...(completedTarget === undefined ? {} : { completed_target: completedTarget }), }, }, ); bindRegularFileCleanup(failure, error); return error; }; const targetResolutionFailure = ( cause: unknown, candidate: string, stage: TargetFilesystemStage, signal?: AbortSignal, ): TargetResolution => { // Physical rejection values do not grant ownership of foreign descriptors. const physical = cause instanceof OwnedRegularFileOperationFailure ? cause.operationCause : cause; if ( physical instanceof AnalysisCancelledError || physical instanceof BinaryTargetError ) return err(physical); if (!(cause instanceof OwnedRegularFileOperationFailure)) { const cancellation = regularFileCancellation(cause); if (cancellation !== undefined) return err( new AnalysisCancelledError("resolve_binary_target", { cause: cancellation.reason, }), ); } if (signal?.aborted === true && physical === signal.reason) return err( new AnalysisCancelledError("resolve_binary_target", { cause: physical }), ); const failure = classifyTargetFilesystemFailure(candidate, physical, stage); if (failure === undefined) throw physical; return err(failure); }; /** * `resolve` covers the caller-selected path before canonicalization; `read` * covers a canonical path opened with symbolic links rejected. */ type TargetFilesystemStage = "resolve" | "read"; /** Classify a filesystem failure while reading a previously selected target. */ export const classifyBinaryTargetFilesystemFailure = ( path: string, cause: unknown, ): BinaryTargetError | undefined => classifyTargetFilesystemFailure(path, cause, "read"); const classifyTargetFilesystemFailure = ( path: string, cause: unknown, stage: TargetFilesystemStage, ): BinaryTargetError | undefined => { if (!(cause instanceof Error)) return undefined; const code = filesystemErrorCode(cause); return code === undefined ? undefined : new BinaryTargetError( path, targetFailureReason(code, cause.message, stage), { cause }, ); }; const filesystemErrorCode = (cause: Error): string | undefined => { const code: unknown = Reflect.get(cause, "code"); return typeof code === "string" ? code : undefined; }; const targetFailureReason = ( code: string, detail: string, stage: TargetFilesystemStage, ): string => { switch (code) { case "ENOENT": case "ENOTDIR": return `target does not exist or a path component is not a directory: ${detail}`; case "EACCES": case "EPERM": return `permission denied while reading target: ${detail}`; case "EISDIR": return `target is a directory, not a readable file: ${detail}`; case "ENOTFILE": return "target is not a regular file"; case "ELOOP": // A canonical path has no symbolic links left, so a link rejected while // reading it appeared after resolution. Before that, ELOOP is a loop or // an over-deep link chain in the selected path itself. return stage === "resolve" ? `target path could not be resolved: too many symbolic links or a symbolic link loop: ${detail}` : `target changed while being opened: ${detail}`; default: return `target could not be read (${code}): ${detail}`; } }; const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new AnalysisCancelledError("resolve_binary_target", { cause: signal.reason, }); }; const detectArtifactFormat = async ( path: string, handle: FileHandle, signal?: AbortSignal, ): Promise< | Exclude< BinaryTarget["format"], | "analysis-database" | "mach-o" | "mach-o-universal" | "elf" | "pe" | "dos-mz" | "dos-com" | "raw-binary" > | undefined > => { throwIfTargetResolutionCancelled(signal); const lower = path.toLowerCase(); const magic = Buffer.alloc(4096); const observed = await handle.read(magic, 0, magic.length, 0); throwIfTargetResolutionCancelled(signal); if ( observed.bytesRead >= 8 && magic.subarray(0, 8).equals(Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0])) ) throw new BinaryTargetError( path, "WebAssembly v1 artifacts are not supported by the native opening route", { constraint: "wasm_requires_static_inspection" }, ); if (hasZipSignature(magic.subarray(0, observed.bytesRead))) return zipRootPackageFormat(lower); if (hasGodotPckHeader(magic.subarray(0, observed.bytesRead))) return "godot-pck"; // Binary plists, including keyed archives, often use other extensions. if (hasBinaryPlistSignature(magic.subarray(0, observed.bytesRead))) return "plist"; const named = namedArtifactFormat(lower); if (named !== undefined) return named; if ( lower.endsWith(".pkg") && observed.bytesRead >= 4 && magic.subarray(0, 4).equals(Buffer.from("xar!")) ) return "pkg"; if (lower.endsWith(".dmg")) { throwIfTargetResolutionCancelled(signal); const size = (await handle.stat()).size; throwIfTargetResolutionCancelled(signal); if (size >= 512) { const trailer = Buffer.alloc(4); const read = await handle.read(trailer, 0, trailer.length, size - 512); throwIfTargetResolutionCancelled(signal); if (read.bytesRead === 4 && trailer.equals(Buffer.from("koly"))) return "dmg"; } } // XML property lists can use archive-specific suffixes such as .archive. // The named-format check above keeps .js, .map, and .asar classifications. const xmlProbe = createXmlPlistSignatureProbe(); let xmlState = xmlProbe.write(magic.subarray(0, observed.bytesRead)); if (xmlState === "matched") return "plist"; const chunk = Buffer.alloc(XML_PLIST_PROBE_CHUNK_BYTES); let bytesRead = observed.bytesRead; while (xmlState === "pending" && bytesRead < MAX_XML_PLIST_PROBE_BYTES) { throwIfTargetResolutionCancelled(signal); const read = await handle.read( chunk, 0, Math.min(chunk.length, MAX_XML_PLIST_PROBE_BYTES - bytesRead), bytesRead, ); throwIfTargetResolutionCancelled(signal); if (read.bytesRead === 0) break; bytesRead += read.bytesRead; xmlState = xmlProbe.write(chunk.subarray(0, read.bytesRead)); if (xmlState === "matched") return "plist"; } if (bytesRead === MAX_XML_PLIST_PROBE_BYTES) { const { size } = await handle.stat(); throwIfTargetResolutionCancelled(signal); if (size > bytesRead) throw new BinaryTargetError( path, "XML plist prolog was not found within the 1 MiB format probe limit", { constraint: "file_format_rejected" }, ); } return undefined; }; const namedArtifactFormat = ( lowerPath: string, ): "asar" | "plist" | "source-map" | "javascript" | undefined => { if (lowerPath.endsWith(".asar")) return "asar"; if (lowerPath.endsWith(".plist")) return "plist"; if (lowerPath.endsWith(".map")) return "source-map"; return /\.(?:m?js|cjs)$/u.test(lowerPath) ? "javascript" : undefined; }; const isArchiveFormat = ( format: Exclude< BinaryTarget["format"], | "analysis-database" | "mach-o" | "mach-o-universal" | "elf" | "pe" | "dos-mz" | "dos-com" | "raw-binary" >, ): format is Extract["format"] => isZipPackageFormat(format) || ["asar", "godot-pck", "dmg", "pkg"].includes(format); /** Whether the leading bytes begin any executable header family, valid or not. */ const claimsLeadingExecutableHeader = async ( handle: FileHandle, size: number, signal?: AbortSignal, ): Promise => claimsExecutableHeader( Buffer.from(await readHandleRange(handle, 0, Math.min(size, 4096), signal)), size, ); /** Read one bounded range, honoring cancellation around the host read. */ const readHandleRange = async ( handle: FileHandle, position: number, length: number, signal?: AbortSignal, ): Promise => { throwIfTargetResolutionCancelled(signal); const bytes = Buffer.alloc(length); const observed = await handle.read(bytes, 0, length, position); throwIfTargetResolutionCancelled(signal); return bytes.subarray(0, observed.bytesRead); }; const sha256Handle = async ( handle: FileHandle, path: string, initial: Stats, signal?: AbortSignal, ): Promise => { const hash = createHash("sha256"); const chunk = Buffer.allocUnsafe(64 * 1024); let position = 0; while (true) { throwIfTargetResolutionCancelled(signal); const observed = await handle.read( chunk, 0, Math.min(chunk.length, initial.size - position + 1), position, ); throwIfTargetResolutionCancelled(signal); if (observed.bytesRead === 0) break; hash.update(chunk.subarray(0, observed.bytesRead)); position += observed.bytesRead; if (position > initial.size) throw new BinaryTargetError(path, "target grew while being read"); } throwIfTargetResolutionCancelled(signal); const [opened, currentPath] = await Promise.all([handle.stat(), lstat(path)]); if ( !sameRegularFileState(initial, opened) || !sameRegularFileState(initial, currentPath) || position !== initial.size ) throw new BinaryTargetError(path, "target changed while being read"); return hash.digest("hex"); }; const resolveAppBundle = async ( path: string, ): Promise> => { const metadata = await stat(path); if (!metadata.isDirectory()) return ok({ executable: path }); if (extname(path).toLowerCase() !== ".app") return err( new BinaryTargetError( path, "this opening route requires a file or app bundle", { constraint: "directory_requires_file", }, ), ); return resolveAppBundleExecutable(path); }; const readExecutableMetadata = async ( handle: FileHandle, hostArchitecture: NodeJS.Architecture, signal?: AbortSignal, ): Promise> => { throwIfTargetResolutionCancelled(signal); const prefix = Buffer.alloc(4096); const prefixRead = await handle.read(prefix, 0, prefix.length, 0); throwIfTargetResolutionCancelled(signal); const bytes = prefix.subarray(0, prefixRead.bytesRead); // Header commitments such as Mach-O load commands and FAT slices are // checked against the whole file, not the probed prefix. const fileSize = (await handle.stat()).size; throwIfTargetResolutionCancelled(signal); if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) { let evidence = parseMzHeaderEvidence(bytes, fileSize); let signatureEvidence: | { readonly offset: number; readonly bytes: Buffer } | undefined; if (evidence.windowsHeader.state === "signature-needed") { const { offset } = evidence.windowsHeader; const signature = Buffer.alloc(Math.min(4, fileSize - offset)); const observedSignature = await handle.read( signature, 0, signature.length, offset, ); throwIfTargetResolutionCancelled(signal); signatureEvidence = { offset, bytes: signature.subarray(0, observedSignature.bytesRead), }; evidence = parseMzHeaderEvidence(bytes, fileSize, signatureEvidence); } const { windowsHeader } = evidence; if (windowsHeader.state === "declared") { const limitation = mzWindowsHeaderLayoutLimitation(windowsHeader); if (limitation !== undefined) return err({ reason: limitation, diagnostics: { unsupported_layout: true, mz_windows_header: { ...windowsHeader }, }, }); return readPeMetadata( handle, windowsHeader.offset, bytes, fileSize, signal, ); } // MZ relocation records may extend past the initial probe; their format // bounds are at most 65535 records inside a 65535-paragraph header. const tableEnd = bytes.length >= 28 ? bytes.readUInt16LE(24) + bytes.readUInt16LE(6) * 4 : 0; const headerBytes = bytes.length >= 28 ? bytes.readUInt16LE(8) * 16 : 0; if ( tableEnd > bytes.length && tableEnd <= headerBytes && tableEnd <= fileSize ) { const table = Buffer.alloc(tableEnd); const observed = await handle.read(table, 0, table.length, 0); throwIfTargetResolutionCancelled(signal); evidence = parseMzHeaderEvidence( table.subarray(0, observed.bytesRead), fileSize, signatureEvidence, ); if (evidence.windowsHeader.state === "candidate") return err( unresolvedMzHeaderFailure(evidence.windowsHeader, evidence.dos), ); return parseExecutableHeader( table.subarray(0, observed.bytesRead), hostArchitecture, fileSize, signatureEvidence, ); } if (windowsHeader.state === "candidate") return err(unresolvedMzHeaderFailure(windowsHeader, evidence.dos)); return parseExecutableHeader( bytes, hostArchitecture, fileSize, signatureEvidence, ); } if (bytes.length >= 4 && bytes.toString("hex", 0, 4) === "7f454c46") return readElfMetadata(handle, bytes, { fileSize, hostArchitecture, signal, }); const thin = await readThinMachoMetadata(handle, bytes, { fileSize, hostArchitecture, signal, }); if (thin !== null) return thin; const hasJvmMagic = bytes.length >= 4 && bytes.readUInt32BE(0) === 0xcafebabe; if ( hasJvmMagic && fileSize > bytes.length && fileSize <= MAX_JVM_CLASS_IDENTITY_BYTES ) { const complete = await readHandleRange(handle, 0, fileSize, signal); if (complete.byteLength === fileSize && isJvmClassFile(complete)) return err("unsupported binary format"); } const fatEvidence = await readFatSliceHeaderEvidence( handle, bytes, fileSize, signal, ); if ( hasJvmMagic && fileSize > MAX_JVM_CLASS_IDENTITY_BYTES && fatEvidence !== undefined && classifyFatContainerIdentity( fatEvidence.header, (offset) => fatEvidence.sliceHeadersByOffset.get(offset)?.magic, ).state === "malformed" ) return err(JVM_CLASS_IDENTITY_LIMIT_REASON); const parsed = parseExecutableHeader( bytes, hostArchitecture, fileSize, undefined, fatEvidence, ); if ( parsed.ok && parsed.value.format === "mach-o-universal" && fatEvidence !== undefined ) { for (const slice of fatEvidence.header.slices) { const framed = await readMachoSliceCommandFrames(handle, slice, signal); if (!framed.ok) return framed; } } return parsed; }; /** ELF structure and family declarations share one descriptor before provider selection. */ const readElfMetadata = async ( handle: FileHandle, bytes: Buffer, options: { readonly fileSize: number; readonly hostArchitecture: NodeJS.Architecture; readonly signal?: AbortSignal | undefined; }, ): Promise> => { const { fileSize, hostArchitecture, signal } = options; const parsed = parseExecutableHeader(bytes, hostArchitecture, fileSize); if (!parsed.ok || parsed.value.format !== "elf") return parsed; const ranges = await readElfDeclaredRanges(handle, bytes, fileSize, signal); if (!ranges.ok) return ranges; if (parsed.value.architecture !== "mips" || parsed.value.mips.elfClass !== 32) return parsed; const declarations = await readMipsElfMetadata( handle, parsed.value.mips, () => throwIfTargetResolutionCancelled(signal), ); if (!declarations.ok) return declarations; return ok({ ...parsed.value, mips: { ...parsed.value.mips, abiFlags: declarations.value.abiFlags, gnuAttributes: declarations.value.gnuAttributes, ...(declarations.value.sharedLoad === undefined ? {} : { sharedLoad: declarations.value.sharedLoad }), ...(declarations.value.limitations.length === 0 ? {} : { limitations: declarations.value.limitations }), }, }); }; /** Borrow the admitted descriptor; every request is at most one 64 KiB table chunk. */ const readElfDeclaredRanges = async ( handle: FileHandle, prefix: Buffer, fileSize: number, signal?: AbortSignal, ): Promise> => { const ranges = validateElfDeclaredRanges(prefix, fileSize); let next = ranges.next(); let window: Buffer | undefined; while (!next.done) { throwIfTargetResolutionCancelled(signal); const { offset, size } = next.value; let bytes: Buffer; if (offset + size <= prefix.length) bytes = prefix.subarray(offset, offset + size); else { window ??= Buffer.allocUnsafe(64 * 1024); bytes = window.subarray(0, size); let length = 0; while (length < size) { const read = await handle.read( bytes, length, size - length, offset + length, ); throwIfTargetResolutionCancelled(signal); if (read.bytesRead === 0) break; length += read.bytesRead; } bytes = bytes.subarray(0, length); } next = ranges.next(bytes); } return next.value; }; type ExecutableMetadataFailure = | string | { readonly reason: string; readonly diagnostics: Readonly>; }; const unresolvedMzHeaderFailure = ( windowsHeader: Extract, dos: Result, ): Exclude => { return { reason: "MZ Windows new-header candidate is unresolved", diagnostics: { mz_windows_header: { state: windowsHeader.state, offset: windowsHeader.offset, signature: windowsHeader.signature, }, mz_dos: dos.ok ? { state: "valid", header_bytes: dos.value.headerBytes, image_bytes: dos.value.imageBytes, module_bytes: dos.value.moduleBytes, overlay_bytes: dos.value.overlayBytes, relocation_count: dos.value.relocationCount, relocation_table_offset: dos.value.relocationTableOffset, entry_segment: dos.value.entrySegment, entry_offset: dos.value.entryOffset, } : { state: "invalid", reason: dos.error }, }, }; }; /** Establish a complete thin-command framing proof before provider admission. */ const readThinMachoMetadata = async ( handle: FileHandle, prefix: Buffer, input: { readonly fileSize: number; readonly hostArchitecture: NodeJS.Architecture; readonly signal?: AbortSignal | undefined; }, ): Promise | null> => { const table = readMachoLoadCommandTableHeader(prefix, input.fileSize); if (!table.ok) return table; if (table.value === null) return null; const parsed = parseExecutableHeader( prefix.subarray(0, table.value.headerSize), input.hostArchitecture, input.fileSize, ); if (!parsed.ok) return parsed; const framed = await readMachoCommandFrames(handle, table.value, { prefix, fileSize: input.fileSize, signal: input.signal, }); return framed.ok ? parsed : framed; }; /** Walk thin command headers through one probe window, skipping payload bytes. */ const readMachoCommandFrames = async ( handle: FileHandle, table: MachoLoadCommandTableHeader, input: { readonly prefix: Buffer; readonly fileSize: number; readonly fileOffset?: number | undefined; readonly signal?: AbortSignal | undefined; }, ): Promise> => { const commands = new MachoLoadCommandCursor(table); let window = input.prefix; let windowOffset = 0; while (commands.nextOffset !== null) { const offset = table.headerSize + commands.nextOffset; if ( offset + 8 <= input.fileSize && (offset < windowOffset || offset + 8 > windowOffset + window.length) ) { window = Buffer.alloc(Math.min(4096, input.fileSize - offset)); windowOffset = offset; const observed = await handle.read( window, 0, window.length, (input.fileOffset ?? 0) + offset, ); throwIfTargetResolutionCancelled(input.signal); window = window.subarray(0, observed.bytesRead); } const relative = offset - windowOffset; const frame = commands.read( relative >= 0 ? window.subarray(relative, relative + 8) : Buffer.alloc(0), ); if (!frame.ok) return frame; } return commands.finish(); }; /** Validate declared FAT slices after the canonical table/identity admission succeeds. */ const readMachoSliceCommandFrames = async ( handle: FileHandle, slice: UniversalMachOSlice, signal?: AbortSignal, ): Promise> => { const prefix = Buffer.alloc(Math.min(4096, slice.size)); const observed = await handle.read(prefix, 0, prefix.length, slice.offset); throwIfTargetResolutionCancelled(signal); const bytes = prefix.subarray(0, observed.bytesRead); const table = readMachoLoadCommandTableHeader(bytes, slice.size); if (!table.ok) return table; if (table.value === null) return err("FAT slice does not identify a thin Mach-O image"); return readMachoCommandFrames(handle, table.value, { prefix: bytes, fileSize: slice.size, fileOffset: slice.offset, signal, }); }; /** Read the validated FAT table and bounded thin-header identity prefixes. */ const readFatSliceHeaderEvidence = async ( handle: FileHandle, prefix: Buffer, fileSize: number, signal?: AbortSignal, ): Promise => { let header = readFatContainerHeader(prefix, fileSize); if (header.state === "needs-data") { // readFatContainerHeader enforces the 128-entry format bound before this // allocation; this is only the table, never a synthesized file image. const table = Buffer.alloc(header.requiredBytes); let bytesRead = 0; while (bytesRead < table.length) { throwIfTargetResolutionCancelled(signal); const read = await handle.read( table, bytesRead, table.length - bytesRead, bytesRead, ); throwIfTargetResolutionCancelled(signal); if (read.bytesRead === 0) break; bytesRead += read.bytesRead; } header = readFatContainerHeader(table.subarray(0, bytesRead), fileSize); } if (header.state !== "parsed") return undefined; const sliceHeadersByOffset = new Map(); for (let index = 0; index < header.slices.length; index += 1) { const slice = header.slices[index]; if (slice === undefined) continue; if (header.rangeIssues[index] !== null) { sliceHeadersByOffset.set( slice.offset, readMachoHeaderPrefixFacts(Buffer.alloc(0)), ); continue; } const observedLength = Math.min(12, slice.size); const thinHeader = Buffer.alloc(observedLength); let bytesRead = 0; while (bytesRead < thinHeader.length) { throwIfTargetResolutionCancelled(signal); const read = await handle.read( thinHeader, bytesRead, thinHeader.length - bytesRead, slice.offset + bytesRead, ); throwIfTargetResolutionCancelled(signal); if (read.bytesRead === 0) break; bytesRead += read.bytesRead; } sliceHeadersByOffset.set( slice.offset, readMachoHeaderPrefixFacts(thinHeader.subarray(0, bytesRead)), ); } return { header, sliceHeadersByOffset }; }; const readPeMetadata = async ( handle: FileHandle, offset: number, mzPrefix: Buffer, fileSize: number, signal?: AbortSignal, ): Promise> => { throwIfTargetResolutionCancelled(signal); const fileHeader = Buffer.alloc(24); const fileHeaderRead = await handle.read( fileHeader, 0, fileHeader.length, offset, ); throwIfTargetResolutionCancelled(signal); if (fileHeaderRead.bytesRead !== fileHeader.length) return err("invalid or truncated PE header"); const windowsHeader = observeMzWindowsHeader(mzPrefix, fileSize, { offset, bytes: fileHeader.subarray(0, 4), }); if (windowsHeader.state !== "declared") return err("invalid Windows new-header signature in MZ image"); if (["NE", "LE", "LX"].includes(windowsHeader.signature)) return err(`unsupported ${windowsHeader.signature} executable in MZ image`); const optionalHeaderSize = fileHeader.readUInt16LE(20); if (optionalHeaderSize > 4096) return err("invalid PE optional header size"); const numberOfSections = fileHeader.readUInt16LE(6); const record = Buffer.alloc(24 + optionalHeaderSize); const recordRead = await handle.read(record, 0, record.length, offset); throwIfTargetResolutionCancelled(signal); if (recordRead.bytesRead !== record.length) return err("invalid or truncated PE header"); const codeView = await readPeCodeView( handle, offset, numberOfSections, record.subarray(24), optionalHeaderSize, signal, ); return parsePeRecord(record, codeView); }; const readPeCodeView = async ( handle: FileHandle, peOffset: number, numberOfSections: number, optionalHeader: Buffer, optionalHeaderSize: number, signal?: AbortSignal, ): Promise => { throwIfTargetResolutionCancelled(signal); const debugDir = peDebugDirectoryEntry(optionalHeader); if (debugDir === undefined || numberOfSections > 96) return undefined; const sectionTableOffset = peOffset + 24 + optionalHeaderSize; const sectionTableSize = numberOfSections * 40; const sectionTable = Buffer.alloc(sectionTableSize); const sectionRead = await handle.read( sectionTable, 0, sectionTableSize, sectionTableOffset, ); throwIfTargetResolutionCancelled(signal); if (sectionRead.bytesRead !== sectionTableSize) return undefined; const debugFileOffset = peRvaToFileOffset( debugDir.debugRva, sectionTable, numberOfSections, debugDir.sizeOfHeaders, Math.min(debugDir.debugSize, 4096), ); if (debugFileOffset === undefined) return undefined; const maxDebugSize = Math.min(debugDir.debugSize, 4096); const debugDirectoryBuffer = Buffer.alloc(maxDebugSize); const debugDirRead = await handle.read( debugDirectoryBuffer, 0, maxDebugSize, debugFileOffset, ); throwIfTargetResolutionCancelled(signal); const numEntries = Math.floor(debugDirRead.bytesRead / 28); for (let j = 0; j < numEntries; j++) { const entryOffset = j * 28; const type = debugDirectoryBuffer.readUInt32LE(entryOffset + 12); if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW const sizeOfData = debugDirectoryBuffer.readUInt32LE(entryOffset + 16); let pointerToRawData = debugDirectoryBuffer.readUInt32LE(entryOffset + 24); if (pointerToRawData === 0) { const addressOfRawData = debugDirectoryBuffer.readUInt32LE( entryOffset + 20, ); const mapped = peRvaToFileOffset( addressOfRawData, sectionTable, numberOfSections, debugDir.sizeOfHeaders, Math.min(sizeOfData, 4096), ); if (mapped === undefined) continue; pointerToRawData = mapped; } if (sizeOfData < 16) continue; const maxCodeViewRead = Math.min(sizeOfData, 4096); const codeViewBuffer = Buffer.alloc(maxCodeViewRead); const codeViewRead = await handle.read( codeViewBuffer, 0, maxCodeViewRead, pointerToRawData, ); throwIfTargetResolutionCancelled(signal); const parsed = parseCodeViewData( codeViewBuffer.subarray(0, codeViewRead.bytesRead), sizeOfData, ); if (parsed !== undefined) return parsed; } return undefined; };