mirror of
https://github.com/morluto/rea
synced 2026-10-25 14:01:13 -04:00
fix(artifacts): retain completed acquisition facts through finalization
Forward the file owner's completed stable-read hash and byte count through descriptor-close failure or cancellation in PE resources, certificates and imports, ELF notes and kernel modules, native exports, Apple IPS, and APK signature inspection. Preserve the primary failure, exact cancellation reason, and failed-close cleanup lease. Validation: 32 acquisition-finalization regressions across eight providers and 30 existing APK provider, lifecycle, and identity tests passed. Existing native filesystem regressions and typecheck/lint also passed. No external analysis engines or Android device were used.
This commit is contained in:
@@ -239,13 +239,19 @@ serialized size of the report together with its limitations (which repeat
|
||||
archive names) is then checked against an 8 MiB output budget (the exact check
|
||||
is authoritative); an over-budget report is refused, not truncated.
|
||||
|
||||
The whole-file reader retains read chunks and a concatenated buffer; total
|
||||
process memory exceeds the selected file's size. It also computes the complete
|
||||
The whole-file reader allocates the selected snapshot and bounded read scratch;
|
||||
total process memory exceeds the selected file's size. It also computes the complete
|
||||
file SHA-256, so inspection is not header-only. Content digests are recomputed
|
||||
at most once per SHA-256/SHA-512 algorithm during one inspection. Input,
|
||||
inflation, decoded-record and serialized-output bounds keep work finite;
|
||||
resource refusals are distinct from malformed format errors.
|
||||
|
||||
If the stable read completes but closing its descriptor fails or is cancelled,
|
||||
the error retains `details.partial_observation` with the selected path, complete
|
||||
file SHA-256 and byte count. These facts describe the acquired file; they do not
|
||||
establish any decoded signing facts. Failed cleanup remains visible until the
|
||||
provider releases the descriptor on a cleanup retry.
|
||||
|
||||
A file that is not a ZIP is refused as an unsupported target. A damaged ZIP
|
||||
(local headers but no end-of-central-directory), a central directory that
|
||||
cannot be walked or whose declared record count does not account for all of
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { completedStableArtifact } from "../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
bindArtifactFileCleanup,
|
||||
} from "../artifacts/ArtifactFileOperation.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../domain/fileSnapshotObservation.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
@@ -128,6 +130,16 @@ export const apkSignatureFailure = (
|
||||
path: string,
|
||||
): AnalysisError => {
|
||||
const primary = primaryFailure(artifactFilePrimaryFacts(cause), path);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -10,6 +13,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { AppleIpsPort } from "../../application/binaryDiagnostics/AppleIpsPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -278,6 +282,16 @@ const appleIpsFailure = (
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { ElfNotesPort } from "../../application/binaryDiagnostics/ElfNotesPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
@@ -240,6 +244,16 @@ const elfNotesFailure = (cause: unknown, input: InspectElfNotesInput) => {
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { KernelModulePort } from "../../application/binaryDiagnostics/KernelModulePort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
@@ -235,6 +239,16 @@ const kernelModuleFailure = (
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -10,6 +13,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { NativeExportsPort } from "../../application/binaryDiagnostics/NativeExportsPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -202,6 +206,16 @@ const nativeExportsFailure = (
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { PeCertificatesPort } from "../../application/binaryDiagnostics/PeCertificatesPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
@@ -192,6 +196,16 @@ const peCertificatesFailure = (
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -10,6 +13,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { PeImportsPort } from "../../application/binaryDiagnostics/PeImportsPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -245,6 +249,16 @@ const peImportsFailure = (
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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";
|
||||
import {
|
||||
readStableArtifact,
|
||||
completedStableArtifact,
|
||||
} from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -10,6 +13,7 @@ import {
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import type { PeResourcesPort } from "../../application/binaryDiagnostics/PeResourcesPort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
@@ -212,6 +216,16 @@ const peResourcesFailure = (cause: unknown, input: InspectPeResourcesInput) => {
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
const snapshot = completedStableArtifact(cause);
|
||||
if (snapshot !== undefined)
|
||||
primary.retainPartialObservation(
|
||||
fileSnapshotPartialObservationSchema.parse({
|
||||
kind: "file-snapshot-hash",
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
}),
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { inspectPeResourcesInputSchema } from "../../../src/domain/native/peResources.js";
|
||||
import { inspectPeCertificatesInputSchema } from "../../../src/domain/native/peCertificates.js";
|
||||
import { inspectPeImportsInputSchema } from "../../../src/domain/native/peImports.js";
|
||||
import { inspectElfNotesInputSchema } from "../../../src/domain/native/elfNotes.js";
|
||||
import { inspectKernelModuleInputSchema } from "../../../src/domain/native/kernelModule.js";
|
||||
import { inspectNativeExportsInputSchema } from "../../../src/domain/native/nativeExports.js";
|
||||
import { inspectAppleIpsInputSchema } from "../../../src/domain/native/appleIps.js";
|
||||
import { inspectApkSignatureInputSchema } from "../../../src/domain/apkSignature/apkSignatureQuery.js";
|
||||
import { PeResourcesProvider } from "../../../src/native/pe/PeResourcesProvider.js";
|
||||
import { PeCertificatesProvider } from "../../../src/native/pe/PeCertificatesProvider.js";
|
||||
import { PeImportsProvider } from "../../../src/native/pe/PeImportsProvider.js";
|
||||
import { ElfNotesProvider } from "../../../src/native/elf/ElfNotesProvider.js";
|
||||
import { KernelModuleProvider } from "../../../src/native/elf/KernelModuleProvider.js";
|
||||
import { NativeExportsProvider } from "../../../src/native/exports/NativeExportsProvider.js";
|
||||
import { AppleIpsProvider } from "../../../src/native/apple/AppleIpsProvider.js";
|
||||
import { ApkSignatureProvider } from "../../../src/apkSignature/ApkSignatureProvider.js";
|
||||
import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: "PE resources",
|
||||
make: () => {
|
||||
const provider = new PeResourcesProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectPeResourcesInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PE certificates",
|
||||
make: () => {
|
||||
const provider = new PeCertificatesProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectPeCertificatesInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PE imports",
|
||||
make: () => {
|
||||
const provider = new PeImportsProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectPeImportsInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ELF notes",
|
||||
make: () => {
|
||||
const provider = new ElfNotesProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectElfNotesInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "kernel modules",
|
||||
make: () => {
|
||||
const provider = new KernelModuleProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectKernelModuleInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "native exports",
|
||||
make: () => {
|
||||
const provider = new NativeExportsProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectNativeExportsInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Apple IPS",
|
||||
make: () => {
|
||||
const provider = new AppleIpsProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectAppleIpsInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "APK signatures",
|
||||
make: () => {
|
||||
const provider = new ApkSignatureProvider();
|
||||
return {
|
||||
inspect: (path: string, signal: AbortSignal) =>
|
||||
provider.inspect(inspectApkSignatureInputSchema.parse({ path }), {
|
||||
signal,
|
||||
}),
|
||||
close: () => provider.close(),
|
||||
};
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
const modes = [
|
||||
"close-failure",
|
||||
"close-cancel",
|
||||
"close-cancel-null",
|
||||
"close-both",
|
||||
] as const;
|
||||
|
||||
it.each(
|
||||
cases.flatMap(({ name, make }) =>
|
||||
modes.map((mode) => ({ name, make, mode })),
|
||||
),
|
||||
)(
|
||||
"retains completed $name acquisition through $mode",
|
||||
async ({ make, mode }) => {
|
||||
const root = await createTestTempDirectory("rea-native-finalization-");
|
||||
const path = join(root, "selected.bin");
|
||||
// The descriptor must finalize before any format parser runs, so one
|
||||
// physical file can exercise every acquisition boundary.
|
||||
const bytes = peResourceFixture().bytes;
|
||||
await writeFile(path, bytes);
|
||||
const controller = new AbortController();
|
||||
const reason =
|
||||
mode === "close-cancel-null" ? null : { request: "cancel during close" };
|
||||
const closeFailure = Object.assign(new Error("controlled close EIO"), {
|
||||
code: "EIO",
|
||||
});
|
||||
const original = OwnedFileHandle.prototype.close;
|
||||
let owner: OwnedFileHandle | undefined;
|
||||
const close = vi
|
||||
.spyOn(OwnedFileHandle.prototype, "close")
|
||||
.mockImplementationOnce(async function (this: OwnedFileHandle) {
|
||||
owner = this;
|
||||
if (mode !== "close-failure") controller.abort(reason);
|
||||
if (mode === "close-failure" || mode === "close-both")
|
||||
throw closeFailure;
|
||||
await original.call(this);
|
||||
});
|
||||
const provider = make();
|
||||
try {
|
||||
const result = await provider.inspect(path, controller.signal);
|
||||
if (result.ok) throw new Error("Expected finalization failure");
|
||||
const acquired = {
|
||||
kind: "file-snapshot-hash",
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
};
|
||||
expect(result.error.partialObservation).toEqual(acquired);
|
||||
expect(
|
||||
projectAnalysisError(result.error).details?.partial_observation,
|
||||
).toEqual(acquired);
|
||||
const failedClose = mode === "close-failure" || mode === "close-both";
|
||||
expect(result.error.cleanupIncomplete).toBe(failedClose);
|
||||
if (failedClose) expect(owner?.handle.fd).toBeGreaterThanOrEqual(0);
|
||||
if (mode !== "close-failure") {
|
||||
const primary = result.error.cleanupPrimary ?? result.error;
|
||||
expect(primary).toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
cause: reason,
|
||||
});
|
||||
}
|
||||
close.mockRestore();
|
||||
await provider.close();
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(owner?.handle.fd).toBe(-1);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
await provider.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user