diff --git a/src/cli/apktoolCommands.ts b/src/cli/apktoolCommands.ts index c7795f9d4..217889f5a 100644 --- a/src/cli/apktoolCommands.ts +++ b/src/cli/apktoolCommands.ts @@ -6,7 +6,10 @@ import { createApktoolResourceAnalysisProvider } from "../composition/apktool.js import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; -import type { ApktoolOperation } from "../domain/apktool/apktoolResourceAnalysis.js"; +import { + apktoolInputSchemas, + type ApktoolOperation, +} from "../domain/apktool/apktoolResourceAnalysis.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; @@ -49,22 +52,19 @@ export const registerApktoolCommands = ( args: z.object({ path: z.string().min(1).describe("Local APK file to decode"), }), + // The locale grammar and the string-projection default belong to the + // apktool input owner. Read them from that schema rather than restating a + // narrower locale pattern, which refused the owner's own advertised + // `b+zh+Hans+CN` form at the CLI boundary. options: z.object({ - locale: z - .string() - .min(2) - .max(16) - .regex( - /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/u, - "Locale must look like de or pt-BR", - ) - .optional() - .describe( - "Project strings from res/values- instead of the default", - ), + locale: apktoolInputSchemas.decode_android_resources.shape.locale, strings: z .boolean() - .default(true) + .default( + apktoolInputSchemas.decode_android_resources.shape.include_strings.parse( + undefined, + ), + ) .describe( "Include string resources; use --no-strings to skip projecting them", ), diff --git a/tests/boundary/cli/apktoolLocaleOption.test.ts b/tests/boundary/cli/apktoolLocaleOption.test.ts new file mode 100644 index 000000000..4bc596695 --- /dev/null +++ b/tests/boundary/cli/apktoolLocaleOption.test.ts @@ -0,0 +1,92 @@ +import { expect, it } from "vitest"; +import { Cli } from "incur"; +import pino from "pino"; + +import type { ApktoolResourceAnalysisPort } from "../../../src/application/apktool/ApktoolResourceAnalysisPort.js"; +import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; +import { registerApktoolCommands } from "../../../src/cli/apktoolCommands.js"; +import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js"; +import type { ApktoolRequest } from "../../../src/domain/apktool/apktoolResourceAnalysis.js"; +import { ok } from "../../../src/domain/result.js"; + +const invoke = async (argv: readonly string[]) => { + const requests: ApktoolRequest[] = []; + const provider: ApktoolResourceAnalysisPort = { + async inspectAvailability() { + throw new Error("unexpected provider probe"); + }, + async close() {}, + async execute(request) { + requests.push(request); + return ok( + createAnalysisExecution( + { strings: [] }, + { id: "apktool", name: "Apktool", version: null }, + ), + ); + }, + }; + const cli = Cli.create("rea", { sync: false }); + registerApktoolCommands(cli, pino({ level: "silent" }), {}, provider); + presentOmittableDefaults(cli); + + let stdout = ""; + let exitCode = 0; + await cli.serve([...argv], { + env: {}, + exit: (code) => { + exitCode = code; + }, + stdout: (text) => { + stdout += text; + }, + }); + return { exitCode, stdout, requests }; +}; + +/** + * Locales the apktool input owner admits. The owner's own rejection message + * advertises `b+zh+Hans+CN`, so the CLI cannot be narrower than this list + * without refusing a caller-selected locale the decoding lane supports. + */ +const OWNER_ADMITTED_LOCALES = [ + "de", + "pt-BR", + "pt-rBR", + "zh-rCN", + "en-419", + "b+zh+Hans+CN", +] as const; + +it("forwards every locale the apktool input owner admits", async () => { + for (const locale of OWNER_ADMITTED_LOCALES) { + const result = await invoke([ + "decode-android-resources", + "/targets/app.apk", + "--locale", + locale, + "--json", + ]); + + expect(result.exitCode, `exit code for locale ${locale}`).toBe(0); + expect(result.requests, `request for locale ${locale}`).toEqual([ + { + operation: "decode_android_resources", + input: { path: "/targets/app.apk", include_strings: true, locale }, + }, + ]); + } +}); + +it("still refuses a locale the apktool input owner refuses", async () => { + const result = await invoke([ + "decode-android-resources", + "/targets/app.apk", + "--locale", + "not a locale", + "--json", + ]); + + expect(result.exitCode).not.toBe(0); + expect(result.requests).toEqual([]); +});