Files
reverse-engineering-anything/src/domain/explicitSensitiveFailure.ts
T
Henglie 467f5572ac fix(native): report PE input byte budgets as file-size limits (#4290)
* fix(native): report PE input byte budgets as file-size limits

PeResourcesProvider translated every ArtifactReaderFailure with reason
"limit" into a memory resource constraint, so an image above the
caller's max_file_bytes was reported as a memory problem with memory
remediation advice. The bound that was exceeded is the input file's
byte length, which the caller raises with a flag.

Record the constrained bound on the failure the way ElectronFuseReader
already does, declare it at the reader's two input-byte refusals and at
the parser's metadata and entry budgets, and report that bound instead
of assuming memory. Readers that do not declare a boundary are
unchanged.

Fixes #4192

* fix(native): keep caller-selected PE budget refusals actionable

Address review findings on the budget classification:

- withCleanup now forwards the constrained bound, so a file that grows past
  its byte limit while its handle also fails to close is still reported as
  a file-size refusal rather than a memory one.
- Both caller-selected bounds carry their own remediation, pointing at
  max_file_bytes and max_entries instead of the file-size fallback's worker
  write-limit advice.
- AnalysisResourceConstraintError can declare that retrying cannot succeed.
  The entry budget is deterministic for the same file and limit, so it no
  longer inherits admission capacity's retryable behaviour.

The projection falls back to the previous resource-kind rule when a caller
does not declare retryability, so other capacity refusals are unchanged.

* fix(native): preserve budget retryability and name the schema ceiling

Address the remaining review findings on the budget classification:

- Failure redaction and the caller-copy path rebuilt resource constraint
  errors without forwarding retryable, so a deterministic capacity refusal
  projected as retryable again and automated callers could loop on it.
  Both constructors now forward the declared boolean.
- Both remediations recommended raising a bound the caller had already
  set to the schema maximum. The maxima are now named constants shared
  with the schema, and the remediation names the operation's hard ceiling
  when the caller is already at it.
2026-10-17 00:25:13 +08:00

187 lines
6.0 KiB
TypeScript

import type { AnalysisError } from "./analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisInputError,
AnalysisOutputError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
AnalysisCancelledError,
AnalysisTimeoutError,
} from "./analysisErrorCore.js";
import { redactExplicitText } from "./explicitSensitiveValues.js";
import type { JsonValue } from "./jsonValue.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import { ProviderCleanupError } from "./providerCleanupError.js";
/** Keep a real JSON-pointer ancestor when a property identity is explicitly excluded. */
export const redactExplicitPointer = (
pointer: string,
values: readonly string[],
): string => {
let parent = "";
for (const segment of pointer.split("/").slice(1)) {
const decoded = segment.replaceAll("~1", "/").replaceAll("~0", "~");
const child = `${parent}/${segment}`;
if (
values.some(
(literal) => decoded.includes(literal) || child.includes(literal),
)
)
return parent;
parent = child;
}
return parent;
};
/** Exclude explicit literals from common acquisition/decoder failures, preserving typed diagnostics. */
export const redactExplicitFailure = (
error: AnalysisError,
values: readonly string[],
): AnalysisError => {
if (values.length === 0) return error;
const text = (value: string): string => redactExplicitText(value, values);
const path = (
parts: readonly (string | number)[],
ordinaryNames: boolean,
): (string | number)[] => {
const retained: (string | number)[] = [];
for (const part of parts) {
if (typeof part !== "string") {
retained.push(part);
} else if (!ordinaryNames && part.startsWith("/")) {
const pointer = redactExplicitPointer(part, values);
retained.push(pointer);
if (pointer !== part) break;
} else {
if (values.some((literal) => part.includes(literal))) break;
retained.push(part);
}
}
return retained;
};
const diagnostics = (
value: Readonly<Record<string, JsonValue>>,
): Record<string, JsonValue> =>
Object.fromEntries(
Object.entries(value)
.filter(([key]) => !values.some((literal) => key.includes(literal)))
.map(([key, item]) => [key, visit(item)]),
);
const visit = (value: JsonValue): JsonValue => {
if (typeof value === "string") return text(value);
if (Array.isArray(value)) return value.map(visit);
if (typeof value === "object" && value !== null) return diagnostics(value);
return value;
};
const context = {
cause: error,
...(error.diagnostics === undefined
? {}
: { diagnostics: diagnostics(error.diagnostics) }),
...(error.capturedOutput === undefined
? {}
: {
capturedOutput: {
...error.capturedOutput,
stdout: text(error.capturedOutput.stdout),
stderr: text(error.capturedOutput.stderr),
},
}),
...(error.cleanup === undefined
? {}
: {
cleanup: {
reason: text(error.cleanup.reason),
resources: error.cleanup.resources.map(text),
},
}),
};
if (error instanceof AnalysisCancelledError)
return new AnalysisCancelledError(error.operation, {
...context,
partialObservation: error.partialObservation,
});
if (error instanceof AnalysisTimeoutError)
return new AnalysisTimeoutError(error.operation, error.timeoutMs, {
...context,
partialObservation: error.partialObservation,
});
if (error instanceof AnalysisAccessDeniedError)
return new AnalysisAccessDeniedError(
error.operation,
text(error.path),
error.systemCode,
context,
);
if (error instanceof AnalysisArtifactChangedError)
return new AnalysisArtifactChangedError(
error.operation,
text(error.path),
text(error.reason),
context,
);
if (error instanceof AnalysisInputError)
return new AnalysisInputError(
error.operation,
context,
error.issues.map((issue) => ({
...issue,
// Unknown-argument paths contain literal field names, including names
// beginning with '/'; they are not producer JSON-pointer coordinates.
path: path(issue.path, issue.reason === "unknown_argument"),
...(issue.message === undefined
? {}
: { message: text(issue.message) }),
...(issue.expected === undefined
? {}
: { expected: visit(issue.expected) }),
})),
);
if (error instanceof AnalysisOutputError)
return new AnalysisOutputError(
error.operation,
text(error.reason),
context,
);
if (error instanceof AnalysisCapabilityUnavailableError)
return new AnalysisCapabilityUnavailableError(
error.providerId,
error.operation,
text(error.reason),
{
...context,
...(error.userMessage === undefined
? {}
: { userMessage: text(error.userMessage) }),
...(error.retryable ? { retryable: true } : {}),
},
);
if (error instanceof AnalysisResourceConstraintError)
return new AnalysisResourceConstraintError(
error.operation,
error.resource,
text(error.reason),
error.reportedLimits === null ? null : diagnostics(error.reportedLimits),
{
...context,
...(error.remediationAction === undefined
? {}
: { remediationAction: text(error.remediationAction) }),
...(error.retryable === undefined
? {}
: { retryable: error.retryable }),
},
);
if (error instanceof ProviderCleanupError)
return new ProviderCleanupError(
error.providerId,
error.cleanupResources.map(text),
diagnostics(error.diagnostics ?? {}),
{ operation: error.operation, cause: error },
);
if (error instanceof ProviderAdapterError)
return new ProviderAdapterError(error.providerId, error.operation, context);
return error;
};