From 540aac85ae7c0d710e8ea1752a3e5be82f67c96b Mon Sep 17 00:00:00 2001 From: AugustHagedal Date: Sun, 22 Feb 2026 00:41:04 +0100 Subject: [PATCH] fix: escape string literals in GenerateIds.ts code generation Module names containing double-quotes, single-quotes, or backslashes were embedded into generated ObjC and TypeScript/JS code without escaping, producing syntactically broken output. Add escapeObjCString and escapeJSSingleQuotedString helpers and apply them at the two affected sites. Adds four regression tests covering both characters for both output languages. --- compiler/companion/src/GenerateIds.spec.ts | 20 ++++++++++++++++++++ compiler/companion/src/GenerateIds.ts | 22 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/compiler/companion/src/GenerateIds.spec.ts b/compiler/companion/src/GenerateIds.spec.ts index 05b691fa..fcef5889 100644 --- a/compiler/companion/src/GenerateIds.spec.ts +++ b/compiler/companion/src/GenerateIds.spec.ts @@ -94,4 +94,24 @@ NSString *SCValdiIdHelloWorldMySecondId() { `, ); }); + + it('escapes double quotes in ObjC impl when module name contains them', () => { + const iosImpl = generateIds('hello"world', '"SCHelloWorld/Ids.h"', TEST_ID_FILES).ios.impl; + expect(iosImpl).toContain('return @"hello\\"world/my_first_id";'); + }); + + it('escapes backslashes in ObjC impl when module name contains them', () => { + const iosImpl = generateIds('hello\\world', '"SCHelloWorld/Ids.h"', TEST_ID_FILES).ios.impl; + expect(iosImpl).toContain('return @"hello\\\\world/my_first_id";'); + }); + + it('escapes single quotes in JS impl when module name contains them', () => { + const jsImpl = generateIds("hello'world", '"SCHelloWorld/Ids.h"', TEST_ID_FILES).typescript.implementation; + expect(jsImpl).toContain("myFirstId: () => 'hello\\'world/my_first_id',"); + }); + + it('escapes backslashes in JS impl when module name contains them', () => { + const jsImpl = generateIds('hello\\world', '"SCHelloWorld/Ids.h"', TEST_ID_FILES).typescript.implementation; + expect(jsImpl).toContain("myFirstId: () => 'hello\\\\world/my_first_id',"); + }); }); diff --git a/compiler/companion/src/GenerateIds.ts b/compiler/companion/src/GenerateIds.ts index 2e0085c1..0dc76da6 100644 --- a/compiler/companion/src/GenerateIds.ts +++ b/compiler/companion/src/GenerateIds.ts @@ -38,6 +38,24 @@ interface Id { description?: string; } +function escapeObjCString(str: string): string { + return str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t'); +} + +function escapeJSSingleQuotedString(str: string): string { + return str + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t'); +} + function parseIds(moduleName: string, fileContent: string): Id[] { const idsYaml = yaml.load(fileContent.toString()) as IdsYaml; @@ -117,7 +135,7 @@ function generateIOSIds(ids: Id[], iosHeaderImportPath: string): ObjectiveCFile header.append(`extern ${functionSignature};\n`); impl.append(`${functionSignature} {\n`); impl.withIndentation(' ', () => { - impl.append(`return @"${id.identifier}";\n`); + impl.append(`return @"${escapeObjCString(id.identifier)}";\n`); }); impl.append('}\n'); } @@ -150,7 +168,7 @@ function generateTypeScriptIds(ids: Id[]): TypeScriptFile { } definition.append(`static ${id.name}(): string;\n`); - implementation.append(`${id.name}: () => '${id.identifier}',\n`); + implementation.append(`${id.name}: () => '${escapeJSSingleQuotedString(id.identifier)}',\n`); } definition.endIndent();