Skip to content

Commit 5f04581

Browse files
authored
Convert JSON parsing failures from assertions to exceptions (#7531)
This allows some useful error reporting with bad source maps. The JSON exception is converted to the existing map parse exception by the source map parser, allowing a unified interface. Also add several tests of bogus source maps.
1 parent ce2abc2 commit 5f04581

File tree

5 files changed

+93
-18
lines changed

5 files changed

+93
-18
lines changed

src/source-map.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919

2020
#include <optional>
2121

22+
#include "support/json.h"
2223
#include "wasm.h"
2324

2425
namespace wasm {
2526

2627
struct MapParseException {
27-
std::string text;
28-
29-
MapParseException(std::string text) : text(text) {}
28+
std::string errorText;
29+
MapParseException(std::string errorText) : errorText(errorText){};
30+
MapParseException(json::JsonParseException ex) : errorText(ex.errorText){};
3031
void dump(std::ostream& o) const;
3132
};
3233

src/support/json.h

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ namespace json {
4545

4646
using IString = wasm::IString;
4747

48+
struct JsonParseException {
49+
std::string errorText;
50+
51+
JsonParseException(std::string errorText) : errorText(errorText) {}
52+
void dump(std::ostream& o) const { o << "JSON parse error: " << errorText; }
53+
};
54+
55+
#define THROW_IF(expr, message) \
56+
if (expr) { \
57+
throw JsonParseException(message); \
58+
}
59+
4860
// Main value type
4961
struct Value {
5062
struct Ref : public std::shared_ptr<Value> {
@@ -277,7 +289,7 @@ struct Value {
277289
do {
278290
close = strchr(close + 1, '"');
279291
} while (*(close - 1) == '\\');
280-
assert(close);
292+
THROW_IF(!close, "malformed JSON string");
281293
*close = 0; // end this string, and reuse it straight from the input
282294
char* raw = curr + 1;
283295
if (stringEncoding == ASCII) {
@@ -301,24 +313,24 @@ struct Value {
301313
if (*curr == ']') {
302314
break;
303315
}
304-
assert(*curr == ',');
316+
THROW_IF(*curr != ',', "malformed JSON array");
305317
curr++;
306318
skip();
307319
}
308320
curr++;
309321
} else if (*curr == 'n') {
310322
// Null
311-
assert(strncmp(curr, "null", 4) == 0);
323+
THROW_IF(strncmp(curr, "null", 4) != 0, "unexpected JSON literal");
312324
setNull();
313325
curr += 4;
314326
} else if (*curr == 't') {
315327
// Bool true
316-
assert(strncmp(curr, "true", 4) == 0);
328+
THROW_IF(strncmp(curr, "true", 4) != 0, "unexpected JSON literal");
317329
setBool(true);
318330
curr += 4;
319331
} else if (*curr == 'f') {
320332
// Bool false
321-
assert(strncmp(curr, "false", 5) == 0);
333+
THROW_IF(strncmp(curr, "false", 5) != 0, "unexpected JSON literal");
322334
setBool(false);
323335
curr += 5;
324336
} else if (*curr == '{') {
@@ -327,15 +339,15 @@ struct Value {
327339
skip();
328340
setObject();
329341
while (*curr != '}') {
330-
assert(*curr == '"');
342+
THROW_IF(*curr != '"', "malformed key in JSON object");
331343
curr++;
332344
char* close = strchr(curr, '"');
333-
assert(close);
345+
THROW_IF(!close, "malformed key in JSON object");
334346
*close = 0; // end this string, and reuse it straight from the input
335347
IString key(curr);
336348
curr = close + 1;
337349
skip();
338-
assert(*curr == ':');
350+
THROW_IF(*curr != ':', "missing ':', in JSON object");
339351
curr++;
340352
skip();
341353
Ref value = Ref(new Value());
@@ -345,7 +357,7 @@ struct Value {
345357
if (*curr == '}') {
346358
break;
347359
}
348-
assert(*curr == ',');
360+
THROW_IF(*curr != ',', "malformed value in JSON object");
349361
curr++;
350362
skip();
351363
}

src/wasm/source-map.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ void MapParseException::dump(std::ostream& o) const {
2828
Colors::red(o);
2929
o << "map parse exception: ";
3030
Colors::green(o);
31-
o << text;
31+
o << errorText;
3232
Colors::magenta(o);
3333
o << "]";
3434
Colors::normal(o);
@@ -39,7 +39,11 @@ void SourceMapReader::parse(Module& wasm) {
3939
return;
4040
}
4141
json::Value json;
42-
json.parse(buffer.data(), json::Value::ASCII);
42+
try {
43+
json.parse(buffer.data(), json::Value::ASCII);
44+
} catch (json::JsonParseException jx) {
45+
throw MapParseException(jx);
46+
}
4347
if (!json.isObject()) {
4448
throw MapParseException("Source map is not valid JSON");
4549
}
@@ -135,6 +139,12 @@ SourceMapReader::readDebugLocationAt(size_t currLocation) {
135139
col += readBase64VLQ();
136140

137141
next = peek();
142+
if (next == ';') {
143+
// Generated JS files can have multiple lines, and mappings for each
144+
// line are separated by ';'. Wasm files do not have lines, so there
145+
// should be only one generated "line".
146+
throw MapParseException("Unexpected mapping for 2nd generated line");
147+
}
138148
if (next == ',' || next == '\"') {
139149
hasSymbol = false;
140150
break;

test/gtest/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ if(BUILD_FUZZTEST)
22
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/fuzztest)
33
else()
44
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googletest/include)
5+
include_directories(SYSTEM ${PROJECT_SOURCE_DIR}/third_party/googletest/googlemock/include)
56
endif()
67

78
set(unittest_SOURCES
@@ -18,7 +19,6 @@ set(unittest_SOURCES
1819
possible-contents.cpp
1920
printing.cpp
2021
scc.cpp
21-
source-map.cpp
2222
stringify.cpp
2323
suffix_tree.cpp
2424
topological-sort.cpp
@@ -29,6 +29,14 @@ set(unittest_SOURCES
2929

3030
if(BUILD_FUZZTEST)
3131
set(unittest_SOURCES ${unittest_SOURCES} type-domains.cpp)
32+
else()
33+
# source-map.cpp uses the gmock-matchers.h header, which is included with the
34+
# "standard" upstream gtest library, but not with the one bundled into the
35+
# fuzztest library (and the gmock included with upstream gtest seems
36+
# incompatible with the one in fuzztest). For now we work around this by just
37+
# excluding source-map.cpp from the fuzztest build, but if we start using
38+
# gmock more we should figure out what the right way to hande this is.
39+
set(unittest_SOURCES ${unittest_SOURCES} source-map.cpp)
3240
endif()
3341

3442
# suffix_tree.cpp includes LLVM header using std::iterator (deprecated in C++17)

test/gtest/source-map.cpp

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "source-map.h"
1818
#include "print-test.h"
19+
#include "gmock/gmock-matchers.h"
1920
#include "gtest/gtest.h"
2021

2122
using namespace wasm;
@@ -53,6 +54,16 @@ class SourceMapTest : public PrintTest {
5354
EXPECT_EQ(loc->columnNumber, col);
5455
EXPECT_EQ(loc->symbolNameIndex, sym);
5556
}
57+
58+
void ExpectParseError(std::string& mapString, const char* expectedError) {
59+
SCOPED_TRACE(mapString);
60+
EXPECT_THROW(parseMap(mapString), MapParseException);
61+
try {
62+
parseMap(mapString);
63+
} catch (MapParseException ex) {
64+
EXPECT_THAT(ex.errorText, ::testing::HasSubstr(expectedError));
65+
}
66+
}
5667
};
5768

5869
// Check that debug location parsers can handle single-segment mappings.
@@ -73,16 +84,49 @@ TEST_F(SourceMapTest, SourceMappingSingleSegment) {
7384
EXPECT_FALSE(loc.has_value());
7485
}
7586

76-
TEST_F(SourceMapTest, BadSourceMap) {
77-
// This source map is missing the version field.
87+
TEST_F(SourceMapTest, BadSourceMaps) {
88+
// Test that a malformed JSON string throws rather than asserting.
7889
std::string sourceMap = R"(
90+
{
91+
"version": 3,
92+
"sources": ["foo.c"],
93+
"mappings": ""
94+
malformed
95+
}
96+
)";
97+
ExpectParseError(sourceMap, "malformed value in JSON object");
98+
99+
// Valid JSON, but missing the version field.
100+
sourceMap = R"(
79101
{
80102
"sources": [],
81103
"names": [],
82104
"mappings": "A"
83105
}
84106
)";
85-
EXPECT_THROW(parseMap(sourceMap), MapParseException);
107+
ExpectParseError(sourceMap, "Source map version missing");
108+
109+
// Valid JSON, but a bad "sources" field.
110+
sourceMap = R"(
111+
{
112+
"version": 3,
113+
"sources": 123,
114+
"mappings": ""
115+
}
116+
)";
117+
ExpectParseError(sourceMap, "Source map sources missing or not an array");
118+
119+
sourceMap = R"(
120+
{
121+
"version": 3,
122+
"sources": ["foo.c"],
123+
"mappings": "C;A"
124+
}
125+
)";
126+
parseMap(sourceMap);
127+
// Mapping strings are parsed incrementally, so errors don't show up until a
128+
// sufficiently far-advanced location is requested to reach the problem.
129+
EXPECT_THROW(reader->readDebugLocationAt(1), MapParseException);
86130
}
87131

88132
TEST_F(SourceMapTest, SourcesAndNames) {

0 commit comments

Comments
 (0)