mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
fix(entropy): retain completed file outcomes through cleanup
This commit is contained in:
1 parent
6d977db1b5
commit
5bda0a7dc3
2 files changed
+261
-130
No files matched your search
@@ -16,22 +16,26 @@ import {
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import {
|
||||
byteEntropySchema,
|
||||
computeBlockEntropy,
|
||||
entropyFromCounts,
|
||||
inspectByteEntropyInputSchema,
|
||||
mergeEntropyRuns,
|
||||
type ByteEntropy,
|
||||
} from "../../domain/native/byteEntropy.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
withArtifactFileHandle,
|
||||
bindArtifactFileCleanup,
|
||||
ArtifactFileCleanupFailure,
|
||||
ArtifactFileCancelled,
|
||||
assertArtifactReadActive,
|
||||
artifactFileCancellationCause,
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
/** Bound retained result rows before constructing the Evidence representation. */
|
||||
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
|
||||
@@ -92,7 +96,9 @@ export class ByteEntropyService {
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
return err(
|
||||
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
||||
);
|
||||
const parsed = inspectByteEntropyInputSchema.safeParse(rawInput);
|
||||
if (!parsed.success)
|
||||
return err(
|
||||
@@ -105,7 +111,62 @@ export class ByteEntropyService {
|
||||
return err(
|
||||
invalid("path", "Expected an absolute filesystem path on this host."),
|
||||
);
|
||||
const evidenceFromReport = (report: ByteEntropy): Evidence =>
|
||||
createEvidence(
|
||||
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
|
||||
BYTE_ENTROPY_PROVIDER,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: {
|
||||
path: input.path,
|
||||
block_size: input.block_size,
|
||||
offset: input.offset,
|
||||
...(input.length === undefined ? {} : { length: input.length }),
|
||||
},
|
||||
result: report,
|
||||
rawResult: null,
|
||||
confidence: "observed",
|
||||
limitations: report.limitations,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
},
|
||||
);
|
||||
// Cleanup errors erase the callback's generic type. Validate their completed
|
||||
// value before publishing it as this operation's observation.
|
||||
const completedObservation = (value: unknown): Evidence | undefined => {
|
||||
const report = byteEntropySchema.safeParse(value);
|
||||
return report.success && report.data.artifact.path === input.path
|
||||
? evidenceFromReport(report.data)
|
||||
: undefined;
|
||||
};
|
||||
let admitted = false;
|
||||
const readFailure = (cause: unknown): AnalysisError => {
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
if (cause instanceof Error && "code" in cause) {
|
||||
if (cause.code === "EACCES" || cause.code === "EPERM")
|
||||
return new AnalysisAccessDeniedError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
cause.code,
|
||||
{ cause },
|
||||
);
|
||||
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
||||
return admitted
|
||||
? new AnalysisArtifactChangedError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
"Selected file path changed during entropy inspection.",
|
||||
{ cause },
|
||||
)
|
||||
: invalid("path", "Selected file could not be found.");
|
||||
}
|
||||
return new AnalysisOutputError(
|
||||
OPERATION,
|
||||
cause instanceof ArtifactReaderFailure
|
||||
? cause.message
|
||||
: "Could not read complete byte entropy evidence.",
|
||||
{ cause },
|
||||
);
|
||||
};
|
||||
try {
|
||||
const before = await this.files.stat(input.path, { bigint: true });
|
||||
if (!before.isFile()) throw invalid("path", "Expected a regular file.");
|
||||
@@ -172,14 +233,14 @@ export class ByteEntropyService {
|
||||
const hash = createHash("sha256");
|
||||
let position = 0;
|
||||
while (position < size) {
|
||||
options?.signal?.throwIfAborted();
|
||||
assertArtifactReadActive(options?.signal);
|
||||
const selected = position >= input.offset && position < end;
|
||||
const boundary =
|
||||
position < input.offset ? input.offset : selected ? end : size;
|
||||
const wanted = Math.min(input.block_size, boundary - position);
|
||||
let filled = 0;
|
||||
while (filled < wanted) {
|
||||
options?.signal?.throwIfAborted();
|
||||
assertArtifactReadActive(options?.signal);
|
||||
const { bytesRead } = await file.read(
|
||||
buffer,
|
||||
filled,
|
||||
@@ -205,7 +266,7 @@ export class ByteEntropyService {
|
||||
!same(await this.files.stat(input.path, { bigint: true }))
|
||||
)
|
||||
throw changed();
|
||||
options?.signal?.throwIfAborted();
|
||||
assertArtifactReadActive(options?.signal);
|
||||
const thresholds = { high: 7.2, low: 1.0 };
|
||||
return {
|
||||
artifact: {
|
||||
@@ -228,70 +289,49 @@ export class ByteEntropyService {
|
||||
};
|
||||
},
|
||||
);
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
return ok(
|
||||
createEvidence(
|
||||
{ path: input.path, format: "file", sha256: report.artifact.sha256 },
|
||||
BYTE_ENTROPY_PROVIDER,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: {
|
||||
path: input.path,
|
||||
block_size: input.block_size,
|
||||
offset: input.offset,
|
||||
...(input.length === undefined ? {} : { length: input.length }),
|
||||
},
|
||||
result: report,
|
||||
rawResult: null,
|
||||
confidence: "observed",
|
||||
limitations: report.limitations,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
},
|
||||
),
|
||||
return completeProviderEvidence(
|
||||
evidenceFromReport(report),
|
||||
options?.signal,
|
||||
);
|
||||
} catch (cause) {
|
||||
if (cause instanceof ArtifactFileCleanupFailure) {
|
||||
const outcome = cause.fileCleanupFailure.outcome;
|
||||
const cleanup = new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Could not confirm closing the selected file.",
|
||||
{ cause, cleanup: cause.cleanup },
|
||||
{
|
||||
cause,
|
||||
cleanup: cause.cleanup,
|
||||
...(outcome.kind === "completed"
|
||||
? { partialObservation: completedObservation(outcome.value) }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
bindArtifactFileCleanup(cause, cleanup);
|
||||
const outcome = cause.fileCleanupFailure.outcome;
|
||||
if (
|
||||
outcome.kind === "failed" &&
|
||||
outcome.cause instanceof ArtifactReaderFailure &&
|
||||
outcome.cause.reason === "cancelled"
|
||||
) {
|
||||
const cancelled = new AnalysisCancelledError(OPERATION, {
|
||||
cause: outcome.cause.cancellation?.reason,
|
||||
cause: artifactFileCancellationCause(outcome.cause),
|
||||
cleanup: cause.cleanup,
|
||||
}).retainSecondaryFailure(cleanup);
|
||||
bindArtifactFileCleanup(cause, cancelled);
|
||||
return err(cancelled);
|
||||
}
|
||||
if (options?.signal?.aborted && outcome.kind === "completed") {
|
||||
const cancellation = cause.fileCleanupFailure.completionCancellation;
|
||||
if (cancellation !== undefined && outcome.kind === "completed") {
|
||||
const cancelled = new AnalysisCancelledError(OPERATION, {
|
||||
cause: options.signal.reason,
|
||||
cause: cancellation.reason,
|
||||
partialObservation: cleanup.partialObservation,
|
||||
cleanup: cause.cleanup,
|
||||
}).retainSecondaryFailure(cleanup);
|
||||
bindArtifactFileCleanup(cause, cancelled);
|
||||
return err(cancelled);
|
||||
}
|
||||
if (outcome.kind === "failed") {
|
||||
const primary =
|
||||
outcome.cause instanceof AnalysisError
|
||||
? outcome.cause
|
||||
: outcome.cause instanceof ArtifactReaderFailure
|
||||
? new AnalysisOutputError(OPERATION, outcome.cause.message, {
|
||||
cause: outcome.cause,
|
||||
})
|
||||
: new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Could not read complete byte entropy evidence.",
|
||||
{ cause: outcome.cause },
|
||||
);
|
||||
const primary = readFailure(outcome.cause);
|
||||
return err(
|
||||
analysisErrorWithCleanupFailure(
|
||||
primary,
|
||||
@@ -306,6 +346,14 @@ export class ByteEntropyService {
|
||||
if (cause.reason === "cancelled") {
|
||||
const cancelled = new AnalysisCancelledError(OPERATION, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
...(cause instanceof ArtifactFileCancelled &&
|
||||
cause.fileCancellation.outcome.kind === "completed"
|
||||
? {
|
||||
partialObservation: completedObservation(
|
||||
cause.fileCancellation.outcome.value,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
bindArtifactFileCleanup(cause, cancelled);
|
||||
return err(cancelled);
|
||||
@@ -321,35 +369,7 @@ export class ByteEntropyService {
|
||||
bindArtifactFileCleanup(cause, retry);
|
||||
return err(retry);
|
||||
}
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION, { cause }));
|
||||
if (cause instanceof AnalysisError) return err(cause);
|
||||
if (cause instanceof Error && "code" in cause) {
|
||||
if (cause.code === "EACCES" || cause.code === "EPERM")
|
||||
return err(
|
||||
new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
||||
return err(
|
||||
admitted
|
||||
? new AnalysisArtifactChangedError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
"Selected file path changed during entropy inspection.",
|
||||
{ cause },
|
||||
)
|
||||
: invalid("path", "Selected file could not be found."),
|
||||
);
|
||||
}
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Could not read complete byte entropy evidence.",
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
return err(readFailure(cause));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user