diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 086c297118..91f01f7243 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -9,6 +9,10 @@ const range = z.strictObject({ offset: bytes, length: bytes.positive() }); export const DEFAULT_FIRMWARE_ANALYSIS_TIMEOUT_MS = 120_000; /** Largest delay supported without overflowing Node timers. */ export const MAX_FIRMWARE_TIMEOUT_MS = 2_147_483_647; +/** Largest staging file-byte budget the schema accepts. */ +export const MAX_FIRMWARE_STAGING_BYTES = 512 * 1024 * 1024; +/** Largest staging entry budget the schema accepts. */ +export const MAX_FIRMWARE_STAGING_ENTRIES = 100_000; const timeout = z.number().int().min(1).max(MAX_FIRMWARE_TIMEOUT_MS); const target = { path: absoluteLocalPathStringSchema.describe( @@ -43,7 +47,7 @@ export const firmwareInputSchemas = { .number() .int() .min(1) - .max(512 * 1024 * 1024) + .max(MAX_FIRMWARE_STAGING_BYTES) .default(256 * 1024 * 1024) .describe( "Staging file-byte budget, checked during extraction; transient overshoot is possible", @@ -52,7 +56,7 @@ export const firmwareInputSchemas = { .number() .int() .min(1) - .max(100_000) + .max(MAX_FIRMWARE_STAGING_ENTRIES) .default(10_000) .describe("Staging entry budget, including directories and links"), publication_timeout_ms: timeout diff --git a/src/firmware/FirmwareFiles.ts b/src/firmware/FirmwareFiles.ts index 9bb5d915e8..27128e78d9 100644 --- a/src/firmware/FirmwareFiles.ts +++ b/src/firmware/FirmwareFiles.ts @@ -12,6 +12,10 @@ import { AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import type { FirmwareRequest } from "../domain/firmware/firmwareAnalysis.js"; +import { + MAX_FIRMWARE_STAGING_BYTES, + MAX_FIRMWARE_STAGING_ENTRIES, +} from "../domain/firmware/firmwareAnalysis.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js"; import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; @@ -442,6 +446,27 @@ export const inventoryFirmwareOutput = async ( ); }; +/** + * Recovery for one staging budget refusal. At the schema ceiling a larger + * selection is impossible, so that case must not recommend raising the value. + */ +export const firmwareStagingBudgetRemediation = ( + budget: "max_output_files" | "max_output_bytes", + selectedLimit: number, +): string => { + const maximum = + budget === "max_output_files" + ? MAX_FIRMWARE_STAGING_ENTRIES + : MAX_FIRMWARE_STAGING_BYTES; + const description = + budget === "max_output_files" + ? "staging entry budget, which counts extracted directories and links" + : "staging file-byte budget, checked during extraction"; + if (selectedLimit < maximum) + return `The ${description} was exceeded (${budget}=${String(selectedLimit)}; supported maximum ${String(maximum)}). Increase ${budget} up to ${String(maximum)} if the larger extraction is required, or select a materially narrower firmware byte range. analysis_timeout_ms does not raise this staging allowance.`; + return `The ${description} reached its supported maximum (${budget}=${String(selectedLimit)}; maximum ${String(maximum)}). Raising ${budget} further is not supported. Select a smaller firmware byte range or analyze a smaller image. analysis_timeout_ms does not raise this staging allowance.`; +}; + /** The same selected aggregate budget is enforced during and after extraction. */ const firmwareStagingLimit = ( operation: string, @@ -455,7 +480,7 @@ const firmwareStagingLimit = ( { [field]: maximum }, { retryable: false, - remediationAction: `Increase ${field} within its schema maximum if the larger extraction is required, or select a materially narrower range. analysis_timeout_ms does not raise this staging allowance. Repeating the unchanged request fails the same way.`, + remediationAction: firmwareStagingBudgetRemediation(field, maximum), }, ); diff --git a/src/firmware/FirmwareStagingBudget.test.ts b/src/firmware/FirmwareStagingBudget.test.ts new file mode 100644 index 0000000000..8a61dccfc0 --- /dev/null +++ b/src/firmware/FirmwareStagingBudget.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { + MAX_FIRMWARE_STAGING_BYTES, + MAX_FIRMWARE_STAGING_ENTRIES, +} from "../domain/firmware/firmwareAnalysis.js"; +import { firmwareStagingBudgetRemediation } from "./FirmwareFiles.js"; + +describe("firmware staging budget recovery", () => { + it("offers room to raise a budget that is below its supported maximum", () => { + const remediation = firmwareStagingBudgetRemediation("max_output_files", 1); + + expect(remediation).toContain( + `supported maximum ${String(MAX_FIRMWARE_STAGING_ENTRIES)}`, + ); + expect(remediation).toContain("Increase max_output_files up to"); + expect(remediation).not.toContain("not supported"); + }); + + it("does not recommend raising a budget already at its supported maximum", () => { + const entries = firmwareStagingBudgetRemediation( + "max_output_files", + MAX_FIRMWARE_STAGING_ENTRIES, + ); + const bytes = firmwareStagingBudgetRemediation( + "max_output_bytes", + MAX_FIRMWARE_STAGING_BYTES, + ); + for (const remediation of [entries, bytes]) { + // Raising a value the schema already caps is not an available recovery. + expect(remediation).toContain("not supported"); + expect(remediation).not.toContain("Increase"); + expect(remediation).toContain("smaller firmware byte range"); + } + }); + + it("names the exhausted budget, the selected value and the ceiling", () => { + expect(firmwareStagingBudgetRemediation("max_output_bytes", 4)).toContain( + "max_output_bytes=4", + ); + expect( + firmwareStagingBudgetRemediation( + "max_output_bytes", + MAX_FIRMWARE_STAGING_BYTES, + ), + ).toContain(`maximum ${String(MAX_FIRMWARE_STAGING_BYTES)}`); + }); + + it("keeps the ceiling advice off the schema limit for both budgets", () => { + for (const [budget, maximum] of [ + ["max_output_files", MAX_FIRMWARE_STAGING_ENTRIES], + ["max_output_bytes", MAX_FIRMWARE_STAGING_BYTES], + ] as const) + expect(firmwareStagingBudgetRemediation(budget, maximum - 1)).toContain( + `Increase ${budget} up to`, + ); + }); +});