diff --git a/src/cli/codex-cli-update.ts b/src/cli/codex-cli-update.ts index 19a7f9d02ad..6e224954709 100644 --- a/src/cli/codex-cli-update.ts +++ b/src/cli/codex-cli-update.ts @@ -31,7 +31,7 @@ export interface CodexCliUpdateCommandDeps { readonly inspectIdentity?: (input: CodexCliInstallationIdentityInput) => Promise; readonly deriveInstallationInput?: ( snapshot: CodexCliInstallationSnapshot, - ) => CodexCliInstallationTargetDerivation; + ) => Promise; } function identitySummary(report: CodexCliInstallationIdentityReport): string[] { @@ -148,7 +148,7 @@ export async function handleCodexCliUpdateCommand( const snapshot = trustedNodeLauncherContext()?.codexCliInspectionEnv; const derive = deps.deriveInstallationInput ?? (await import("../codex/cli-installation-targets")).deriveCodexCliInstallationInput; - const derived = derive({ + const derived = await derive({ codexCliPath: snapshot?.codexCliPath ?? null, path: snapshot?.path ?? null, pathExt: snapshot?.pathExt ?? null, diff --git a/src/codex/cli-installation-targets.ts b/src/codex/cli-installation-targets.ts index 3c96da6e0d6..17da28dc6ab 100644 --- a/src/codex/cli-installation-targets.ts +++ b/src/codex/cli-installation-targets.ts @@ -1,4 +1,3 @@ -import { closeSync, existsSync, openSync, readSync } from "node:fs"; import { win32 } from "node:path"; import { SHIM_MARKER } from "./shim-templates"; import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity"; @@ -27,45 +26,30 @@ export type CodexCliInstallationTargetDerivation = export interface CodexCliInstallationTargetDeps { readonly platform?: NodeJS.Platform; - readonly exists?: (path: string) => boolean; + /** `refused` means the probe could not decide; a PATH scan must stop rather than + * attest a later candidate that the real launcher would never reach. */ + readonly exists?: (path: string) => boolean | "refused" | Promise; /** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */ - readonly fileContains?: (path: string, marker: string) => boolean; + readonly fileContains?: (path: string, marker: string) => + boolean | "unavailable" | Promise; } const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1"; const SHIM_PROBE_BYTES = 8 * 1024; const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js"; -function defaultFileContains(path: string, marker: string): boolean { - let descriptor: number | undefined; - let contains = false; - try { - descriptor = openSync(path, "r"); - const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES); - const count = readSync(descriptor, bytes, 0, bytes.length, 0); - contains = bytes.subarray(0, count).toString("utf8").includes(marker); - } catch { - contains = false; - } finally { - if (descriptor !== undefined) { - try { closeSync(descriptor); } catch { contains = false; } - } - } - return contains; -} - /** * First match wins, mirroring PATH resolution: directories in order, and within * each directory every PATHEXT suffix in order (or the exact name when it * already carries an extension). Skipping a hit to keep scanning would attest * something other than the launcher that actually resolves. */ -function scanPath( +async function scanPath( name: string, pathValue: string | null | undefined, pathExt: string | null | undefined, - exists: (path: string) => boolean, -): string | null { + exists: (path: string) => boolean | "refused" | Promise, +): Promise { const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean); const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase()); for (const entry of (pathValue ?? "").split(";")) { @@ -73,7 +57,9 @@ function scanPath( if (!dir) continue; for (const candidateName of names) { const candidate = win32.join(dir, candidateName); - if (exists(candidate)) return candidate; + const found = await exists(candidate); + if (found === "refused") return null; + if (found) return candidate; } } return null; @@ -86,40 +72,56 @@ function scanPath( * false identity. No ambient environment is read: without a snapshot the result * is unavailable rather than silently trusting the child's environment. */ -export function deriveCodexCliInstallationInput( +export async function deriveCodexCliInstallationInput( snapshot: CodexCliInstallationSnapshot, deps: CodexCliInstallationTargetDeps = {}, -): CodexCliInstallationTargetDerivation { +): Promise { if ((deps.platform ?? process.platform) !== "win32") { return { kind: "unavailable", reason: "unsupported_platform" }; } - const exists = deps.exists ?? existsSync; - const fileContains = deps.fileContains ?? defaultFileContains; + const safeRead = async (path: string, maxBytes: number, prefixOnly = false) => { + const { inspectWindowsInstallationFiles } = await import("./windows-installation-files"); + return inspectWindowsInstallationFiles([{ path, maxBytes, metadataOnly: maxBytes === 0, prefixOnly }]); + }; + const exists = deps.exists ?? (async (path: string) => { + const result = await safeRead(path, 0); + if (result.kind === "observed") return true; + return result.kind === "refused" ? "refused" : false; + }); + const fileContains = deps.fileContains ?? (async (path: string, marker: string) => { + const result = await safeRead(path, SHIM_PROBE_BYTES, true); + if (result.kind !== "observed") return "unavailable"; + return Buffer.from(result.files[0]!.bytes).toString("utf8").includes(marker); + }); const configured = snapshot.codexCliPath; let candidate: string | null; if (configured) { if (/^[a-z]:[\\/]/i.test(configured)) { - if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" }; + if (await exists(configured) !== true) return { kind: "unavailable", reason: "candidate_unavailable" }; candidate = win32.normalize(configured); } else { if (configured.includes("/") || configured.includes("\\")) { return { kind: "unavailable", reason: "candidate_unavailable" }; } - candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists); + candidate = await scanPath(configured, snapshot.path, snapshot.pathExt, exists); if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" }; } } else { - candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists); + candidate = await scanPath("codex", snapshot.path, snapshot.pathExt, exists); if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" }; } // An OpenCodex wrapper at the npm prefix is our own launcher, not the npm // artifact. The renamed original beside it is the file npm wrote. - if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) { - const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd"; - if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" }; - candidate = backing; + if (/\.cmd$/i.test(candidate)) { + const marker = await fileContains(candidate, SHIM_MARKER); + if (marker === "unavailable") return { kind: "unavailable", reason: "candidate_unavailable" }; + if (marker) { + const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd"; + if (await exists(backing) !== true) return { kind: "unavailable", reason: "unsupported_layout" }; + candidate = backing; + } } const base = win32.basename(candidate).toLowerCase(); @@ -131,19 +133,19 @@ export function deriveCodexCliInstallationInput( } else { return { kind: "unavailable", reason: "unsupported_layout" }; } - if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) { + if (await exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json")) !== true) { return { kind: "unavailable", reason: "unsupported_layout" }; } // The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH. let node = win32.join(prefix, "node.exe"); - if (!exists(node)) { - const resolved = scanPath("node.exe", snapshot.path, null, exists); + if (await exists(node) !== true) { + const resolved = await scanPath("node.exe", snapshot.path, null, exists); if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" }; node = resolved; } const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js"); - if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" }; + if (await exists(npmCli) !== true) return { kind: "unavailable", reason: "toolchain_unresolved" }; return { kind: "derived", diff --git a/src/codex/windows-installation-files.ts b/src/codex/windows-installation-files.ts index 6e25f86a954..972fe86b37a 100644 --- a/src/codex/windows-installation-files.ts +++ b/src/codex/windows-installation-files.ts @@ -18,6 +18,10 @@ export interface WindowsInstallationFileRequest { readonly path: string; readonly maxBytes: number; readonly hashOnly?: boolean; + /** Validate and hold the path without reading its contents. */ + readonly metadataOnly?: boolean; + /** Read at most maxBytes from the start instead of refusing an oversized file. */ + readonly prefixOnly?: boolean; } export interface WindowsInstallationFileIdentity { readonly volumeSerial: string; @@ -76,6 +80,8 @@ export async function inspectWindowsInstallationFiles( const parsedPath = request && components(request.path); if (!parsedPath || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 0 || (request.hashOnly !== undefined && typeof request.hashOnly !== "boolean") + || (request.metadataOnly !== undefined && typeof request.metadataOnly !== "boolean") + || (request.prefixOnly !== undefined && typeof request.prefixOnly !== "boolean") || request.maxBytes > (request.hashOnly ? 256 * MIB : MIB)) return null; ceiling += request.maxBytes; return parsedPath; @@ -184,29 +190,35 @@ export async function inspectWindowsInstallationFiles( } const handle = relativeOpen(parent, names[names.length - 1]!, false); const identity = inspect(handle, false); - if (identity.size > request.maxBytes) throw new InspectionRefusal("size-limit"); + if (!request.metadataOnly && !(request.prefixOnly && !request.hashOnly) + && identity.size > request.maxBytes) throw new InspectionRefusal("size-limit"); return { request, handle, identity }; }); openedForTests?.(); const observed = files.map(({ request, handle, identity }) => { const hash = createHash("sha256"); - const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(identity.size); - const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, identity.size))); + if (request.metadataOnly) return { path: request.path, identity, bytes: new Uint8Array(), digest: "" }; + const truncated = Boolean(request.prefixOnly) && !request.hashOnly && identity.size > request.maxBytes; + const readLimit = truncated ? request.maxBytes : identity.size; + const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(readLimit); + const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, readLimit))); const read = Buffer.alloc(4); let offset = 0; - while (offset < identity.size) { - const length = Math.min(chunk.length, identity.size - offset); + while (offset < readLimit) { + const length = Math.min(chunk.length, readLimit - offset); if (!k.ReadFile!(handle, ffi.ptr(chunk), length, ffi.ptr(read), null)) throw new InspectionRefusal("read-failed"); const count = read.readUInt32LE(0); if (!count || count > length) throw new InspectionRefusal("read-failed"); - hash.update(chunk.subarray(0, count)); if (!request.hashOnly) bytes.set(chunk.subarray(0, count), offset); + if (!truncated) hash.update(chunk.subarray(0, count)); offset += count; } - if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) { - throw new InspectionRefusal("identity-changed"); + if (!truncated) { + if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) { + throw new InspectionRefusal("identity-changed"); + } } - return { path: request.path, identity, bytes, digest: hash.digest("hex") }; + return { path: request.path, identity, bytes, digest: truncated ? "" : hash.digest("hex") }; }); for (const file of files) { if (JSON.stringify(inspect(file.handle, false)) !== JSON.stringify(file.identity)) { diff --git a/structure/runtime.md b/structure/runtime.md index 384b6b394e6..f42090e9446 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -96,7 +96,9 @@ backing, the npm prefix layout, and the Node/npm toolchain beside the resolved n Configured values containing a path separator must be drive-absolute; otherwise derivation refuses with `candidate_unavailable` instead of substituting a different PATH candidate. Bare command names and the unset default continue to resolve only through the captured PATH. -Discovery only proposes paths and never reads ambient state. Four explicit absolute paths +Discovery probes candidates through the same local-volume, reparse-refusing held-handle +reader as final observation, so captured PATH entries cannot trigger network filesystem I/O. +It never reads ambient state. Four explicit absolute paths remain accepted as an all-or-none override. Only the standard npm command shim or direct Codex package entry is accepted. The native reader in `src/codex/windows-installation-files.ts` holds ancestor/file handles for bounded reads and diff --git a/tests/cli/cli-codex-cli-update.test.ts b/tests/cli/cli-codex-cli-update.test.ts index e31c675a32b..4e2161d406c 100644 --- a/tests/cli/cli-codex-cli-update.test.ts +++ b/tests/cli/cli-codex-cli-update.test.ts @@ -65,7 +65,7 @@ describe("Codex CLI update CLI", () => { try { console.log = (...values: unknown[]) => logs.push(values.map(String).join(" ")); expect(await handleCodexCliUpdateCommand(["attest", "--json"], { - deriveInstallationInput: input => { + deriveInstallationInput: async input => { snapshot = input; return { kind: "derived", input: { ...identityInput, candidateSource: "selected" as const } }; }, @@ -92,7 +92,7 @@ describe("Codex CLI update CLI", () => { try { console.log = (...values: unknown[]) => logs.push(values.map(String).join(" ")); expect(await handleCodexCliUpdateCommand(["attest", "--json"], { - deriveInstallationInput: () => ({ kind: "unavailable", reason: "candidate_unavailable" }), + deriveInstallationInput: async () => ({ kind: "unavailable", reason: "candidate_unavailable" }), inspectIdentity: async () => { inspectCalls += 1; return identityReport; }, })).toBe(0); expect(inspectCalls).toBe(0); diff --git a/tests/codex-integration/codex-cli-installation-targets.test.ts b/tests/codex-integration/codex-cli-installation-targets.test.ts index bd1e49dc376..5e662acfd8c 100644 --- a/tests/codex-integration/codex-cli-installation-targets.test.ts +++ b/tests/codex-integration/codex-cli-installation-targets.test.ts @@ -38,9 +38,9 @@ function snapshot(extra: Partial = {}): CodexCliIn } describe("selected Codex CLI installation target derivation", () => { - test("refuses non-Windows platforms before touching the filesystem", () => { + test("refuses non-Windows platforms before touching the filesystem", async () => { let probed = 0; - const result = deriveCodexCliInstallationInput(snapshot(), { + const result = await deriveCodexCliInstallationInput(snapshot(), { platform: "linux", exists: () => { probed += 1; return true; }, }); @@ -48,20 +48,36 @@ describe("selected Codex CLI installation target derivation", () => { expect(probed).toBe(0); }); - test("reports candidate_unavailable when nothing identifies a candidate", () => { + test("reports candidate_unavailable when nothing identifies a candidate", async () => { for (const snap of [ {}, { codexCliPath: null, path: null }, { codexCliPath: "C:\\missing\\codex.cmd", path: PREFIX }, { codexCliPath: null, path: "C:\\empty" }, ]) { - expect(deriveCodexCliInstallationInput(snap, depsFor(fixtureFiles()))) + expect(await deriveCodexCliInstallationInput(snap, depsFor(fixtureFiles()))) .toEqual({ kind: "unavailable", reason: "candidate_unavailable" }); } }); - test("derives the npm-global layout from the configured candidate", () => { - const result = deriveCodexCliInstallationInput( + test("a refused PATH probe stops the scan instead of attesting a later candidate", async () => { + const files = fixtureFiles(); + files.add((NODE_DIR + "\\codex.cmd").toLowerCase()); + const refused = PREFIX + "\\codex.cmd"; + const deps = depsFor(files); + const result = await deriveCodexCliInstallationInput( + snapshot({ codexCliPath: "codex" }), + { + ...deps, + exists: (path: string) => + path.toLowerCase() === refused.toLowerCase() ? "refused" : deps.exists(path), + }, + ); + expect(result).toEqual({ kind: "unavailable", reason: "candidate_unavailable" }); + }); + + test("derives the npm-global layout from the configured candidate", async () => { + const result = await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(fixtureFiles()), ); @@ -77,11 +93,11 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("resolves the first PATH codex.cmd and prefers a prefix-local node.exe", () => { + test("resolves the first PATH codex.cmd and prefers a prefix-local node.exe", async () => { const files = fixtureFiles(); files.add((PREFIX + "\\node.exe").toLowerCase()); files.add((PREFIX + "\\node_modules\\npm\\bin\\npm-cli.js").toLowerCase()); - const result = deriveCodexCliInstallationInput( + const result = await deriveCodexCliInstallationInput( snapshot({ path: "C:\\nowhere;" + PREFIX + ";C:\\later;" + NODE_DIR }), depsFor(files), ); @@ -97,22 +113,22 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("an earlier PATH codex.exe is the selected launcher and refuses the layout", () => { + test("an earlier PATH codex.exe is the selected launcher and refuses the layout", async () => { const files = fixtureFiles(); files.delete((PREFIX + "\\codex.cmd").toLowerCase()); files.add("c:\\bin\\codex.exe"); - const result = deriveCodexCliInstallationInput( + const result = await deriveCodexCliInstallationInput( snapshot({ path: "C:\\bin;" + PREFIX }), depsFor(files), ); expect(result).toEqual({ kind: "unavailable", reason: "unsupported_layout" }); }); - test("an OpenCodex wrapper attests the renamed npm artifact, not the wrapper", () => { + test("an OpenCodex wrapper attests the renamed npm artifact, not the wrapper", async () => { const files = fixtureFiles(); files.add((PREFIX + "\\codex.opencodex-real.cmd").toLowerCase()); const marked = new Set([(PREFIX + "\\codex.cmd").toLowerCase()]); - const result = deriveCodexCliInstallationInput( + const result = await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(files, marked), ); @@ -128,19 +144,29 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("a wrapper without its npm backing refuses instead of attesting our own launcher", () => { + test("a wrapper without its npm backing refuses instead of attesting our own launcher", async () => { const marked = new Set([(PREFIX + "\\codex.cmd").toLowerCase()]); - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(fixtureFiles(), marked), )).toEqual({ kind: "unavailable", reason: "unsupported_layout" }); }); - test("a fresh npm shim replacing the wrapper attests it directly, ignoring a stale backing", () => { + test("an unreadable wrapper probe is unavailable, not marker absence", async () => { + const files = fixtureFiles(); + files.add((PREFIX + "\\codex.opencodex-real.cmd").toLowerCase()); + const result = await deriveCodexCliInstallationInput( + snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), + { ...depsFor(files), fileContains: () => "unavailable" as const }, + ); + expect(result).toEqual({ kind: "unavailable", reason: "candidate_unavailable" }); + }); + + test("a fresh npm shim replacing the wrapper attests it directly, ignoring a stale backing", async () => { const files = fixtureFiles(); files.add((PREFIX + "\\codex.opencodex-real.cmd").toLowerCase()); // codex.cmd does NOT contain the marker: npm install -g overwrote the wrapper. - const result = deriveCodexCliInstallationInput( + const result = await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(files), ); @@ -150,9 +176,9 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("a direct package bin/codex.js candidate derives its owning prefix", () => { + test("a direct package bin/codex.js candidate derives its owning prefix", async () => { const bin = PREFIX + "\\node_modules\\@openai\\codex\\bin\\codex.js"; - const result = deriveCodexCliInstallationInput( + const result = await deriveCodexCliInstallationInput( snapshot({ codexCliPath: bin }), depsFor(fixtureFiles()), ); @@ -168,42 +194,42 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("a candidate outside the npm package layout is unsupported", () => { + test("a candidate outside the npm package layout is unsupported", async () => { const files = fixtureFiles(); files.add("c:\\tools\\codex.cmd"); - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: "C:\\tools\\codex.cmd" }), depsFor(files), )).toEqual({ kind: "unavailable", reason: "unsupported_layout" }); }); - test("a prefix without the codex package manifest is unsupported", () => { + test("a prefix without the codex package manifest is unsupported", async () => { const files = fixtureFiles(); files.delete((PREFIX + "\\node_modules\\@openai\\codex\\package.json").toLowerCase()); - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(files), )).toEqual({ kind: "unavailable", reason: "unsupported_layout" }); }); - test("missing node.exe or npm-cli.js refuses the toolchain, not the candidate", () => { + test("missing node.exe or npm-cli.js refuses the toolchain, not the candidate", async () => { const noNode = fixtureFiles(); noNode.delete((NODE_DIR + "\\node.exe").toLowerCase()); - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(noNode), )).toEqual({ kind: "unavailable", reason: "toolchain_unresolved" }); const noNpm = fixtureFiles(); noNpm.delete((NODE_DIR + "\\node_modules\\npm\\bin\\npm-cli.js").toLowerCase()); - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: PREFIX + "\\codex.cmd" }), depsFor(noNpm), )).toEqual({ kind: "unavailable", reason: "toolchain_unresolved" }); }); - test("a bare configured command name resolves through the captured PATH", () => { - const result = deriveCodexCliInstallationInput( + test("a bare configured command name resolves through the captured PATH", async () => { + const result = await deriveCodexCliInstallationInput( snapshot({ codexCliPath: "codex" }), depsFor(fixtureFiles()), ); @@ -216,9 +242,9 @@ describe("selected Codex CLI installation target derivation", () => { }); }); - test("a relative path-shaped configured candidate refuses instead of substituting PATH codex", () => { + test("a relative path-shaped configured candidate refuses instead of substituting PATH codex", async () => { for (const configured of ["tools\\codex.cmd", "tools/codex.cmd"]) { - expect(deriveCodexCliInstallationInput( + expect(await deriveCodexCliInstallationInput( snapshot({ codexCliPath: configured }), depsFor(fixtureFiles()), )).toEqual({ kind: "unavailable", reason: "candidate_unavailable" }); diff --git a/tests/codex-integration/codex-cli-windows-installation-files.test.ts b/tests/codex-integration/codex-cli-windows-installation-files.test.ts index 86fa951f929..d9c1df4b16e 100644 --- a/tests/codex-integration/codex-cli-windows-installation-files.test.ts +++ b/tests/codex-integration/codex-cli-windows-installation-files.test.ts @@ -127,4 +127,39 @@ describe("explicit Windows installation file snapshot", () => { expect(result.files[0]!.identity.size).toBe(3 * 1024 * 1024); expect(result.files[0]!.digest).toBe(expected.digest("hex")); }); + + nativeTest("metadataOnly observes an oversized file's identity without reading or hashing", async () => { + const path = join(fixture, "oversized.fixture"); + writeFileSync(path, Buffer.alloc(2048, 0x6f)); + const result = await inspectWindowsInstallationFiles([{ path, maxBytes: 0, metadataOnly: true }]); + expect(result.kind).toBe("observed"); + if (result.kind !== "observed") return; + const file = result.files[0]!; + expect(file.path).toBe(path); + expect(file.identity.size).toBe(2048); + expect(file.identity.fileId).toMatch(/^[a-f0-9]{32}$/); + expect(file.bytes).toHaveLength(0); + expect(file.digest).toBe(""); + }); + + nativeTest("prefixOnly returns a bounded prefix of an oversized file without a digest", async () => { + const path = join(fixture, "wrapper.cmd"); + const prefix = Buffer.from("@echo off\r\nrem marker\r\n"); + const tail = Buffer.alloc(4096, 0x20); + writeFileSync(path, Buffer.concat([prefix, tail])); + const result = await inspectWindowsInstallationFiles([{ path, maxBytes: prefix.length, prefixOnly: true }]); + expect(result.kind).toBe("observed"); + if (result.kind !== "observed") return; + const file = result.files[0]!; + expect(Buffer.from(file.bytes)).toEqual(prefix); + expect(file.identity.size).toBe(prefix.length + tail.length); + expect(file.digest).toBe(""); + const small = join(fixture, "small.cmd"); + writeFileSync(small, prefix); + const full = await inspectWindowsInstallationFiles([{ path: small, maxBytes: 1024, prefixOnly: true }]); + expect(full.kind).toBe("observed"); + if (full.kind !== "observed") return; + expect(Buffer.from(full.files[0]!.bytes)).toEqual(prefix); + expect(full.files[0]!.digest).toBe(createHash("sha256").update(prefix).digest("hex")); + }); });