mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(cli): reuse firmware and web module input constraints
Derive extraction limits and web module fields from the shared contracts. Reject empty or NUL-containing local paths before CLI path resolution, preserve firmware relative-path handling, and apply the advertised extraction defaults. Validate advertised JSON Schemas with Ajv and check the registered parser defaults; prune redundant schema-shape and description assertions.
This commit is contained in:
1 parent
6fb3a345fe
commit
48c352b6bf
3 files changed
+102
-51
No files matched your search
+18
-36
@@ -4,6 +4,8 @@ import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysi
|
||||
import { createFirmwareAnalysisProvider } from "../composition/firmware.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { firmwareInputSchemas } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import type { Logger } from "pino";
|
||||
@@ -19,6 +21,8 @@ export const registerFirmwareCommands = (
|
||||
const service = new FirmwareAnalysisService(
|
||||
createFirmwareAnalysisProvider(environment),
|
||||
);
|
||||
const extractInput = firmwareInputSchemas.extract_firmware.shape;
|
||||
const range = extractInput.range.unwrap().shape;
|
||||
const execute = (
|
||||
name: string,
|
||||
operation: FirmwareOperation,
|
||||
@@ -34,7 +38,9 @@ export const registerFirmwareCommands = (
|
||||
}
|
||||
}),
|
||||
);
|
||||
const path = z.string().describe("Local firmware path; never executed");
|
||||
const path = localPathStringSchema.describe(
|
||||
"Local firmware path; never executed",
|
||||
);
|
||||
cli.command(CLI_COMMANDS.inspectFirmwareRegions, {
|
||||
description:
|
||||
"Inspect firmware signature candidates without extracting them",
|
||||
@@ -52,38 +58,20 @@ export const registerFirmwareCommands = (
|
||||
"Extract selected firmware bytes and return verified files and provenance",
|
||||
args: z.object({
|
||||
path,
|
||||
outputDirectory: z
|
||||
.string()
|
||||
.describe("Absolute absent output directory; parent must exist"),
|
||||
outputDirectory: localPathStringSchema.describe(
|
||||
"Absent output directory; parent must exist",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
offset: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
offset: range.offset
|
||||
.optional()
|
||||
.describe("Selected byte offset; requires length"),
|
||||
length: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
length: range.length
|
||||
.optional()
|
||||
.describe("Selected byte length; requires offset"),
|
||||
maxDepth: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe("Extraction depth, 1–10; default 3"),
|
||||
maxOutputBytes: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe("Staging byte budget; default 256 MiB, maximum 512 MiB"),
|
||||
maxOutputFiles: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.describe("Staging entry budget; default 10000"),
|
||||
maxDepth: extractInput.max_depth,
|
||||
maxOutputBytes: extractInput.max_output_bytes,
|
||||
maxOutputFiles: extractInput.max_output_files,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
|
||||
@@ -92,15 +80,9 @@ export const registerFirmwareCommands = (
|
||||
...(options.offset === undefined && options.length === undefined
|
||||
? {}
|
||||
: { range: { offset: options.offset, length: options.length } }),
|
||||
...(options.maxDepth === undefined
|
||||
? {}
|
||||
: { max_depth: options.maxDepth }),
|
||||
...(options.maxOutputBytes === undefined
|
||||
? {}
|
||||
: { max_output_bytes: options.maxOutputBytes }),
|
||||
...(options.maxOutputFiles === undefined
|
||||
? {}
|
||||
: { max_output_files: options.maxOutputFiles }),
|
||||
max_depth: options.maxDepth,
|
||||
max_output_bytes: options.maxOutputBytes,
|
||||
max_output_files: options.maxOutputFiles,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -3,10 +3,14 @@ import { createWebModuleTraceService } from "../composition/webModules.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { webModuleTraceInputSchema } from "../domain/webModuleTrace.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
|
||||
const importMapInputSchema =
|
||||
webModuleTraceInputSchema.shape.import_map.unwrap();
|
||||
|
||||
/** Expose the same selected-context module trace through the CLI. */
|
||||
export const registerWebModuleCommands = (
|
||||
cli: CliInstance,
|
||||
@@ -18,9 +22,7 @@ export const registerWebModuleCommands = (
|
||||
description:
|
||||
"Trace one captured script's native imports to URLs and captured source candidates",
|
||||
args: z.object({
|
||||
manifestPath: z
|
||||
.string()
|
||||
.describe("Absolute export_web_scripts manifest path"),
|
||||
manifestPath: webModuleTraceInputSchema.shape.manifest_path,
|
||||
scriptIndex: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
@@ -28,18 +30,15 @@ export const registerWebModuleCommands = (
|
||||
.describe("Zero-based manifest script index"),
|
||||
}),
|
||||
options: z.object({
|
||||
importerUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Explicit HTTP(S) importer URL"),
|
||||
importMapPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Absolute local import-map JSON"),
|
||||
importMapBaseUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("HTTP(S) import-map base URL"),
|
||||
importerUrl: webModuleTraceInputSchema.shape.importer_url,
|
||||
importMapPath: importMapInputSchema.shape.path
|
||||
.describe(
|
||||
"Absolute local import-map JSON path; requires --import-map-base-url",
|
||||
)
|
||||
.optional(),
|
||||
importMapBaseUrl: importMapInputSchema.shape.base_url
|
||||
.describe("HTTP(S) import-map base URL; requires --import-map-path")
|
||||
.optional(),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
|
||||
@@ -131,3 +131,73 @@ describe("CLI advertised input schemas", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware CLI advertised input schema", () => {
|
||||
it("keeps limits and defaults from its shared input contract", async () => {
|
||||
const cli = createCli({});
|
||||
const advertised = await advertisedSchema(cli, "extract-firmware");
|
||||
const ajv = new Ajv2020({ strict: false });
|
||||
const args = ajv.compile(advertised.args ?? {});
|
||||
const options = ajv.compile(advertised.options ?? {});
|
||||
|
||||
expect(args({ path: "firmware.bin", outputDirectory: "output" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(args({ path: "", outputDirectory: "output" })).toBe(false);
|
||||
expect(args({ path: "firmware.bin", outputDirectory: "" })).toBe(false);
|
||||
expect(options({})).toBe(true);
|
||||
expect(options({ maxDepth: 0 })).toBe(false);
|
||||
expect(options({ maxDepth: 11 })).toBe(false);
|
||||
expect(options({ maxOutputBytes: 0 })).toBe(false);
|
||||
expect(options({ maxOutputBytes: 512 * 1024 * 1024 + 1 })).toBe(false);
|
||||
expect(options({ maxOutputFiles: 100_001 })).toBe(false);
|
||||
expect(advertised.options?.properties?.["maxDepth"]).toMatchObject({
|
||||
default: 3,
|
||||
});
|
||||
expect(advertised.options?.properties?.["maxOutputBytes"]).toMatchObject({
|
||||
default: 256 * 1024 * 1024,
|
||||
});
|
||||
expect(advertised.options?.properties?.["maxOutputFiles"]).toMatchObject({
|
||||
default: 10_000,
|
||||
});
|
||||
|
||||
const maxOffset = Number.MAX_SAFE_INTEGER;
|
||||
expect(options({ offset: maxOffset + 1 })).toBe(false);
|
||||
expect(options({ length: maxOffset + 1 })).toBe(false);
|
||||
|
||||
expect(
|
||||
parserSchema(cli, "extract-firmware", "options")?.parse({}),
|
||||
).toMatchObject({
|
||||
maxDepth: 3,
|
||||
maxOutputBytes: 256 * 1024 * 1024,
|
||||
maxOutputFiles: 10_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("web module CLI advertised input schema", () => {
|
||||
it("advertises the shared path and URL constraints", async () => {
|
||||
const cli = createCli({});
|
||||
const advertised = await advertisedSchema(cli, "trace-web-module-imports");
|
||||
const ajv = new Ajv2020({ strict: false });
|
||||
const args = ajv.compile(advertised.args ?? {});
|
||||
const options = ajv.compile(advertised.options ?? {});
|
||||
|
||||
expect(
|
||||
args({ manifestPath: resolve("manifest.json"), scriptIndex: 0 }),
|
||||
).toBe(true);
|
||||
expect(args({ manifestPath: "", scriptIndex: 0 })).toBe(false);
|
||||
expect(args({ manifestPath: "manifest\u0000.json", scriptIndex: 0 })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(args({ manifestPath: "./manifest.json", scriptIndex: -1 })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(options({})).toBe(true);
|
||||
expect(options({ importerUrl: "" })).toBe(false);
|
||||
expect(options({ importMapPath: "" })).toBe(false);
|
||||
expect(options({ importMapPath: "map\u0000.json" })).toBe(false);
|
||||
expect(options({ importMapBaseUrl: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
import { resolve } from "node:path";
|
||||
Reference in new issue
Block a user