From 4e76d6e4277be5b04ffa3fadf854df5f097f17aa Mon Sep 17 00:00:00 2001 From: samzong Date: Wed, 13 May 2026 16:47:50 +0800 Subject: [PATCH] fix(ui): harden i18n report filters Signed-off-by: samzong --- scripts/control-ui-i18n-report.ts | 29 +++++++++++++---- src/scripts/control-ui-i18n-report.test.ts | 36 +++++++++++++++++++++- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/scripts/control-ui-i18n-report.ts b/scripts/control-ui-i18n-report.ts index 305a21cfcba..ceff0a5a001 100644 --- a/scripts/control-ui-i18n-report.ts +++ b/scripts/control-ui-i18n-report.ts @@ -30,6 +30,7 @@ const LOCALE_LABELS: Record = { "zh-CN": "Simplified Chinese", "zh-TW": "Traditional Chinese", }; +const REPORT_LOCALES = new Set(Object.keys(LOCALE_LABELS)); const PATH_LABELS: Record = { "ui/src/ui/chat/chat-queue.ts": "Chat queue", "ui/src/ui/chat/grouped-render.ts": "Chat message groups", @@ -108,13 +109,16 @@ export function parseArgs(argv: string[]): ReportArgs { continue; } if (arg === "--locale") { - args.locale = readOptionValue(argv, (index += 1), arg); + args.locale = parseLocale(readOptionValue(argv, (index += 1), arg)); continue; } if (arg === "--top") { const raw = readOptionValue(argv, (index += 1), arg); + if (!/^[1-9][0-9]*$/.test(raw)) { + throw new Error(`--top must be a positive integer: ${raw}`); + } const top = Number.parseInt(raw, 10); - if (!Number.isSafeInteger(top) || top < 1) { + if (!Number.isSafeInteger(top)) { throw new Error(`--top must be a positive integer: ${raw}`); } args.top = top; @@ -133,6 +137,13 @@ function readOptionValue(argv: string[], index: number, flag: string) { return value; } +function parseLocale(locale: string) { + if (!REPORT_LOCALES.has(locale)) { + throw new Error(`unknown locale: ${locale}`); + } + return locale; +} + export function filterRawCopyEntries(entries: RawCopyBaselineEntry[], surface?: string) { if (!surface) { return entries; @@ -167,9 +178,13 @@ function compareCountThenName(nameOf: (value: T) => string) { } function pathTokens(repoPath: string) { - return repoPath - .split("/") - .flatMap((part) => part.replace(/\.[^.]+$/, "").split(/[^A-Za-z0-9]+/)) + return repoPath.split("/").flatMap((part) => surfaceTokens(part.replace(/\.[^.]+$/, ""))); +} + +function surfaceTokens(value: string) { + return value + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .split(/[^A-Za-z0-9]+/) .filter(Boolean) .map(normalizeToken); } @@ -206,7 +221,9 @@ export function filterTranslationKeysBySurface(keys: string[], surface?: string) return keys; } const normalized = normalizeToken(surface); - return keys.filter((key) => key.split(".").some((part) => normalizeToken(part) === normalized)); + return keys.filter((key) => + key.split(".").some((part) => surfaceTokens(part).some((token) => token === normalized)), + ); } export function formatReport(input: ReportInput) { diff --git a/src/scripts/control-ui-i18n-report.test.ts b/src/scripts/control-ui-i18n-report.test.ts index d16866933f8..90924db7805 100644 --- a/src/scripts/control-ui-i18n-report.test.ts +++ b/src/scripts/control-ui-i18n-report.test.ts @@ -5,6 +5,7 @@ import { filterTranslationKeysBySurface, flattenTranslations, formatReport, + parseArgs, summarizeRawCopy, type RawCopyBaselineEntry, } from "../../scripts/control-ui-i18n-report.ts"; @@ -35,6 +36,23 @@ const entries: RawCopyBaselineEntry[] = [ ]; describe("control-ui-i18n report helpers", () => { + it("rejects invalid numeric limits", () => { + expect(() => parseArgs(["--top", "3abc"])).toThrow("--top must be a positive integer"); + expect(() => parseArgs(["--top", "1.5"])).toThrow("--top must be a positive integer"); + expect(() => parseArgs(["--top", "0"])).toThrow("--top must be a positive integer"); + expect(() => parseArgs(["--top", "999999999999999999999999999"])).toThrow( + "--top must be a positive integer", + ); + }); + + it("rejects locale path traversal before filesystem access", () => { + expect(parseArgs(["--locale", "zh-CN"])).toMatchObject({ locale: "zh-CN" }); + expect(() => parseArgs(["--locale", "../zh-CN"])).toThrow("unknown locale"); + expect(() => parseArgs(["--locale", "../../../../scripts/control-ui-i18n-report"])).toThrow( + "unknown locale", + ); + }); + it("filters raw-copy entries by path surface token", () => { expect(filterRawCopyEntries(entries, "agents")).toEqual([entries[1]]); expect(filterRawCopyEntries(entries, "config")).toEqual([entries[2]]); @@ -70,10 +88,26 @@ describe("control-ui-i18n report helpers", () => { it("filters same-as-English keys by translation key surface token", () => { expect( filterTranslationKeysBySurface( - ["agents.tabs.cronJobs", "chat.composer.send", "usage.common.emptyValue"], + [ + "agents.tabs.cronJobs", + "chat.composer.send", + "sessionsView.thinking", + "usage.common.emptyValue", + ], "chat", ), ).toEqual(["chat.composer.send"]); + expect( + filterTranslationKeysBySurface( + [ + "agents.tabs.cronJobs", + "chat.composer.send", + "sessionsView.thinking", + "usage.common.emptyValue", + ], + "sessions", + ), + ).toEqual(["sessionsView.thinking"]); }); it("formats pasteable report text", () => {