diff --git a/src/native/pe/PeResourcesProvider.ts b/src/native/pe/PeResourcesProvider.ts index 50fb13a29..2093d44b5 100644 --- a/src/native/pe/PeResourcesProvider.ts +++ b/src/native/pe/PeResourcesProvider.ts @@ -5,7 +5,9 @@ import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { AnalysisAccessDeniedError, + AnalysisArtifactChangedError, AnalysisCancelledError, + AnalysisInputError, AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; import type { InspectPeResourcesInput } from "../../domain/native/peResources.js"; @@ -37,47 +39,62 @@ export class PeResourcesProvider implements PeResourcesPort { ), ); } catch (cause) { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError("inspect_pe_resources", { cause }), - ); - if (cause instanceof ArtifactReaderFailure && cause.reason === "limit") - return err( - new AnalysisResourceConstraintError( - "inspect_pe_resources", - "memory", - cause.message, - { - max_file_bytes: input.max_file_bytes, - max_entries: input.max_entries, - }, - { cause }, - ), - ); - if ( - cause instanceof Error && - "code" in cause && - (cause.code === "EACCES" || cause.code === "EPERM") - ) - return err( - new AnalysisAccessDeniedError( - "inspect_pe_resources", - input.path, - cause.code, - { cause }, - ), - ); - return err( - new ArtifactOperationError( - "inspect_pe_resources", - cause instanceof ArtifactReaderFailure ? cause.reason : "io", - undefined, - cause instanceof Error - ? cause.message - : "PE artifact inspection failed.", - { cause }, - ), - ); + return err(peResourcesFailure(cause, input, options?.signal)); } } } + +const errorCode = (cause: unknown): unknown => + cause instanceof Error && "code" in cause ? cause.code : undefined; + +// A missing or non-regular caller selection is an input error, as for the +// EVM bytecode carrier read; only other I/O remains an artifact fault. +const peResourcesFailure = ( + cause: unknown, + input: InspectPeResourcesInput, + signal: AbortSignal | undefined, +) => { + const operation = "inspect_pe_resources"; + if (signal?.aborted) return new AnalysisCancelledError(operation, { cause }); + const reason = cause instanceof ArtifactReaderFailure ? cause.reason : "io"; + const message = cause instanceof Error ? cause.message : String(cause); + if (reason === "limit") + return new AnalysisResourceConstraintError( + operation, + "memory", + message, + { + max_file_bytes: input.max_file_bytes, + max_entries: input.max_entries, + }, + { cause }, + ); + if (reason === "path") + return new AnalysisInputError(operation, { cause }, [ + { path: ["path"], reason: "invalid_format", message }, + ]); + if (reason === "integrity") + return new AnalysisArtifactChangedError(operation, input.path, message, { + cause, + }); + const code = errorCode(cause); + if (code === "ENOENT" || code === "ENOTDIR") + return new AnalysisInputError(operation, { cause }, [ + { + path: ["path"], + reason: "invalid_value", + message: `Selected PE image could not be read (${code}): ${input.path}.`, + }, + ]); + if (code === "EACCES" || code === "EPERM") + return new AnalysisAccessDeniedError(operation, input.path, code, { + cause, + }); + return new ArtifactOperationError( + operation, + reason, + undefined, + cause instanceof Error ? cause.message : "PE artifact inspection failed.", + { cause }, + ); +}; diff --git a/tests/boundary/filesystem/peResources.test.ts b/tests/boundary/filesystem/peResources.test.ts index a343feaa1..b704c51de 100644 --- a/tests/boundary/filesystem/peResources.test.ts +++ b/tests/boundary/filesystem/peResources.test.ts @@ -6,17 +6,32 @@ import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("PE resource stable filesystem boundary", () => { - it("rejects symlinks, missing files and directories without inventing empty inventory", async () => { + it("rejects symlinks, missing files and directories as caller input without inventing empty inventory", async () => { const root = await createTestTempDirectory("rea-pe-resource-files-"); const path = join(root, "image.exe"); const alias = join(root, "alias.exe"); await writeFile(path, peResourceFixture().bytes); await symlink(path, alias); const service = createPeResourcesService(); - for (const candidate of [root, alias, join(root, "missing.exe")]) { + for (const [candidate, reason] of [ + [root, "invalid_format"], + [alias, "invalid_format"], + [join(root, "missing.exe"), "invalid_value"], + [join(path, "child.exe"), "invalid_value"], + ] as const) { const result = await service.inspect({ path: candidate }); if (result.ok) throw new Error("Expected file acquisition failure"); - expect(result.error._tag).toBe("ArtifactOperationError"); + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + operation: "inspect_pe_resources", + issues: [ + { + path: ["path"], + reason, + message: expect.stringContaining(candidate), + }, + ], + }); } });