From f1212b9e4e5d01a26131c6001193d9858b8b6bbc Mon Sep 17 00:00:00 2001 From: RRFZ <247457381+Plastic-time@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:13:45 -0500 Subject: [PATCH] fix(cli): reject non-decimal native UI and command deadline options (#8920) --- src/cli/utilityCommands.ts | 35 ++--- .../cli/utilityIntegerOptions.test.ts | 120 ++++++++++++++++++ 2 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 tests/boundary/cli/utilityIntegerOptions.test.ts diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts index f565dbf9fc..5dd39f7079 100644 --- a/src/cli/utilityCommands.ts +++ b/src/cli/utilityCommands.ts @@ -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", () => diff --git a/tests/boundary/cli/utilityIntegerOptions.test.ts b/tests/boundary/cli/utilityIntegerOptions.test.ts new file mode 100644 index 0000000000..a0f4e79a6f --- /dev/null +++ b/tests/boundary/cli/utilityIntegerOptions.test.ts @@ -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 => { + const root = await createTestTempDirectory("rea-utility-options-"); + const path = join(root, "Info.plist"); + await writeFile( + path, + '\n\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", + ], + }), + ); + }); +});