From 16e8a4a87860c98a567780a254448be5a19dc767 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 6 Jul 2026 15:02:43 -0400 Subject: [PATCH 1/3] fix(http-client-python): avoid shell-quote leak in package-pprint-name Shell-escaping quotes for package-pprint-name were baked into the option value in addDefaultOptions. When venv creation fails and the emitter falls back to Pyodide, that already-quoted value is passed as a literal kwarg, producing an invalid setup.py (PACKAGE_PPRINT_NAME = ""..."") that black cannot parse. Quoting is now applied only when building the native Python shell command via quoteShellArg. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...pprint-name-shell-quote-leak-2026-7-6-14-56-0.md | 7 +++++++ packages/http-client-python/emitter/src/emitter.ts | 12 ------------ .../http-client-python/emitter/src/node-runner.ts | 3 ++- packages/http-client-python/emitter/src/utils.ts | 13 +++++++++++++ .../http-client-python/emitter/test/utils.test.ts | 11 ++++++++++- 5 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 .chronus/changes/fix-pprint-name-shell-quote-leak-2026-7-6-14-56-0.md diff --git a/.chronus/changes/fix-pprint-name-shell-quote-leak-2026-7-6-14-56-0.md b/.chronus/changes/fix-pprint-name-shell-quote-leak-2026-7-6-14-56-0.md new file mode 100644 index 00000000000..d63c8a80df2 --- /dev/null +++ b/.chronus/changes/fix-pprint-name-shell-quote-leak-2026-7-6-14-56-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Fix Python SDK generation failure when `package-pprint-name` contains spaces. The shell-escaping quotes were baked into the option value and leaked into the Pyodide runtime, producing an invalid `setup.py` (e.g. `PACKAGE_PPRINT_NAME = ""Azure Web PubSub Chat Service""`) that `black` could not parse. Quoting is now applied only when building the native Python shell command. diff --git a/packages/http-client-python/emitter/src/emitter.ts b/packages/http-client-python/emitter/src/emitter.ts index ef58cecb204..982096c91b3 100644 --- a/packages/http-client-python/emitter/src/emitter.ts +++ b/packages/http-client-python/emitter/src/emitter.ts @@ -42,18 +42,6 @@ function addDefaultOptions(sdkContext: PythonSdkContext) { // Explicitly set unbranded flavor when not azure (options as any).flavor = "unbranded"; } - - if ( - options["package-pprint-name"] !== undefined && - !options["package-pprint-name"].startsWith('"') - ) { - // Only add quotes for shell compatibility when NOT using emit-yaml-only mode - // (emit-yaml-only passes options via JSON config files, not shell) - const needsShellQuoting = !options["use-pyodide"] && !options["emit-yaml-only"]; - options["package-pprint-name"] = needsShellQuoting - ? `"${options["package-pprint-name"]}"` - : `${options["package-pprint-name"]}`; - } } async function createPythonSdkContext( diff --git a/packages/http-client-python/emitter/src/node-runner.ts b/packages/http-client-python/emitter/src/node-runner.ts index 47af1553517..1d75b7c4c59 100644 --- a/packages/http-client-python/emitter/src/node-runner.ts +++ b/packages/http-client-python/emitter/src/node-runner.ts @@ -16,6 +16,7 @@ import { blackExcludeDirs, PYGEN_WHEEL_FILENAME } from "./constants.js"; import { saveCodeModelAsYaml } from "./external-process.js"; import { PythonEmitterOptions, reportDiagnostic } from "./lib.js"; import { runPython3 } from "./run-python3.js"; +import { quoteShellArg } from "./utils.js"; export interface RunNodeEmitArgs { context: EmitContext; @@ -106,7 +107,7 @@ export async function runNodeEmit({ commandArgs["output-folder"] = outputDir; commandArgs["tsp-file"] = yamlPath; const commandFlags = Object.entries(commandArgs) - .map(([key, value]) => `--${key}=${value}`) + .map(([key, value]) => `--${key}=${quoteShellArg(String(value))}`) .join(" "); const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`; execSync(command); diff --git a/packages/http-client-python/emitter/src/utils.ts b/packages/http-client-python/emitter/src/utils.ts index 31d819015ed..1d3a0f5a8db 100644 --- a/packages/http-client-python/emitter/src/utils.ts +++ b/packages/http-client-python/emitter/src/utils.ts @@ -268,6 +268,19 @@ export function capitalize(name: string): string { return name[0].toUpperCase() + name.slice(1); } +/** + * Quotes a value so it can be safely embedded in a shell command line. + * + * The value is wrapped in double quotes (handling spaces and other separators) + * and any embedded double quotes are escaped. This quoting must only be applied + * when the value is passed through a shell (e.g. `execSync`); it must never be + * baked into the option value itself, otherwise the quotes leak into non-shell + * consumers such as the Pyodide runtime and end up in generated files. + */ +export function quoteShellArg(value: string): string { + return `"${value.replace(/"/g, '\\"')}"`; +} + // Library namespaces that should not be used as client namespaces const LIB_NAMESPACE = [ "azure.core", diff --git a/packages/http-client-python/emitter/test/utils.test.ts b/packages/http-client-python/emitter/test/utils.test.ts index 733f5258a04..b8dbed4cf51 100644 --- a/packages/http-client-python/emitter/test/utils.test.ts +++ b/packages/http-client-python/emitter/test/utils.test.ts @@ -1,6 +1,6 @@ import { strictEqual } from "assert"; import { describe, it } from "vitest"; -import { camelToSnakeCase, md2Rst } from "../src/utils.js"; +import { camelToSnakeCase, md2Rst, quoteShellArg } from "../src/utils.js"; describe("typespec-python: utils", () => { it("camelToSnakeCase", async () => { @@ -27,4 +27,13 @@ describe("typespec-python: utils", () => { const des = "Format: .."; strictEqual(md2Rst(des), "Format: .."); }); + + it("quoteShellArg wraps values in double quotes without doubling existing content", async () => { + // Regression test for https://github.com/microsoft/typespec Python SDK generation failure + // where a package-pprint-name with spaces (e.g. "Azure Web PubSub Chat Service") got quotes + // baked into the option value and then leaked into the generated setup.py as doubled quotes. + strictEqual(quoteShellArg("Azure Web PubSub Chat Service"), '"Azure Web PubSub Chat Service"'); + strictEqual(quoteShellArg("simple"), '"simple"'); + strictEqual(quoteShellArg('has "quotes"'), '"has \\"quotes\\""'); + }); }); From 422d32ab95587bf8fd5db92d72ac36bfc7d807bf Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 6 Jul 2026 15:18:50 -0400 Subject: [PATCH 2/3] fix(http-client-python): escape backslashes in quoteShellArg Address CodeQL 'Incomplete string escaping' by escaping backslash characters before double quotes, so a trailing or pre-quote backslash cannot break out of the quoted argument. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-python/emitter/src/utils.ts | 2 +- packages/http-client-python/emitter/test/utils.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/emitter/src/utils.ts b/packages/http-client-python/emitter/src/utils.ts index 1d3a0f5a8db..2f4c22ee57e 100644 --- a/packages/http-client-python/emitter/src/utils.ts +++ b/packages/http-client-python/emitter/src/utils.ts @@ -278,7 +278,7 @@ export function capitalize(name: string): string { * consumers such as the Pyodide runtime and end up in generated files. */ export function quoteShellArg(value: string): string { - return `"${value.replace(/"/g, '\\"')}"`; + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } // Library namespaces that should not be used as client namespaces diff --git a/packages/http-client-python/emitter/test/utils.test.ts b/packages/http-client-python/emitter/test/utils.test.ts index b8dbed4cf51..332bf597e76 100644 --- a/packages/http-client-python/emitter/test/utils.test.ts +++ b/packages/http-client-python/emitter/test/utils.test.ts @@ -35,5 +35,7 @@ describe("typespec-python: utils", () => { strictEqual(quoteShellArg("Azure Web PubSub Chat Service"), '"Azure Web PubSub Chat Service"'); strictEqual(quoteShellArg("simple"), '"simple"'); strictEqual(quoteShellArg('has "quotes"'), '"has \\"quotes\\""'); + strictEqual(quoteShellArg("trailing\\"), '"trailing\\\\"'); + strictEqual(quoteShellArg('back\\slash "q"'), '"back\\\\slash \\"q\\""'); }); }); From 5b6eb0b9d58a1333a3ab0f6ea783c1cf3e8beed5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 6 Jul 2026 15:21:18 -0400 Subject: [PATCH 3/3] format --- .../http-client-python/generator/pygen/preprocess/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/preprocess/__init__.py b/packages/http-client-python/generator/pygen/preprocess/__init__.py index 6961d890a2d..8441a03bd77 100644 --- a/packages/http-client-python/generator/pygen/preprocess/__init__.py +++ b/packages/http-client-python/generator/pygen/preprocess/__init__.py @@ -420,7 +420,6 @@ def add_body_param_type( code_model["types"].append(body_parameter["type"]) - def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str: # we want to pad hidden variables as well if not name: