fix(providers): share selected-file error classification

This commit is contained in:
morluto committed 2026-10-24 00:54:28 +08:00
1 parent 8d8d85ba22
commit a32d731ed7
10 files changed
+96 -38

No files matched your search

+2 -5
View File
@@ -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"],
+2 -5
View File
@@ -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],
+2 -5
View File
@@ -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"],
+2 -5
View File
@@ -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"],
+2 -5
View File
@@ -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"],
+2 -1
View File
@@ -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"],
+2 -5
View File
@@ -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"],
+2 -5
View File
@@ -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"],
@@ -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");
@@ -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) => {