From f339b15869b68ba2f8ea00e25921851d588f21aa 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 3b5fccf4..d067eeac 100644 --- a/compiler/companion/src/GenerateIds.spec.ts +++ b/compiler/companion/src/GenerateIds.spec.ts @@ -102,4 +102,24 @@ NS_ASSUME_NONNULL_END `, ); }); + + 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 ac011013..6b1f196d 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; @@ -122,7 +140,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'); } @@ -158,7 +176,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();