diff --git a/src/erlang/BeamProvider.ts b/src/erlang/BeamProvider.ts index c7a1fe15f5..d4b79afbda 100644 --- a/src/erlang/BeamProvider.ts +++ b/src/erlang/BeamProvider.ts @@ -1,7 +1,7 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, bindArtifactFileCleanup, artifactFileCancellationCause, } from "../artifacts/ArtifactFileOperation.js"; @@ -85,7 +85,7 @@ export class BeamProvider implements BeamPort { ); }); } catch (cause) { - return err(beamFailure(cause, input, options?.signal)); + return err(beamFailure(cause, input)); } } } @@ -185,18 +185,8 @@ const beamFailurePrimary = (cause: unknown, input: InspectErlangBeamInput) => { ); }; -const beamFailure = ( - cause: unknown, - input: InspectErlangBeamInput, - signal: AbortSignal | undefined, -) => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? beamFailurePrimary(cause.fileCleanupFailure.outcome.cause, input) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : beamFailurePrimary(cause, input); +const beamFailure = (cause: unknown, input: InspectErlangBeamInput) => { + const primary = beamFailurePrimary(artifactFilePrimaryFailure(cause), input); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index 838eb16c70..c9d96be25b 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -1,6 +1,6 @@ import { readStableArtifact } from "../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, assertArtifactReadActive, bindArtifactFileCleanup, artifactFileCancellationCause, @@ -132,18 +132,8 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => { }); }; -const failureFor = ( - cause: unknown, - path: string, - signal?: AbortSignal, -): AnalysisError => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? failureForPrimary(cause.fileCleanupFailure.outcome.cause, path) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : failureForPrimary(cause, path); +const failureFor = (cause: unknown, path: string): AnalysisError => { + const primary = failureForPrimary(artifactFilePrimaryFailure(cause), path); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( @@ -308,7 +298,7 @@ export class GoBinaryProvider implements GoBinaryPort { return ok(parsed.data); }); } catch (cause: unknown) { - return err(failureFor(cause, input.path, options?.signal)); + return err(failureFor(cause, input.path)); } } } diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index b215ec461a..3074606422 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -11,6 +11,7 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, ArtifactFileCancelled, bindArtifactFileCleanup, artifactFileCancellationCause, @@ -67,7 +68,6 @@ const OUTPUT_BYTES = 64 * 1024 * 1024; const failureForPrimary = ( cause: unknown, path: string, - signal?: AbortSignal, field: "path" | "binary_path" = "path", maximum = INPUT_BYTES, ): AnalysisError => { @@ -164,23 +164,15 @@ const failureForPrimary = ( const failureFor = ( cause: unknown, path: string, - signal?: AbortSignal, field: "path" | "binary_path" = "path", maximum = INPUT_BYTES, ): AnalysisError => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? failureForPrimary( - cause.fileCleanupFailure.outcome.cause, - path, - signal, - field, - maximum, - ) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : failureForPrimary(cause, path, signal, field, maximum); + const primary = failureForPrimary( + artifactFilePrimaryFailure(cause), + path, + field, + maximum, + ); if ( cause instanceof ArtifactReaderFailure && cause.partialObservation !== undefined && @@ -511,7 +503,6 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort { const failure = failureFor( cause, binaryPath, - options?.signal, "binary_path", BINARY_BYTES, ); diff --git a/src/native/elf/ElfNotesProvider.ts b/src/native/elf/ElfNotesProvider.ts index 06c25408fe..766148a92e 100644 --- a/src/native/elf/ElfNotesProvider.ts +++ b/src/native/elf/ElfNotesProvider.ts @@ -1,7 +1,7 @@ import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, bindArtifactFileCleanup, artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; @@ -78,7 +78,7 @@ export class ElfNotesProvider implements ElfNotesPort { ); }); } catch (cause) { - return err(elfNotesFailure(cause, input, options?.signal)); + return err(elfNotesFailure(cause, input)); } } } @@ -181,18 +181,11 @@ const elfNotesFailurePrimary = ( ); }; -const elfNotesFailure = ( - cause: unknown, - input: InspectElfNotesInput, - signal: AbortSignal | undefined, -) => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? elfNotesFailurePrimary(cause.fileCleanupFailure.outcome.cause, input) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : elfNotesFailurePrimary(cause, input); +const elfNotesFailure = (cause: unknown, input: InspectElfNotesInput) => { + const primary = elfNotesFailurePrimary( + artifactFilePrimaryFailure(cause), + input, + ); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( diff --git a/src/native/exports/NativeExportsProvider.ts b/src/native/exports/NativeExportsProvider.ts index 49291eee81..67e7bda0f7 100644 --- a/src/native/exports/NativeExportsProvider.ts +++ b/src/native/exports/NativeExportsProvider.ts @@ -1,7 +1,7 @@ import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, assertArtifactReadActive, bindArtifactFileCleanup, artifactFileCancellationCause, @@ -81,7 +81,7 @@ export class NativeExportsProvider implements NativeExportsPort { return ok(report); }); } catch (cause) { - return err(nativeExportsFailure(cause, input, options?.signal)); + return err(nativeExportsFailure(cause, input)); } } } @@ -186,18 +186,11 @@ const nativeExportsFailurePrimary = ( const nativeExportsFailure = ( cause: unknown, input: InspectNativeExportsInput, - signal: AbortSignal | undefined, ): AnalysisError => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? nativeExportsFailurePrimary( - cause.fileCleanupFailure.outcome.cause, - input, - ) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : nativeExportsFailurePrimary(cause, input); + const primary = nativeExportsFailurePrimary( + artifactFilePrimaryFailure(cause), + input, + ); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( diff --git a/src/native/pe/PeCertificatesProvider.ts b/src/native/pe/PeCertificatesProvider.ts index bd64e37c05..cd3b727ff2 100644 --- a/src/native/pe/PeCertificatesProvider.ts +++ b/src/native/pe/PeCertificatesProvider.ts @@ -1,7 +1,7 @@ import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, bindArtifactFileCleanup, artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; @@ -77,7 +77,7 @@ export class PeCertificatesProvider implements PeCertificatesPort { ); }); } catch (cause) { - return err(peCertificatesFailure(cause, input, options?.signal)); + return err(peCertificatesFailure(cause, input)); } } } @@ -175,18 +175,11 @@ const peCertificatesFailurePrimary = ( const peCertificatesFailure = ( cause: unknown, input: InspectPeCertificatesInput, - signal: AbortSignal | undefined, ) => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? peCertificatesFailurePrimary( - cause.fileCleanupFailure.outcome.cause, - input, - ) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : peCertificatesFailurePrimary(cause, input); + const primary = peCertificatesFailurePrimary( + artifactFilePrimaryFailure(cause), + input, + ); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( diff --git a/src/native/pe/PeResourcesProvider.ts b/src/native/pe/PeResourcesProvider.ts index 668b2b2035..42866a9df6 100644 --- a/src/native/pe/PeResourcesProvider.ts +++ b/src/native/pe/PeResourcesProvider.ts @@ -2,7 +2,7 @@ import { fixedNativeAllowance } from "../../domain/native/nativeDeadlines.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, bindArtifactFileCleanup, artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; @@ -80,7 +80,7 @@ export class PeResourcesProvider implements PeResourcesPort { ); }); } catch (cause) { - return err(peResourcesFailure(cause, input, options?.signal)); + return err(peResourcesFailure(cause, input)); } } } @@ -191,20 +191,11 @@ const peResourcesFailurePrimary = ( ); }; -const peResourcesFailure = ( - cause: unknown, - input: InspectPeResourcesInput, - signal: AbortSignal | undefined, -) => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? peResourcesFailurePrimary(cause.fileCleanupFailure.outcome.cause, input) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError("inspect_pe_resources", { - cause: signal.reason, - }) - : peResourcesFailurePrimary(cause, input); +const peResourcesFailure = (cause: unknown, input: InspectPeResourcesInput) => { + const primary = peResourcesFailurePrimary( + artifactFilePrimaryFailure(cause), + input, + ); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index 9f0de38dc9..015f67eb39 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -1,6 +1,7 @@ import { readStableArtifact } from "../artifacts/readStableArtifact.js"; import { - ArtifactFileCleanupFailure, + artifactFilePrimaryFailure, + artifactFileCancellationCause, assertArtifactReadActive, bindArtifactFileCleanup, } from "../artifacts/ArtifactFileOperation.js"; @@ -126,7 +127,7 @@ export class RustBinaryProvider implements RustBinaryPort { return ok(parsed.data); }); } catch (cause: unknown) { - return err(failureFor(cause, input.path, options?.signal)); + return err(failureFor(cause, input.path)); } } } @@ -260,18 +261,8 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => { return artifactFailure(cause, path); }; -const failureFor = ( - cause: unknown, - path: string, - signal?: AbortSignal, -): AnalysisError => { - const primary = - cause instanceof ArtifactFileCleanupFailure && - cause.fileCleanupFailure.outcome.kind === "failed" - ? failureForPrimary(cause.fileCleanupFailure.outcome.cause, path) - : cause instanceof ArtifactFileCleanupFailure && signal?.aborted - ? new AnalysisCancelledError(OPERATION, { cause: signal.reason }) - : failureForPrimary(cause, path); +const failureFor = (cause: unknown, path: string): AnalysisError => { + const primary = failureForPrimary(artifactFilePrimaryFailure(cause), path); if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) return primary; const cleanup = new ProviderCleanupError( @@ -324,7 +315,9 @@ const artifactReaderFailure = ( cause, }); if (cause.reason === "cancelled") - return new AnalysisCancelledError(OPERATION, { cause: cause.cause }); + return new AnalysisCancelledError(OPERATION, { + cause: artifactFileCancellationCause(cause), + }); if (cause.reason === "limit") return new AnalysisResourceConstraintError( OPERATION, diff --git a/tests/process-global/stableArtifactConsumers.test.ts b/tests/process-global/stableArtifactConsumers.test.ts index 6c067ab964..0f7b471f7d 100644 --- a/tests/process-global/stableArtifactConsumers.test.ts +++ b/tests/process-global/stableArtifactConsumers.test.ts @@ -80,6 +80,14 @@ import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js"; import { GoBinaryProvider } from "../../src/go/GoBinaryProvider.js"; import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js"; import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js"; +import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js"; +import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js"; +import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js"; +import { BeamProvider } from "../../src/erlang/BeamProvider.js"; +import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js"; +import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js"; +import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js"; +import { inspectErlangBeamInputSchema } from "../../src/domain/erlang/beam.js"; import { ModelArtifactProvider } from "../../src/model/ModelArtifactProvider.js"; import { ggufFixture } from "../fixtures/model/model.js"; import { inspectPeResourcesInputSchema } from "../../src/domain/native/peResources.js"; @@ -661,3 +669,154 @@ it("preserves upstream resource-bound facts through an unconfirmed descriptor cl await scope.close(); } }); + +const fileFailureProviders = [ + ...staticProviders, + ["ELF notes", () => new ElfNotesProvider()], + ["PE certificates", () => new PeCertificatesProvider()], + ["native exports", () => new NativeExportsProvider()], + ["BEAM", () => new BeamProvider()], +] as const; + +const inspectFileFailure = ( + provider: ReturnType<(typeof fileFailureProviders)[number][1]>, + path: string, + signal?: AbortSignal, +) => { + const options = signal === undefined ? undefined : { signal }; + if (provider instanceof ElfNotesProvider) + return provider.inspect( + inspectElfNotesInputSchema.parse({ path }), + options, + ); + if (provider instanceof PeCertificatesProvider) + return provider.inspect( + inspectPeCertificatesInputSchema.parse({ path }), + options, + ); + if (provider instanceof NativeExportsProvider) + return provider.inspect( + inspectNativeExportsInputSchema.parse({ path }), + options, + ); + if (provider instanceof BeamProvider) + return provider.inspect( + inspectErlangBeamInputSchema.parse({ path }), + options, + ); + if (provider instanceof PeResourcesProvider) + return provider.inspect( + inspectPeResourcesInputSchema.parse({ path }), + options, + ); + return provider.inspect({ path }, options); +}; + +it.each(fileFailureProviders)( + "uses the original %s physical failure across nested descriptor owners", + async (_name, makeProvider) => { + const path = await selectedImage(); + const scope = new ArtifactResourceScope(); + const provider = makeProvider(); + const primary = new ArtifactReaderFailure( + "integrity", + "original source replacement", + ); + try { + const nested = await scope + .run(() => + withArtifactRegularFile(path, scope, undefined, async () => { + throw primary; + }), + ) + .catch((cause: unknown) => cause); + expect(nested).toBeInstanceOf(ArtifactFileCleanupFailure); + fault.readFailure = { cause: nested }; + const result = await inspectFileFailure(provider, path); + if (result.ok) throw new Error("Expected nested physical failure"); + expect(result.error.cleanupPrimary).toBeInstanceOf( + AnalysisArtifactChangedError, + ); + expect(result.error.cleanupPrimary?.cause).toBe(primary); + expect(result.error.cleanupIncomplete).toBe(true); + expect(fault.handles).toHaveLength(2); + fault.blocked.delete(fault.handles[1]!); + await provider.close(); + expect(fault.handles[1]?.fd).toBe(-1); + expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0); + expect(result.error.cleanupIncomplete).toBe(true); + fault.blocked.clear(); + await scope.close(); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + fault.blocked.clear(); + await provider.close(); + await scope.close(); + } + }, +); + +it.each( + fileFailureProviders.flatMap(([name, makeProvider]) => + ["recorded", "later"].map((timing) => ({ name, makeProvider, timing })), + ), +)( + "classifies $name finalization using the owner with $timing cancellation", + async ({ makeProvider, timing }) => { + const path = await selectedImage(); + const scope = new ArtifactResourceScope(); + const provider = makeProvider(); + const controller = new AbortController(); + const reason = new Error("file owner cancellation"); + if (timing === "recorded") fault.onClose = () => controller.abort(reason); + let run: ReturnType | undefined; + try { + const failure = await scope + .run(() => + withArtifactRegularFile( + path, + scope, + timing === "recorded" ? controller.signal : undefined, + async () => undefined, + ), + ) + .catch((cause: unknown) => cause); + expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure); + // Inject a real completed-close failure at the provider's acquisition + // boundary, after the owner has recorded its outcome and cancellation. + run = vi + .spyOn(ArtifactResourceScope.prototype, "run") + .mockImplementationOnce(async () => { + if (timing === "later") controller.abort(reason); + throw failure; + }); + const result = await inspectFileFailure( + provider, + path, + timing === "later" ? controller.signal : undefined, + ); + run.mockRestore(); + run = undefined; + if (result.ok) throw new Error("Expected retained close failure"); + expect(result.error.cleanupIncomplete).toBe(true); + if (timing === "recorded") { + expect(result.error.cleanupPrimary).toBeInstanceOf( + AnalysisCancelledError, + ); + expect(result.error.cleanupPrimary?.cause).toBe(reason); + } else + expect(result.error.cleanupPrimary).not.toBeInstanceOf( + AnalysisCancelledError, + ); + fault.blocked.clear(); + await scope.close(); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + run?.mockRestore(); + fault.onClose = undefined; + fault.blocked.clear(); + await provider.close(); + await scope.close(); + } + }, +);