fix(cli): reject non-decimal native UI and command deadline options (#8920)

This commit is contained in:
RRFZ authored and GitHub committed 2026-11-06 10:13:45 +08:00
1 parent 3eff8065e7
commit f1212b9e4e
2 files changed
+140 -15

No files matched your search

+20 -15
View File
@@ -43,13 +43,22 @@ export const registerUtilityCommands = (
registerReferenceSourceCommand(cli, logger, environment);
};
/**
* A native deadline option keeps the shared field's bounds but admits only
* decimal argv text; Incur's Number() coercion would accept "0x10" or "1e3".
*/
const cliNativeDeadline = (field: typeof nativeUiDeadlineField) =>
cliIntegerArgument(1)
.pipe(field.unwrap())
.optional()
.describe(field.description ?? "");
// Decimal tokens only, for the same reason as cliNativeDeadline.
const nativeUiScopeOptions = {
pid: z.number().int().positive().describe("Existing application PID"),
windowId: z
.number()
.int()
.positive()
.describe("Exact selected application window ID"),
pid: cliIntegerArgument(1).describe("Existing application PID"),
windowId: cliIntegerArgument(1).describe(
"Exact selected application window ID",
),
screenshot: z
.boolean()
.default(true)
@@ -58,14 +67,10 @@ const nativeUiScopeOptions = {
.boolean()
.default(true)
.describe("Read the selected window accessibility tree"),
maxNodes: z
.number()
.int()
.positive()
.safe()
maxNodes: cliIntegerArgument(1)
.default(500)
.describe("Maximum accessibility nodes per capture"),
nativeUiDeadlineMs: nativeUiDeadlineField,
nativeUiDeadlineMs: cliNativeDeadline(nativeUiDeadlineField),
};
const nativeUiScope = (options: {
@@ -203,7 +208,7 @@ const registerNativeCommands = (
path: localPathStringSchema.describe("Mach-O or app path"),
}),
options: z.object({
nativeCommandTimeoutMs: nativeCommandTimeoutField,
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, command, () =>
@@ -230,7 +235,7 @@ const registerNativeCommands = (
.describe(
"Plist path relative to the app root, or an absolute path (default: the bundle's Info.plist)",
),
nativeCommandTimeoutMs: nativeCommandTimeoutField,
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inspect-plist", () =>
@@ -258,7 +263,7 @@ const registerNativeCommands = (
symbols: swiftSymbolsSchema.describe("Swift mangled symbols to demangle"),
}),
options: z.object({
nativeCommandTimeoutMs: nativeCommandTimeoutField,
nativeCommandTimeoutMs: cliNativeDeadline(nativeCommandTimeoutField),
}),
run: ({ args, options }) =>
logCliCommand(logger, "demangle-swift", () =>
@@ -0,0 +1,120 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type CliResult } from "../../support/cli/cliFixture.js";
// Numeric coercion reads each of these as 16 (or 10 for "1e1").
const NON_DECIMAL_TEXT = ["0x10", "0b10000", "1e1", "+16", "16.0", " 16"];
const nativeUi = (...flags: string[]): string[] => [
"observe-native-ui",
"Demo.app",
"--pid=16",
"--window-id=7",
...flags,
];
/** A present plist, so only option parsing can refuse at validation. */
const plist = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-utility-options-");
const path = join(root, "Info.plist");
await writeFile(
path,
'<?xml version="1.0" encoding="UTF-8"?>\n<plist version="1.0"><dict/></plist>\n',
);
return path;
};
const expectFieldRefusal = (result: CliResult, field: string): void => {
expect(result.exitCode).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
code: "VALIDATION_ERROR",
fieldErrors: expect.arrayContaining([
expect.objectContaining({ path: field }),
]),
});
};
const expectPastValidation = (result: CliResult): void => {
expect(JSON.parse(result.stdout)).not.toMatchObject({
code: "VALIDATION_ERROR",
});
};
describe("utility command integer options", () => {
// The options share one parser, so one takes every malformed spelling.
for (const [flag, field, values] of [
["--pid", "pid", NON_DECIMAL_TEXT],
["--window-id", "windowId", ["0x10"]],
["--max-nodes", "maxNodes", ["0x10"]],
["--native-ui-deadline-ms", "nativeUiDeadlineMs", ["0x10"]],
] as const) {
cliTest.for(values)(
`rejects %j as a native UI ${flag} value`,
async (value, { cli }) => {
// A later flag replaces the decimal --pid/--window-id defaults.
const result = await cli.run({
arguments: [...nativeUi(`${flag}=${value}`), "--json"],
});
expectFieldRefusal(result, field);
},
);
}
cliTest('rejects "0x10" as a native command timeout', async ({ cli }) => {
const result = await cli.run({
arguments: [
"inspect-plist",
await plist(),
"--native-command-timeout-ms=0x10",
"--json",
],
});
expectFieldRefusal(result, "nativeCommandTimeoutMs");
});
cliTest(
"keeps the native deadline bounds for decimal text",
async ({ cli }) => {
for (const value of ["0", "2147483648"]) {
const result = await cli.run({
arguments: [
...nativeUi(`--native-ui-deadline-ms=${value}`),
"--json",
],
});
expectFieldRefusal(result, "nativeUiDeadlineMs");
}
expectFieldRefusal(
await cli.run({ arguments: [...nativeUi("--pid=0"), "--json"] }),
"pid",
);
},
);
cliTest("admits decimal utility integer options", async ({ cli }) => {
expectPastValidation(
await cli.run({
arguments: [
...nativeUi("--max-nodes=500", "--native-ui-deadline-ms=2147483647"),
"--json",
],
}),
);
expectPastValidation(
await cli.run({
arguments: [
"inspect-plist",
await plist(),
"--native-command-timeout-ms=1000",
"--json",
],
}),
);
});
});