Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/emscripten-optimizer/simple_ast.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <limits>
#include <ostream>
#include <set>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
Expand All @@ -37,6 +38,7 @@
#include "snprintf.h"
#include "support/mixed_arena.h"
#include "support/safe_integer.h"
#include "support/string.h"

#define errv(str, ...) fprintf(stderr, str "\n", __VA_ARGS__);
#define printErr(str) fprintf(stderr, str "\n");
Expand Down Expand Up @@ -1080,8 +1082,12 @@ struct JSPrinter {
}

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

Expand Down
44 changes: 44 additions & 0 deletions src/support/string.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,50 @@ std::ostream& printEscapedJSON(std::ostream& os, std::string_view str) {
return os << '"';
}

std::ostream& printEscapedJS(std::ostream& os, std::string_view str) {
for (size_t i = 0; i < str.size(); i++) {
unsigned char c = str[i];
switch (c) {
case '\\':
os << "\\\\";
break;
case '\'':
os << "\\'";
break;
case '"':
os << "\\\"";
break;
case '\n':
os << "\\n";
break;
case '\r':
os << "\\r";
break;
default:
// Check for U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
// which are valid in JSON strings but act as line terminators in JS.
// They are encoded as E2 80 A8 and E2 80 A9 in UTF-8.
Comment on lines +491 to +493

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine there are many other characters we will want to escape so that the output remains human readable. Instead of adding a new escaping method, can we reuse printEscapedJSON, maybe by adding a flag for the encoding of the input? Alternatively, we could have printEscapedJS convert its input to WTF16 and then call printEscapedJSON. We can move this comment about LINE SEPARATOR and PARAGRAPH SEPARATOR into printEscapedJSON so we remember to continue escaping them in the future.

if (c == 0xE2 && i + 2 < str.size() &&
static_cast<unsigned char>(str[i + 1]) == 0x80) {
unsigned char c2 = str[i + 2];
if (c2 == 0xA8) {
os << "\\u2028";
i += 2;
break;
}
if (c2 == 0xA9) {
os << "\\u2029";
i += 2;
break;
}
}
os << char(c);
break;
}
}
return os;
}

bool isUTF8(std::string_view str) {
while (str.size()) {
auto u = takeWTF8CodePoint(str);
Expand Down
7 changes: 7 additions & 0 deletions src/support/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ std::ostream& printEscaped(std::ostream& os, std::string_view str);
// `str` must be a valid WTF-16 string.
std::ostream& printEscapedJSON(std::ostream& os, std::string_view str);

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

std::ostream& writeWTF8CodePoint(std::ostream& os, uint32_t u);

std::ostream& writeWTF16CodePoint(std::ostream& os, uint32_t u);
Expand Down
23 changes: 17 additions & 6 deletions src/wasm2js.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "passes/passes.h"
#include "support/base64.h"
#include "support/file.h"
#include "support/string.h"
#include "wasm-builder.h"
#include "wasm-io.h"
#include "wasm-validator.h"
Expand Down Expand Up @@ -2652,6 +2653,15 @@ void Wasm2JSBuilder::addMemoryGrowFunc(Ref ast, Module* wasm) {
ast->push_back(memoryGrowFunc);
}

// Escape a string for safe inclusion in a JavaScript string literal.
// WebAssembly module/base names are arbitrary UTF-8 and may contain characters
// that are JS string metacharacters (quotes, backslashes, newlines, etc.).
static std::string escapeJSString(std::string_view str) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use printEscapedJSON from support/string.h instead? If not, that would be a better place to add new escaping logic.

std::ostringstream ss;
String::printEscapedJS(ss, str);
return ss.str();
}

// Wasm2JSBuilder emits the core of the module - the functions etc. that would
// be the asm.js function in an asm.js world. This class emits the rest of the
// "glue" around that.
Expand Down Expand Up @@ -2732,7 +2742,7 @@ void Wasm2JSGlue::emitPreES6() {
baseModuleMap[base] = module;
if (!seenModules.contains(module)) {
out << "import * as " << asmangle(module.toString()) << " from '"
<< module << "';\n";
<< escapeJSString(module.toString()) << "';\n";
seenModules.insert(module);
}
};
Expand Down Expand Up @@ -2795,7 +2805,7 @@ void Wasm2JSGlue::emitPostES6() {
if (seenModules.contains(import->module)) {
return;
}
out << " \"" << import->module
out << " \"" << escapeJSString(import->module.toString())
<< "\": " << asmangle(import->module.toString()) << ",\n";
seenModules.insert(import->module);
});
Expand All @@ -2806,7 +2816,7 @@ void Wasm2JSGlue::emitPostES6() {
if (ABI::wasm2js::isHelper(import->base)) {
return;
}
out << " \"" << import->module << "\": {\n";
out << " \"" << escapeJSString(import->module.toString()) << "\": {\n";
out << " " << asmangle(import->base.toString()) << ": { buffer : mem"
<< moduleName.str << " }\n";
out << " },\n";
Expand All @@ -2821,7 +2831,7 @@ void Wasm2JSGlue::emitPostES6() {
if (seenModules.contains(import->module)) {
return;
}
out << " \"" << import->module
out << " \"" << escapeJSString(import->module.toString())
<< "\": " << asmangle(import->module.toString()) << ",\n";
seenModules.insert(import->module);
});
Expand Down Expand Up @@ -2925,8 +2935,9 @@ void Wasm2JSGlue::emitMemory() {
if (auto* get = segment.offset->dynCast<GlobalGet>()) {
auto internalName = get->name;
auto importedGlobal = wasm.getGlobal(internalName);
return std::string("imports['") + importedGlobal->module.toString() +
"']['" + importedGlobal->base.toString() + "']";
return std::string("imports['") +
escapeJSString(importedGlobal->module.toString()) + "']['" +
escapeJSString(importedGlobal->base.toString()) + "']";
}
Fatal() << "non-constant offsets aren't supported yet\n";
};
Expand Down
1 change: 1 addition & 0 deletions test/gtest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ set(unittest_SOURCES
printing.cpp
public-type-validator.cpp
scc.cpp
string.cpp
stringify.cpp
subtype-exprs.cpp
suffix_tree.cpp
Expand Down
70 changes: 70 additions & 0 deletions test/gtest/string.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#include <sstream>

#include "support/string.h"
#include "gtest/gtest.h"

using namespace wasm;

using StringTest = ::testing::Test;

namespace {

std::string escapeJS(std::string_view str) {
std::stringstream ss;
String::printEscapedJS(ss, str);
return ss.str();
}

} // anonymous namespace

TEST_F(StringTest, PrintEscapedJSPlain) {
EXPECT_EQ(escapeJS(""), "");
EXPECT_EQ(escapeJS("env"), "env");
EXPECT_EQ(escapeJS("wasi_snapshot_preview1"), "wasi_snapshot_preview1");
}

TEST_F(StringTest, PrintEscapedJSQuotes) {
// Both quote characters are escaped so the result is safe in either a
// single- or double-quoted literal.
EXPECT_EQ(escapeJS("it's"), "it\\'s");
EXPECT_EQ(escapeJS("say \"hi\""), "say \\\"hi\\\"");
EXPECT_EQ(escapeJS("mixed'and\"quotes"), "mixed\\'and\\\"quotes");
}

TEST_F(StringTest, PrintEscapedJSBackslash) {
EXPECT_EQ(escapeJS("a\\b"), "a\\\\b");
// A backslash followed by a quote must not collapse into a lone escape.
EXPECT_EQ(escapeJS("\\'"), "\\\\\\'");
}

TEST_F(StringTest, PrintEscapedJSNewlines) {
EXPECT_EQ(escapeJS("line1\nline2"), "line1\\nline2");
EXPECT_EQ(escapeJS("line1\r\nline2"), "line1\\r\\nline2");
}

TEST_F(StringTest, PrintEscapedJSLineSeparators) {
// U+2028 and U+2029 are valid unescaped in JSON strings but are line
// terminators in JavaScript, so they must be escaped.
EXPECT_EQ(escapeJS("a\xE2\x80\xA8"
"b"),
"a\\u2028b");
EXPECT_EQ(escapeJS("a\xE2\x80\xA9"
"b"),
"a\\u2029b");
// Other characters in the same UTF-8 range pass through unchanged.
EXPECT_EQ(escapeJS("a\xE2\x80\xA6"
"b"),
"a\xE2\x80\xA6"
"b");
// A truncated sequence at the end of the string is passed through
// byte-by-byte rather than read out of bounds.
EXPECT_EQ(escapeJS("\xE2\x80"), "\xE2\x80");
}

TEST_F(StringTest, PrintEscapedJSNonASCII) {
// Non-ASCII UTF-8 is passed through unmodified.
EXPECT_EQ(escapeJS("m\xC3\xB3"
"dulo"),
"m\xC3\xB3"
"dulo");
}
28 changes: 28 additions & 0 deletions test/wasm2js/string_escapes.2asm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as mod_ule_x from 'mod\'ule\"x';

function asmFunc(imports) {
var Math_imul = Math.imul;
var Math_fround = Math.fround;
var Math_abs = Math.abs;
var Math_clz32 = Math.clz32;
var Math_min = Math.min;
var Math_max = Math.max;
var Math_floor = Math.floor;
var Math_ceil = Math.ceil;
var Math_trunc = Math.trunc;
var Math_sqrt = Math.sqrt;
var mod_ule_x = imports["mod\'ule\"x"];
var base = mod_ule_x["ba\\se\'"];
function exported() {
base();
}

return {
"exported": exported
};
}

var retasmFunc = asmFunc({
"mod\'ule\"x": mod_ule_x,
});
export var exported = retasmFunc.exported;
28 changes: 28 additions & 0 deletions test/wasm2js/string_escapes.2asm.js.opt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as mod_ule_x from 'mod\'ule\"x';

function asmFunc(imports) {
var Math_imul = Math.imul;
var Math_fround = Math.fround;
var Math_abs = Math.abs;
var Math_clz32 = Math.clz32;
var Math_min = Math.min;
var Math_max = Math.max;
var Math_floor = Math.floor;
var Math_ceil = Math.ceil;
var Math_trunc = Math.trunc;
var Math_sqrt = Math.sqrt;
var mod_ule_x = imports["mod\'ule\"x"];
var base = mod_ule_x["ba\\se\'"];
function exported() {
base();
}

return {
"exported": exported
};
}

var retasmFunc = asmFunc({
"mod\'ule\"x": mod_ule_x,
});
export var exported = retasmFunc.exported;
6 changes: 6 additions & 0 deletions test/wasm2js/string_escapes.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(module
(import "mod'ule\"x" "ba\\se'" (func $base))
(func $exported (export "exported")
(call $base)
)
)
Loading