diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index 0ad91d0e89..9f83929deb 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { readStableArtifact } from "../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFailure, @@ -110,11 +111,7 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => { return new AnalysisAccessDeniedError(OPERATION, path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index 9e7d61d4cb..2de3d83c06 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js"; import { Il2cppMetadataFormatFailure } from "./Il2cppMetadataFormat.js"; import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js"; @@ -138,11 +139,7 @@ const failureForPrimary = ( return new AnalysisAccessDeniedError(OPERATION, path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: [field], diff --git a/src/native/elf/ElfNotesProvider.ts b/src/native/elf/ElfNotesProvider.ts index 9adcb4a3ef..896e5c36bd 100644 --- a/src/native/elf/ElfNotesProvider.ts +++ b/src/native/elf/ElfNotesProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { @@ -201,11 +202,7 @@ const elfNotesFailurePrimary = ( return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/src/native/exports/NativeExportsProvider.ts b/src/native/exports/NativeExportsProvider.ts index 4678907d62..8931e41b6c 100644 --- a/src/native/exports/NativeExportsProvider.ts +++ b/src/native/exports/NativeExportsProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { @@ -161,11 +162,7 @@ const nativeExportsFailurePrimary = ( return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/src/native/pe/PeCertificatesProvider.ts b/src/native/pe/PeCertificatesProvider.ts index 87138b5f78..5f2c70a740 100644 --- a/src/native/pe/PeCertificatesProvider.ts +++ b/src/native/pe/PeCertificatesProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { @@ -148,11 +149,7 @@ const peCertificatesFailurePrimary = ( return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/src/native/pe/PeResourcesProvider.ts b/src/native/pe/PeResourcesProvider.ts index 90846b3193..0266f962ba 100644 --- a/src/native/pe/PeResourcesProvider.ts +++ b/src/native/pe/PeResourcesProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js"; import { fixedNativeAllowance } from "../../domain/native/nativeDeadlines.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; @@ -172,7 +173,7 @@ const peResourcesFailurePrimary = ( return new AnalysisAccessDeniedError(operation, input.path, cause.code, { cause, }); - if (cause.code === "ENOENT" || cause.code === "ENOTDIR") + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(operation, { cause }, [ { path: ["path"], diff --git a/src/python/PythonPycProvider.ts b/src/python/PythonPycProvider.ts index c291e1b7f1..3d76bca80c 100644 --- a/src/python/PythonPycProvider.ts +++ b/src/python/PythonPycProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { artifactFileCancellationCause, artifactFilePrimaryFailure, @@ -96,11 +97,7 @@ const failureForPrimary = (cause: unknown, input: InspectPythonPycInput) => { return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index 797dbed443..5766e6e0e6 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -1,3 +1,4 @@ +import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { readStableArtifact } from "../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFailure, @@ -299,11 +300,7 @@ const artifactFailure = (cause: unknown, path: string): AnalysisError => { return new AnalysisAccessDeniedError(OPERATION, path, cause.code, { cause, }); - if ( - ["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes( - String(cause.code), - ) - ) + if (isSelectedRegularFileError(cause)) return new AnalysisInputError(OPERATION, { cause }, [ { path: ["path"], diff --git a/tests/boundary/filesystem/peResources.test.ts b/tests/boundary/filesystem/peResources.test.ts index 70ae2b4f27..7945cb4fc0 100644 --- a/tests/boundary/filesystem/peResources.test.ts +++ b/tests/boundary/filesystem/peResources.test.ts @@ -38,6 +38,30 @@ describe("PE resource stable filesystem boundary", () => { } }); + it.skipIf(process.platform === "win32")( + "reports a native symlink-loop failure as invalid selected-path input", + async () => { + const root = await createTestTempDirectory("rea-pe-resource-path-loop-"); + const loop = join(root, "loop"); + await symlink(loop, loop, "dir"); + const path = join(loop, "image.exe"); + const result = await createPeResourcesService().inspect({ path }); + if (result.ok) throw new Error("Expected symlink-loop failure"); + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + operation: "inspect_pe_resources", + issues: [ + { + path: ["path"], + reason: "invalid_value", + message: expect.stringContaining("ELOOP"), + }, + ], + cause: { code: "ELOOP" }, + }); + }, + ); + it("returns typed resource and cancellation failures", async () => { const root = await createTestTempDirectory("rea-pe-resource-budget-"); const path = join(root, "image.exe"); diff --git a/tests/process-global/stableArtifactConsumers.test.ts b/tests/process-global/stableArtifactConsumers.test.ts index 6bd07285b9..b166e9015f 100644 --- a/tests/process-global/stableArtifactConsumers.test.ts +++ b/tests/process-global/stableArtifactConsumers.test.ts @@ -67,6 +67,7 @@ import { parseEvidence } from "../../src/domain/evidence.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { AnalysisCancelledError, + AnalysisInputError, AnalysisArtifactChangedError, AnalysisAccessDeniedError, AnalysisResourceConstraintError, @@ -696,15 +697,19 @@ it("preserves upstream resource-bound facts through an unconfirmed descriptor cl } }); -const fileFailureProviders = [ +const stableArtifactProviders = [ ...staticProviders, ["ELF notes", () => new ElfNotesProvider()], ["PE certificates", () => new PeCertificatesProvider()], ["native exports", () => new NativeExportsProvider()], - ["BEAM", () => new BeamProvider()], ["Python pyc", () => new PythonPycProvider()], ] as const; +const fileFailureProviders = [ + ...stableArtifactProviders, + ["BEAM", () => new BeamProvider()], +] as const; + const inspectFileFailure = ( provider: ReturnType<(typeof fileFailureProviders)[number][1]>, path: string, @@ -744,6 +749,55 @@ const inspectFileFailure = ( return provider.inspect({ path }, options); }; +it.each( + stableArtifactProviders.flatMap(([name, makeProvider]) => + [ + ["ESTALE", "AnalysisInputError"], + ["ENOTFILE", "AnalysisInputError"], + ["EISDIR", "AnalysisInputError"], + ["ELOOP", "AnalysisInputError"], + ["EACCES", "AnalysisAccessDeniedError"], + ["EIO", undefined], + ].map(([code, expectedTag]) => ({ name, makeProvider, code, expectedTag })), + ), +)( + "preserves $name descriptor failure classification for $code", + async ({ makeProvider, code, expectedTag }) => { + const path = await selectedImage(); + const provider = makeProvider(); + const physical = Object.assign(new Error("descriptor metadata failed"), { + code, + }); + fault.blockNew = false; + fault.readFailure = { cause: physical }; + try { + const result = await inspectFileFailure(provider, path); + if (result.ok) throw new Error("Expected physical read failure"); + expect(result.error.cause).toBe(physical); + expect(result.error._tag).toBe( + expectedTag ?? + (provider instanceof ElfNotesProvider || + provider instanceof PeCertificatesProvider || + provider instanceof PeResourcesProvider + ? "ArtifactOperationError" + : "ProviderAdapterError"), + ); + if (result.error instanceof AnalysisInputError) + expect(result.error.issues).toEqual([ + { + path: ["path"], + reason: "invalid_value", + message: expect.stringContaining(path), + }, + ]); + expect(fault.handles).toHaveLength(1); + expect(fault.handles[0]!.fd).toBe(-1); + } finally { + await provider.close(); + } + }, +); + it.each(fileFailureProviders)( "uses the original %s physical failure across nested descriptor owners", async (_name, makeProvider) => {