mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(javascript): report oversized recovery inputs as file-size limits (#4757)
scanRecoveryFile raised a plain AnalysisOutputError when a selected file exceeded the remaining operation-wide input allowance, and snapshotRecoveryInput folded that, read failures and UTF-8 decode failures into one invalid_value request issue. A 64 MiB + 1 input was therefore reported as a retryable invalid argument whose only record of the 67108864-byte budget was message text that also called it an invalid analysis output, and malformed UTF-8 received the same reason. The scanner now marks its byte-bound refusals with the bound it enforced. snapshotRecoveryInput, which owns the meaning of that bound as the remaining total input allowance, reports it as a non-retryable file-size AnalysisResourceConstraintError with maximum_input_bytes and remaining_input_bytes, and decodes UTF-8 separately as an invalid_format issue. Other scanner consumers still receive an AnalysisOutputError, and the limit stays enforced. Fixes #4629 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a407619fe
commit
31c87b40ee
4 files changed
+175
-8
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
@@ -23,6 +24,16 @@ const OPERATION = "recover_javascript_sources";
|
||||
export const recoveryDigest = (bytes: Uint8Array): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
/** A file exceeded the byte bound its caller selected; the caller owns what that bound means. */
|
||||
class RecoveryFileSizeError extends AnalysisOutputError {
|
||||
constructor(
|
||||
reason: string,
|
||||
readonly maximum: number,
|
||||
) {
|
||||
super(OPERATION, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/** Read a stable regular file without retaining bytes during engine fingerprinting. */
|
||||
const scanRecoveryFile = async (
|
||||
path: string,
|
||||
@@ -54,9 +65,9 @@ const scanRecoveryFile = async (
|
||||
try {
|
||||
const before = await handle.stat();
|
||||
if (before.size > maximum)
|
||||
throw new AnalysisOutputError(
|
||||
OPERATION,
|
||||
throw new RecoveryFileSizeError(
|
||||
`Expected a regular file no larger than ${String(maximum)} bytes: ${path}`,
|
||||
maximum,
|
||||
);
|
||||
let bytes = 0;
|
||||
for await (const raw of handle.createReadStream({ autoClose: false })) {
|
||||
@@ -64,9 +75,9 @@ const scanRecoveryFile = async (
|
||||
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
||||
bytes += chunk.length;
|
||||
if (bytes > maximum)
|
||||
throw new AnalysisOutputError(
|
||||
OPERATION,
|
||||
throw new RecoveryFileSizeError(
|
||||
`File exceeds ${String(maximum)} bytes: ${path}`,
|
||||
maximum,
|
||||
);
|
||||
hash.update(chunk);
|
||||
consume(chunk);
|
||||
@@ -138,15 +149,44 @@ const snapshotRecoveryInput = async (
|
||||
let bytes: Buffer;
|
||||
try {
|
||||
bytes = await readRecoveryFile(path, maximum, signal);
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof AnalysisCancelledError) throw cause;
|
||||
// The operation-wide input allowance is a deterministic byte budget, not
|
||||
// a malformed argument; the same selection fails again unchanged.
|
||||
if (cause instanceof RecoveryFileSizeError)
|
||||
throw new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"file-size",
|
||||
`Selected JavaScript input ${path} exceeds the ${String(cause.maximum)} bytes remaining of the ${String(RECOVERY_LIMITS.inputBytes)}-byte total input allowance`,
|
||||
{
|
||||
maximum_input_bytes: RECOVERY_LIMITS.inputBytes,
|
||||
remaining_input_bytes: cause.maximum,
|
||||
},
|
||||
{
|
||||
cause,
|
||||
retryable: false,
|
||||
remediationAction:
|
||||
"Select JavaScript inputs whose combined size is within maximum_input_bytes. Do not truncate required source; retrying the unchanged selection will fail again.",
|
||||
},
|
||||
);
|
||||
throw recoveryInputFailure(
|
||||
"path",
|
||||
`Cannot snapshot UTF-8 JavaScript input ${path}: ${recoveryFailureMessage(cause)}`,
|
||||
cause,
|
||||
);
|
||||
}
|
||||
try {
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
||||
} catch (cause: unknown) {
|
||||
throw new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
expected: "UTF-8",
|
||||
message: `JavaScript input is not valid UTF-8: ${path}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
await mkdir(dirname(snapshotPath), { recursive: true, mode: 0o700 });
|
||||
await writeFile(snapshotPath, bytes, { flag: "wx", mode: 0o600 });
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user