Issue #1732 - Implement JSON superset proposal.

This change makes unicode line and paragraph separators allowed in JS strings,
removing the only quirk of the JS string definition that made JSON not a valid
subset of JS.

Resolves #1732
This commit is contained in:
Moonchild 2021-02-05 12:44:26 +00:00 • committed by roytam1
commit 34dc51690f
4 changed files with 44 additions and 89 deletions

View file

@ -141,35 +141,29 @@ template <typename CharT>
static bool static bool
EvalStringMightBeJSON(const mozilla::Range<const CharT> chars) EvalStringMightBeJSON(const mozilla::Range<const CharT> chars)
{ {
// If the eval string starts with '(' or '[' and ends with ')' or ']', it may be JSON. // If the eval string starts with '(' or '[' and ends with ')' or ']', it
// Try the JSON parser first because it's much faster. If the eval string // may be JSON. Try the JSON parser first because it's much faster. If
// isn't JSON, JSON parsing will probably fail quickly, so little time // the eval string isn't JSON, JSON parsing will probably fail quickly, so
// will be lost. // little time will be lost.
size_t length = chars.length(); size_t length = chars.length();
if (length > 2 && if (length < 2)
((chars[0] == '[' && chars[length - 1] == ']') || return false;
(chars[0] == '(' && chars[length - 1] == ')')))
{
// Remarkably, JavaScript syntax is not a superset of JSON syntax:
// strings in JavaScript cannot contain the Unicode line and paragraph
// terminator characters U+2028 and U+2029, but strings in JSON can.
// Rather than force the JSON parser to handle this quirk when used by
// eval, we simply don't use the JSON parser when either character
// appears in the provided string. See bug 657367.
if (sizeof(CharT) > 1) {
for (RangedPtr<const CharT> cp = chars.begin() + 1, end = chars.end() - 1;
cp < end;
cp++)
{
char16_t c = *cp;
if (c == 0x2028 || c == 0x2029)
return false;
}
}
return true; // It used to be that strings in JavaScript forbid U+2028 LINE SEPARATOR
} // and U+2029 PARAGRAPH SEPARATOR, so something like
return false; //
// eval("['" + "\u2028" + "']");
//
// i.e. an array containing a string with a line separator in it, *would*
// be JSON but *would not* be valid JavaScript. Handing such a string to
// the JSON parser would then fail to recognize a syntax error. As of
// <https://tc39.github.io/proposal-json-superset/> JavaScript strings may
// contain these two code points, so it's safe to JSON-parse eval strings
// that contain them.
CharT first = chars[0], last = chars[length - 1];
return (first == '[' && last == ']') ||
(first == '(' && last == ')');
} }
template <typename CharT> template <typename CharT>

View file

@ -603,6 +603,10 @@ TokenStream::TokenBuf::findEOLMax(size_t start, size_t max)
if (n >= max) if (n >= max)
break; break;
n++; n++;
// This stops at U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR in
// string and template literals. These code points do affect line and
// column coordinates, even as they encode their literal values.
if (TokenBuf::isRawEOLChar(*p++)) if (TokenBuf::isRawEOLChar(*p++))
break; break;
} }
@ -2129,8 +2133,9 @@ TokenStream::getStringOrTemplateToken(int untilChar, Token** tp)
} }
break; break;
} }
} else if (TokenBuf::isRawEOLChar(c)) { } else if (c == '\r' || c == '\n') {
if (!parsingTemplate) { if (!parsingTemplate) {
// String literals don't allow ASCII line breaks.
ungetCharIgnoreEOL(c); ungetCharIgnoreEOL(c);
error(JSMSG_UNTERMINATED_STRING); error(JSMSG_UNTERMINATED_STRING);
return false; return false;
@ -2138,10 +2143,18 @@ TokenStream::getStringOrTemplateToken(int untilChar, Token** tp)
if (c == '\r') { if (c == '\r') {
c = '\n'; c = '\n';
if (userbuf.peekRawChar() == '\n') if (userbuf.peekRawChar() == '\n')
// Treat CRLF as a single line break.
skipCharsIgnoreEOL(1); skipCharsIgnoreEOL(1);
} }
updateLineInfoForEOL(); updateLineInfoForEOL();
updateFlagsForEOL(); updateFlagsForEOL();
} else if (c == LINE_SEPARATOR || c == PARA_SEPARATOR) {
// U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR encode
// their literal values in template literals and (as of the
// JSON superset proposal) string literals, but they still count
// as line terminators when computing line/column coordinates.
updateLineInfoForEOL();
updateFlagsForEOL();
} else if (parsingTemplate && c == '$') { } else if (parsingTemplate && c == '$') {
if ((nc = getCharIgnoreEOL()) == '{') if ((nc = getCharIgnoreEOL()) == '{')
break; break;

View file

@ -55,13 +55,10 @@ function testEvalHackNotJSON() {
arr = eval("[]; var z; [1, 2, 3, \"abc\u1200\"]"); arr = eval("[]; var z; [1, 2, 3, \"abc\u1200\"]");
assertEq(JSON.stringify(arr), '[1,2,3,"abc\u1200"]'); assertEq(JSON.stringify(arr), '[1,2,3,"abc\u1200"]');
try { // JSON superset
eval("[1, 2, 3, \"abc\u2028\"]"); var arr = eval("[1, 2, 3, \"abc\u2028\"]");
throw new Error("U+2028 shouldn't eval"); assertEq(arr.length, 4);
} catch (e) { assertEq(arr[3], "abc\u2028");
assertEq(e instanceof SyntaxError, true,
"should have thrown a SyntaxError, instead got " + e);
}
} }
testEvalHackNotJSON(); testEvalHackNotJSON();

View file

@ -3,7 +3,9 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
var BUGNUMBER = 657367; var BUGNUMBER = 657367;
var summary = "eval must not parse strings containing U+2028 or U+2029"; var summary =
"eval via the JSON parser should parse strings containing U+2028/U+2029 " +
"(as of <https://tc39.github.io/proposal-json-superset/>, that is)";
print(BUGNUMBER + ": " + summary); print(BUGNUMBER + ": " + summary);
@ -11,59 +13,8 @@ print(BUGNUMBER + ": " + summary);
* BEGIN TEST * * BEGIN TEST *
**************/ **************/
function esc(s) assertEq(eval('("\u2028")'), "\u2028");
{ assertEq(eval('("\u2029")'), "\u2029");
return s.split("").map(function(v)
{
var code =
("000" + v.charCodeAt(0).toString(16)).slice(-4);
return "\\u" + code;
}).join("");
}
try
{
var r = eval('"\u2028"');
throw new Error("\"\\u2028\" didn't throw, returned " + esc(r));
}
catch (e)
{
assertEq(e instanceof SyntaxError, true,
"U+2028 is not a valid string character");
}
try
{
var r = eval('("\u2028")');
throw new Error("(\"\\u2028\") didn't throw, returned " + esc(r));
}
catch (e)
{
assertEq(e instanceof SyntaxError, true,
"U+2028 is not a valid string character");
}
try
{
var r = eval('"\u2029"');
throw new Error("\"\\u2029\" didn't throw, returned " + esc(r));
}
catch (e)
{
assertEq(e instanceof SyntaxError, true,
"U+2029 is not a valid string character");
}
try
{
var r = eval('("\u2029")');
throw new Error("(\"\\u2029\") didn't throw, returned " + esc(r));
}
catch (e)
{
assertEq(e instanceof SyntaxError, true,
"U+2029 is not a valid string character");
}
/******************************************************************************/ /******************************************************************************/