From 9f5dc52be8420fb5e874595d63312bf80d07a3a2 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:04:42 +0000 Subject: [PATCH] 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 --- src/hopper/HopperMachOImage.test.ts | 37 +++++++++++++++++++++++++++++ src/hopper/HopperMachOImage.ts | 12 ++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/hopper/HopperMachOImage.test.ts diff --git a/src/hopper/HopperMachOImage.test.ts b/src/hopper/HopperMachOImage.test.ts new file mode 100644 index 000000000..cb96a57a1 --- /dev/null +++ b/src/hopper/HopperMachOImage.test.ts @@ -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); + } + }, +); diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts index 0b6ac600e..1ba69861f 100644 --- a/src/hopper/HopperMachOImage.ts +++ b/src/hopper/HopperMachOImage.ts @@ -35,15 +35,23 @@ export const resolveHopperMachOImage = async ( ): Promise< Result > => { - 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 {