fix(native): report missing and non-file PE resource targets as input errors

`inspect_pe_resources` turned every acquisition failure other than
limits and access denial into `artifact_operation_failed` with "Artifact
could not be read or written. Check file access and try again." A
missing path, a directory, a symlink or a path under a regular file got
that advice, although the caller selected a target that cannot be a PE
image.

Map the stable reader's non-regular-file refusal and ENOENT/ENOTDIR to
`AnalysisInputError` issues on `path`, and a file that changed during the
read to `AnalysisArtifactChangedError`, as the sibling EVM carrier read
does. Other I/O failures stay artifact faults.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kaoru0822
2026-10-11 02:30:25 +00:00
co-authored by Claude Opus 5.5
parent 83e52559b8
commit e1def2bf1a
2 changed files with 76 additions and 44 deletions
+58 -41
View File
@@ -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 },
);
};
+18 -3
View File
@@ -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),
},
],
});
}
});