mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(go): retain completed artifact facts through finalization
This commit is contained in:
@@ -5,6 +5,12 @@ modules, replacements and build settings are embedded in one selected local
|
||||
executable or library. It reads the file without running it and requires no
|
||||
Go installation, active binary target, Hopper, Ghidra or IDA.
|
||||
|
||||
If a stable read completes but descriptor finalization fails or receives
|
||||
cancellation, the error retains the selected path, SHA-256 and byte length as
|
||||
a `file-snapshot-hash` partial observation. It does not claim that Go metadata
|
||||
was decoded. Failed cleanup stays incomplete until its exact owner confirms
|
||||
release; cancellation keeps the caller's reason.
|
||||
|
||||
```bash
|
||||
rea inspect-go-binary /absolute/path/to/application --json
|
||||
```
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { isSelectedRegularFileError } from "../filesystem/RegularFile.js";
|
||||
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
readStableArtifact,
|
||||
type StableArtifactSnapshot,
|
||||
} from "../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFailure,
|
||||
artifactFileCompletion,
|
||||
assertArtifactReadActive,
|
||||
bindArtifactFileCleanup,
|
||||
artifactFileCancellationCause,
|
||||
@@ -38,6 +42,7 @@ import {
|
||||
BinaryResourceFailure,
|
||||
} from "../native/metadata/BinaryContainer.js";
|
||||
import { parseGoModuleBytes, parseGoModuleText } from "./GoModuleText.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../domain/fileSnapshotObservation.js";
|
||||
|
||||
/** Bundled format reader identity; embedded compiler versions are separate observed facts. */
|
||||
export const GO_BINARY_PROVIDER_IDENTITY = {
|
||||
@@ -131,6 +136,20 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
|
||||
|
||||
const failureFor = (cause: unknown, path: string): AnalysisError => {
|
||||
const primary = failureForPrimary(artifactFilePrimaryFailure(cause), path);
|
||||
const completed = artifactFileCompletion(cause);
|
||||
if (completed !== undefined) {
|
||||
// This inspect boundary acquires exactly one StableArtifactSnapshot before
|
||||
// metadata decoding. Completion describes that read, not a decoded report.
|
||||
const snapshot = completed.value as StableArtifactSnapshot;
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { GoBinaryProvider } from "../../../src/go/GoBinaryProvider.js";
|
||||
import { GoBinaryService } from "../../../src/application/go/GoBinaryService.js";
|
||||
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { createGoBinaryFixture } from "../../fixtures/go/image.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.each(["control", "close-failure", "close-cancel", "close-both"] as const)(
|
||||
"retains completed Go acquisition through %s",
|
||||
async (mode) => {
|
||||
const root = await createTestTempDirectory("rea-go-finalization-");
|
||||
const path = join(root, "selected.elf");
|
||||
const bytes = createGoBinaryFixture().bytes;
|
||||
await writeFile(path, bytes);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "cancel during Go close" };
|
||||
const closeFailure = Object.assign(new Error("controlled Go 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-cancel" || mode === "close-both")
|
||||
controller.abort(reason);
|
||||
if (mode === "close-failure" || mode === "close-both")
|
||||
throw closeFailure;
|
||||
await original.call(this);
|
||||
});
|
||||
const service = new GoBinaryService(new GoBinaryProvider());
|
||||
try {
|
||||
const result = await service.inspect(
|
||||
{ path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (mode === "control") {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
build_info: { go_version: "go1.26.0" },
|
||||
});
|
||||
} else {
|
||||
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 service.close();
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
}
|
||||
expect(owner?.handle.fd).toBe(-1);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
await service.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user