Files
rea/tests/boundary/cli/emptyLocalTargetAdmission.test.ts
RRFZ 6db024dc96 fix(cli): reject empty native and managed targets before resolution (#7956)
Local file commands in the core binary, native, managed and native-tool
CLI groups declared their positional target as a plain string. An empty
argument therefore reached path admission, where it resolved to the
working directory, as docs/cli.md says it never should.

Declare those targets with the shared nonempty, NUL-free local path
schema already used by analyze, so the CLI rejects them as a
VALIDATION_ERROR naming the field before any analysis runner is called.
Explicit "." is unchanged.

Fixes #7436
Fixes #7404
2026-10-29 08:31:12 +08:00

119 lines
3.8 KiB
TypeScript

import { Cli } from "incur";
import { expect, it, vi } from "vitest";
import { registerCoreBinaryCommands } from "../../../src/cli/coreBinaryCommands.js";
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
import { registerManagedCommands } from "../../../src/cli/managedCommands.js";
import { registerUtilityCommands } from "../../../src/cli/utilityCommands.js";
import { silentLogger } from "../../../src/logger.js";
const registry = () => {
const failure = {
ok: false as const,
error: { code: "invalid_request", message: "Admitted target control" },
};
const run = () =>
vi.fn(async (_path: string, ..._rest: unknown[]) => failure);
const runDirectAnalysis = run();
const runProviderAnalysis = run();
const cli = Cli.create("empty-target-admission", { sync: false });
registerCoreBinaryCommands(cli, silentLogger, runDirectAnalysis, {});
registerCoreNativeCommands(cli, silentLogger, runDirectAnalysis);
registerManagedCommands(cli, silentLogger, runProviderAnalysis);
registerUtilityCommands(
cli,
silentLogger,
{},
{
runProviderAnalysis,
runSessionStatus: async () => ({ ok: false, error: null }),
},
);
const serve = async (argv: readonly string[]): Promise<unknown> => {
let stdout = "";
await cli.serve([...argv, "--json"], {
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
});
return JSON.parse(stdout);
};
return { serve, runs: [runDirectAnalysis, runProviderAnalysis] };
};
// Each command with its other required arguments; the target comes first.
const commands: readonly (readonly string[])[] = [
["inspect"],
["decompile", "main"],
["xrefs", "0x1000"],
["trace", "feature"],
["function", "0x1000"],
["instructions", "0x1000"],
["search", "name"],
["search-bytes", "00"],
["inspect-native-load-image"],
["read-bytes", "0x1000"],
["address-to-file-offset", "0x1000"],
["trace-native-values", "main"],
["inspect-native-instruction", "0x1000"],
["resolve-native-call-targets", "0x1000"],
["inspect-native-dispatch-metadata"],
["trace-native-ui-action", "openDocument:"],
["inspect-native-api", "0x1000"],
["annotate-native-function", "main", "--name", "entry"],
["inspect-managed-artifact"],
["inspect-managed-members"],
["inspect-managed-native-boundaries"],
["observe-native-ui", "--pid", "1", "--window-id", "1"],
[
"capture-native-ui-scenario",
"--pid",
"1",
"--window-id",
"1",
"--steps",
'[{"kind":"wait","milliseconds":0}]',
],
["inspect-macho"],
["inspect-signature"],
["list-architectures"],
["inspect-plist"],
["demangle-swift", "$s4main"],
];
it.each(commands)(
"rejects an empty %s target before resolution and admits explicit .",
async (command, ...rest) => {
const { serve, runs } = registry();
// docs/cli.md: an empty string never selects the working directory.
expect(await serve([command, "", ...rest])).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: [
expect.objectContaining({ path: "path", code: "too_small" }),
],
});
for (const run of runs) expect(run).not.toHaveBeenCalled();
await serve([command, ".", ...rest]);
expect(runs.flatMap((run) => run.mock.calls.map(([path]) => path))).toEqual(
["."],
);
},
);
it.each([
{ argv: ["", "."], field: "leftPath" },
{ argv: [".", ""], field: "rightPath" },
])(
"rejects an empty compare-managed-members $field before reading either input",
async ({ argv, field }) => {
const { serve } = registry();
expect(await serve(["compare-managed-members", ...argv])).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: [
expect.objectContaining({ path: field, code: "too_small" }),
],
});
},
);