fix(hopper): keep the OS error when the FAT64 source cannot be reopened

resolveHopperMachOImage built its BinaryTargetError without the open
failure as its cause, so a target that became unreadable after admission
still projected as `target_unavailable` instead of `access_denied`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-09 04:04:42 +00:00
parent d0ff57a45c
commit 9f5dc52be8
2 changed files with 47 additions and 2 deletions

View File

@ -0,0 +1,37 @@
import { chmod, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { resolveHopperMachOImage } from "./HopperMachOImage.js";
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"reports a FAT64 source that became unreadable as a host read denial",
async () => {
const directory = await createTestTempDirectory("rea-hopper-fat64-");
const path = join(directory, "universal");
await writeFile(path, Buffer.alloc(64));
await chmod(path, 0);
try {
const result = await resolveHopperMachOImage({
path,
sha256: "0".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({ systemCode: "EACCES" });
expect(projectAnalysisError(result.error)).toMatchObject({
code: "access_denied",
details: { path, system_code: "EACCES" },
});
} finally {
await chmod(path, 0o600);
}
},
);

View File

@ -35,15 +35,23 @@ export const resolveHopperMachOImage = async (
): Promise<
Result<HopperMachOImage, BinaryTargetError | AnalysisCancelledError>
> => {
const invalid = (reason: string) =>
err(new BinaryTargetError(target.path, reason));
const invalid = (reason: string, cause?: unknown) =>
err(
new BinaryTargetError(
target.path,
reason,
cause === undefined ? undefined : { cause },
),
);
if (signal?.aborted) return err(new AnalysisCancelledError("open_binary"));
let file: FileHandle;
try {
file = await open(target.path, "r");
} catch (cause: unknown) {
// Keep the OS error so a read-permission denial reports access_denied.
return invalid(
`Cannot read FAT64 source for Hopper image preparation: ${cause instanceof Error ? cause.message : String(cause)}`,
cause,
);
}
try {