Skip to content

Commit 5b6b5aa

Browse files
sumleoclaude
andcommitted
Move JS string escaping to support/string and escape the JS printer's string literals
Per review feedback, move the escaping logic into support/string.{h,cpp} as a reusable String::printEscapedJS utility; wasm2js.h keeps a thin escapeJSString wrapper so call sites are unchanged. Also apply the escaping in cashew's printString, which previously emitted string contents verbatim between double quotes. This covers the imports object accesses in the asm.js function body (e.g. imports["mod'ule"] in the --emscripten output path) that the earlier commit missed. Add gtest coverage for printEscapedJS and an end-to-end wasm2js golden test (string_escapes.wast) with quote/backslash metacharacters in import module and base names. Regenerating the wasm2js expectations produced no changes to existing tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3dcfb40 commit 5b6b5aa

9 files changed

Lines changed: 195 additions & 44 deletions

File tree

src/emscripten-optimizer/simple_ast.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <limits>
3030
#include <ostream>
3131
#include <set>
32+
#include <sstream>
3233
#include <unordered_map>
3334
#include <unordered_set>
3435
#include <vector>
@@ -37,6 +38,7 @@
3738
#include "snprintf.h"
3839
#include "support/mixed_arena.h"
3940
#include "support/safe_integer.h"
41+
#include "support/string.h"
4042

4143
#define errv(str, ...) fprintf(stderr, str "\n", __VA_ARGS__);
4244
#define printErr(str) fprintf(stderr, str "\n");
@@ -1080,8 +1082,12 @@ struct JSPrinter {
10801082
}
10811083

10821084
void printString(Ref node) {
1085+
// String contents (e.g. wasm module and base names) are arbitrary and may
1086+
// contain JS string metacharacters, so they must be escaped.
1087+
std::ostringstream escaped;
1088+
wasm::String::printEscapedJS(escaped, node[1]->getCString());
10831089
emit('"');
1084-
emit(node[1]->getCString());
1090+
emit(escaped.str().c_str());
10851091
emit('"');
10861092
}
10871093

src/support/string.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,50 @@ std::ostream& printEscapedJSON(std::ostream& os, std::string_view str) {
468468
return os << '"';
469469
}
470470

471+
std::ostream& printEscapedJS(std::ostream& os, std::string_view str) {
472+
for (size_t i = 0; i < str.size(); i++) {
473+
unsigned char c = str[i];
474+
switch (c) {
475+
case '\\':
476+
os << "\\\\";
477+
break;
478+
case '\'':
479+
os << "\\'";
480+
break;
481+
case '"':
482+
os << "\\\"";
483+
break;
484+
case '\n':
485+
os << "\\n";
486+
break;
487+
case '\r':
488+
os << "\\r";
489+
break;
490+
default:
491+
// Check for U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
492+
// which are valid in JSON strings but act as line terminators in JS.
493+
// They are encoded as E2 80 A8 and E2 80 A9 in UTF-8.
494+
if (c == 0xE2 && i + 2 < str.size() &&
495+
static_cast<unsigned char>(str[i + 1]) == 0x80) {
496+
unsigned char c2 = str[i + 2];
497+
if (c2 == 0xA8) {
498+
os << "\\u2028";
499+
i += 2;
500+
break;
501+
}
502+
if (c2 == 0xA9) {
503+
os << "\\u2029";
504+
i += 2;
505+
break;
506+
}
507+
}
508+
os << char(c);
509+
break;
510+
}
511+
}
512+
return os;
513+
}
514+
471515
bool isUTF8(std::string_view str) {
472516
while (str.size()) {
473517
auto u = takeWTF8CodePoint(str);

src/support/string.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ std::ostream& printEscaped(std::ostream& os, std::string_view str);
9797
// `str` must be a valid WTF-16 string.
9898
std::ostream& printEscapedJSON(std::ostream& os, std::string_view str);
9999

100+
// Escape a UTF-8 string for safe inclusion in a JavaScript string literal.
101+
// Escapes both quote characters (so the result is usable in either a single-
102+
// or double-quoted literal), backslashes, newlines, and the U+2028/U+2029
103+
// separators that are line terminators in JS. Does not emit surrounding
104+
// quotes.
105+
std::ostream& printEscapedJS(std::ostream& os, std::string_view str);
106+
100107
std::ostream& writeWTF8CodePoint(std::ostream& os, uint32_t u);
101108

102109
std::ostream& writeWTF16CodePoint(std::ostream& os, uint32_t u);

src/wasm2js.h

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include "passes/passes.h"
4444
#include "support/base64.h"
4545
#include "support/file.h"
46+
#include "support/string.h"
4647
#include "wasm-builder.h"
4748
#include "wasm-io.h"
4849
#include "wasm-validator.h"
@@ -2656,49 +2657,9 @@ void Wasm2JSBuilder::addMemoryGrowFunc(Ref ast, Module* wasm) {
26562657
// WebAssembly module/base names are arbitrary UTF-8 and may contain characters
26572658
// that are JS string metacharacters (quotes, backslashes, newlines, etc.).
26582659
static std::string escapeJSString(std::string_view str) {
2659-
std::string result;
2660-
result.reserve(str.size());
2661-
for (size_t i = 0; i < str.size(); i++) {
2662-
unsigned char c = str[i];
2663-
switch (c) {
2664-
case '\\':
2665-
result += "\\\\";
2666-
break;
2667-
case '\'':
2668-
result += "\\'";
2669-
break;
2670-
case '"':
2671-
result += "\\\"";
2672-
break;
2673-
case '\n':
2674-
result += "\\n";
2675-
break;
2676-
case '\r':
2677-
result += "\\r";
2678-
break;
2679-
default:
2680-
// Check for U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
2681-
// which are valid in JSON strings but act as line terminators in JS.
2682-
// They are encoded as E2 80 A8 and E2 80 A9 in UTF-8.
2683-
if (c == 0xE2 && i + 2 < str.size() &&
2684-
static_cast<unsigned char>(str[i + 1]) == 0x80) {
2685-
unsigned char c2 = str[i + 2];
2686-
if (c2 == 0xA8) {
2687-
result += "\\u2028";
2688-
i += 2;
2689-
break;
2690-
}
2691-
if (c2 == 0xA9) {
2692-
result += "\\u2029";
2693-
i += 2;
2694-
break;
2695-
}
2696-
}
2697-
result += static_cast<char>(c);
2698-
break;
2699-
}
2700-
}
2701-
return result;
2660+
std::ostringstream ss;
2661+
String::printEscapedJS(ss, str);
2662+
return ss.str();
27022663
}
27032664

27042665
// Wasm2JSBuilder emits the core of the module - the functions etc. that would

test/gtest/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ set(unittest_SOURCES
3030
printing.cpp
3131
public-type-validator.cpp
3232
scc.cpp
33+
string.cpp
3334
stringify.cpp
3435
subtype-exprs.cpp
3536
suffix_tree.cpp

test/gtest/string.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#include <sstream>
2+
3+
#include "support/string.h"
4+
#include "gtest/gtest.h"
5+
6+
using namespace wasm;
7+
8+
using StringTest = ::testing::Test;
9+
10+
namespace {
11+
12+
std::string escapeJS(std::string_view str) {
13+
std::stringstream ss;
14+
String::printEscapedJS(ss, str);
15+
return ss.str();
16+
}
17+
18+
} // anonymous namespace
19+
20+
TEST_F(StringTest, PrintEscapedJSPlain) {
21+
EXPECT_EQ(escapeJS(""), "");
22+
EXPECT_EQ(escapeJS("env"), "env");
23+
EXPECT_EQ(escapeJS("wasi_snapshot_preview1"), "wasi_snapshot_preview1");
24+
}
25+
26+
TEST_F(StringTest, PrintEscapedJSQuotes) {
27+
// Both quote characters are escaped so the result is safe in either a
28+
// single- or double-quoted literal.
29+
EXPECT_EQ(escapeJS("it's"), "it\\'s");
30+
EXPECT_EQ(escapeJS("say \"hi\""), "say \\\"hi\\\"");
31+
EXPECT_EQ(escapeJS("mixed'and\"quotes"), "mixed\\'and\\\"quotes");
32+
}
33+
34+
TEST_F(StringTest, PrintEscapedJSBackslash) {
35+
EXPECT_EQ(escapeJS("a\\b"), "a\\\\b");
36+
// A backslash followed by a quote must not collapse into a lone escape.
37+
EXPECT_EQ(escapeJS("\\'"), "\\\\\\'");
38+
}
39+
40+
TEST_F(StringTest, PrintEscapedJSNewlines) {
41+
EXPECT_EQ(escapeJS("line1\nline2"), "line1\\nline2");
42+
EXPECT_EQ(escapeJS("line1\r\nline2"), "line1\\r\\nline2");
43+
}
44+
45+
TEST_F(StringTest, PrintEscapedJSLineSeparators) {
46+
// U+2028 and U+2029 are valid unescaped in JSON strings but are line
47+
// terminators in JavaScript, so they must be escaped.
48+
EXPECT_EQ(escapeJS("a\xE2\x80\xA8"
49+
"b"),
50+
"a\\u2028b");
51+
EXPECT_EQ(escapeJS("a\xE2\x80\xA9"
52+
"b"),
53+
"a\\u2029b");
54+
// Other characters in the same UTF-8 range pass through unchanged.
55+
EXPECT_EQ(escapeJS("a\xE2\x80\xA6"
56+
"b"),
57+
"a\xE2\x80\xA6"
58+
"b");
59+
// A truncated sequence at the end of the string is passed through
60+
// byte-by-byte rather than read out of bounds.
61+
EXPECT_EQ(escapeJS("\xE2\x80"), "\xE2\x80");
62+
}
63+
64+
TEST_F(StringTest, PrintEscapedJSNonASCII) {
65+
// Non-ASCII UTF-8 is passed through unmodified.
66+
EXPECT_EQ(escapeJS("m\xC3\xB3"
67+
"dulo"),
68+
"m\xC3\xB3"
69+
"dulo");
70+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as mod_ule_x from 'mod\'ule\"x';
2+
3+
function asmFunc(imports) {
4+
var Math_imul = Math.imul;
5+
var Math_fround = Math.fround;
6+
var Math_abs = Math.abs;
7+
var Math_clz32 = Math.clz32;
8+
var Math_min = Math.min;
9+
var Math_max = Math.max;
10+
var Math_floor = Math.floor;
11+
var Math_ceil = Math.ceil;
12+
var Math_trunc = Math.trunc;
13+
var Math_sqrt = Math.sqrt;
14+
var mod_ule_x = imports["mod\'ule\"x"];
15+
var base = mod_ule_x["ba\\se\'"];
16+
function exported() {
17+
base();
18+
}
19+
20+
return {
21+
"exported": exported
22+
};
23+
}
24+
25+
var retasmFunc = asmFunc({
26+
"mod\'ule\"x": mod_ule_x,
27+
});
28+
export var exported = retasmFunc.exported;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as mod_ule_x from 'mod\'ule\"x';
2+
3+
function asmFunc(imports) {
4+
var Math_imul = Math.imul;
5+
var Math_fround = Math.fround;
6+
var Math_abs = Math.abs;
7+
var Math_clz32 = Math.clz32;
8+
var Math_min = Math.min;
9+
var Math_max = Math.max;
10+
var Math_floor = Math.floor;
11+
var Math_ceil = Math.ceil;
12+
var Math_trunc = Math.trunc;
13+
var Math_sqrt = Math.sqrt;
14+
var mod_ule_x = imports["mod\'ule\"x"];
15+
var base = mod_ule_x["ba\\se\'"];
16+
function exported() {
17+
base();
18+
}
19+
20+
return {
21+
"exported": exported
22+
};
23+
}
24+
25+
var retasmFunc = asmFunc({
26+
"mod\'ule\"x": mod_ule_x,
27+
});
28+
export var exported = retasmFunc.exported;

test/wasm2js/string_escapes.wast

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
(module
2+
(import "mod'ule\"x" "ba\\se'" (func $base))
3+
(func $exported (export "exported")
4+
(call $base)
5+
)
6+
)

0 commit comments

Comments
 (0)