Bug 564621, bug 582077 - JSON.parse shouldn't allow {"a" : "b",} or [1,]. But, because Firefox's bookmarks "JSON" generation has historically generated invalid JSON (it no longer does, see bug 505656), preserve a "legacy" mode of parsing that can be used to load bookmarks.json files (at least until we no longer support migration from Firefox <4 profiles :-) ). r=sayrer

This commit is contained in:
Jeff Walden 2010-07-14 13:48:36 -05:00
parent 0e03a3a79b
commit 04a95a7188
11 changed files with 258 additions and 68 deletions

View File

@ -1 +1 @@
{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]}]}
{"title":"","id":1,"dateAdded":1233157910552624,"lastModified":1233157955206833,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157993171424,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"title":"examplejson","id":27,"parent":2,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157972101126,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"}],"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"title":"examplejson","id":26,"parent":3,"dateAdded":1233157972101126,"lastModified":1233157984999673,"type":"text/x-moz-place","uri":"http://example.com/"}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157910582667,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1233157910552624,"lastModified":1233157911033315,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[]},]}

View File

@ -52,7 +52,7 @@ interface nsIScriptGlobalObject;
/**
* Encode and decode JSON text.
*/
[scriptable, uuid(6fcf09ee-87d0-42ec-a72a-8d60114e974f)]
[scriptable, uuid(a4d68b4e-0c0b-4c7c-b540-ef2f9834171f)]
interface nsIJSON : nsISupports
{
AString encode(/* in JSObject value */);
@ -71,4 +71,25 @@ interface nsIJSON : nsISupports
// Make sure you GCroot the result of this function before using it.
[noscript] jsval decodeToJSVal(in AString str, in JSContext cx);
/*
* Decode a JSON string, but also accept some strings in non-JSON format, as
* the decoding methods here did previously before tightening.
*
* This method is provided only as a temporary transition path for users of
* the old code who depended on the ability to decode leniently; new users
* should use the non-legacy decoding methods.
*
* @param str the string to parse
*/
void /* JSObject */ legacyDecode(in AString str);
/* Identical to legacyDecode, but decode the contents of stream. */
void /* JSObject */ legacyDecodeFromStream(in nsIInputStream stream,
in long contentLength);
/* Identical to legacyDecode, but decode into a jsval. */
// Make sure you GCroot the result of this function before using it.
[noscript] jsval legacyDecodeToJSVal(in AString str, in JSContext cx);
};

View File

@ -417,7 +417,7 @@ nsJSON::DecodeToJSVal(const nsAString &str, JSContext *cx, jsval *result)
// Since we've called JS_BeginJSONParse, we have to call JS_FinishJSONParse,
// even if JS_ConsumeJSONText fails. But if either fails, we'll report an
// error.
ok = ok && JS_FinishJSONParse(cx, parser, JSVAL_NULL);
ok &= JS_FinishJSONParse(cx, parser, JSVAL_NULL);
if (!ok) {
return NS_ERROR_UNEXPECTED;
@ -429,7 +429,8 @@ nsJSON::DecodeToJSVal(const nsAString &str, JSContext *cx, jsval *result)
nsresult
nsJSON::DecodeInternal(nsIInputStream *aStream,
PRInt32 aContentLength,
PRBool aNeedsConverter)
PRBool aNeedsConverter,
DecodingMode mode /* = STRICT */)
{
nsresult rv;
nsIXPConnect *xpc = nsContentUtils::XPConnect();
@ -464,7 +465,7 @@ nsJSON::DecodeInternal(nsIInputStream *aStream,
return NS_ERROR_FAILURE;
nsRefPtr<nsJSONListener>
jsonListener(new nsJSONListener(cx, retvalPtr, aNeedsConverter));
jsonListener(new nsJSONListener(cx, retvalPtr, aNeedsConverter, mode));
if (!jsonListener)
return NS_ERROR_OUT_OF_MEMORY;
@ -514,6 +515,52 @@ nsJSON::DecodeInternal(nsIInputStream *aStream,
return NS_OK;
}
NS_IMETHODIMP
nsJSON::LegacyDecode(const nsAString& json)
{
const PRUnichar *data;
PRUint32 len = NS_StringGetData(json, &data);
nsCOMPtr<nsIInputStream> stream;
nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream),
(const char*) data,
len * sizeof(PRUnichar),
NS_ASSIGNMENT_DEPEND);
NS_ENSURE_SUCCESS(rv, rv);
return DecodeInternal(stream, len, PR_FALSE, LEGACY);
}
NS_IMETHODIMP
nsJSON::LegacyDecodeFromStream(nsIInputStream *aStream, PRInt32 aContentLength)
{
return DecodeInternal(aStream, aContentLength, PR_TRUE, LEGACY);
}
NS_IMETHODIMP
nsJSON::LegacyDecodeToJSVal(const nsAString &str, JSContext *cx, jsval *result)
{
JSAutoRequest ar(cx);
JSONParser *parser = JS_BeginJSONParse(cx, result);
NS_ENSURE_TRUE(parser, NS_ERROR_UNEXPECTED);
JSBool ok = js_ConsumeJSONText(cx, parser,
(jschar*)PromiseFlatString(str).get(),
(uint32)str.Length(),
LEGACY);
// Since we've called JS_BeginJSONParse, we have to call JS_FinishJSONParse,
// even if js_ConsumeJSONText fails. But if either fails, we'll report an
// error.
ok &= JS_FinishJSONParse(cx, parser, JSVAL_NULL);
if (!ok) {
return NS_ERROR_UNEXPECTED;
}
return NS_OK;
}
nsresult
NS_NewJSON(nsISupports* aOuter, REFNSIID aIID, void** aResult)
{
@ -528,11 +575,13 @@ NS_NewJSON(nsISupports* aOuter, REFNSIID aIID, void** aResult)
}
nsJSONListener::nsJSONListener(JSContext *cx, jsval *rootVal,
PRBool needsConverter)
PRBool needsConverter,
DecodingMode mode /* = STRICT */)
: mNeedsConverter(needsConverter),
mJSONParser(nsnull),
mCx(cx),
mRootVal(rootVal)
mRootVal(rootVal),
mDecodingMode(mode)
{
}
@ -706,7 +755,8 @@ nsJSONListener::Consume(const PRUnichar* aBuffer, PRUint32 aByteLength)
if (!mJSONParser)
return NS_ERROR_FAILURE;
if (!JS_ConsumeJSONText(mCx, mJSONParser, (jschar*) aBuffer, aByteLength)) {
if (!js_ConsumeJSONText(mCx, mJSONParser, (jschar*) aBuffer, aByteLength,
mDecodingMode)) {
Cleanup();
return NS_ERROR_FAILURE;
}

View File

@ -40,6 +40,7 @@
#define nsJSON_h__
#include "jsapi.h"
#include "json.h"
#include "nsIJSON.h"
#include "nsString.h"
#include "nsCOMPtr.h"
@ -86,9 +87,11 @@ public:
protected:
nsresult EncodeInternal(nsJSONWriter *writer);
nsresult DecodeInternal(nsIInputStream *aStream,
PRInt32 aContentLength,
PRBool aNeedsConverter);
PRBool aNeedsConverter,
DecodingMode mode = STRICT);
nsCOMPtr<nsIURI> mURI;
};
@ -98,7 +101,8 @@ NS_NewJSON(nsISupports* aOuter, REFNSIID aIID, void** aResult);
class nsJSONListener : public nsIStreamListener
{
public:
nsJSONListener(JSContext *cx, jsval *rootVal, PRBool needsConverter);
nsJSONListener(JSContext *cx, jsval *rootVal, PRBool needsConverter,
DecodingMode mode);
virtual ~nsJSONListener();
NS_DECL_ISUPPORTS
@ -112,6 +116,7 @@ protected:
jsval *mRootVal;
nsCOMPtr<nsIUnicodeDecoder> mDecoder;
nsCString mSniffBuffer;
DecodingMode mDecodingMode;
nsresult ProcessBytes(const char* aBuffer, PRUint32 aByteLength);
nsresult ConsumeConverted(const char* aBuffer, PRUint32 aByteLength);
nsresult Consume(const PRUnichar *data, PRUint32 len);

View File

@ -988,37 +988,30 @@ HandleData(JSContext *cx, JSONParser *jp, JSONDataType type)
}
JSBool
js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len)
js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len,
DecodingMode decodingMode)
{
uint32 i;
CHECK_REQUEST(cx);
if (*jp->statep == JSON_PARSE_STATE_INIT) {
PushState(cx, jp, JSON_PARSE_STATE_VALUE);
}
for (i = 0; i < len; i++) {
for (uint32 i = 0; i < len; i++) {
jschar c = data[i];
switch (*jp->statep) {
case JSON_PARSE_STATE_VALUE:
case JSON_PARSE_STATE_ARRAY_INITIAL_VALUE:
if (c == ']') {
// empty array
if (!PopState(cx, jp))
return JS_FALSE;
if (*jp->statep != JSON_PARSE_STATE_ARRAY)
return JSONParseError(jp, cx);
JS_ASSERT(*jp->statep == JSON_PARSE_STATE_ARRAY_AFTER_ELEMENT);
if (!CloseArray(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
break;
}
// fall through if non-empty array or whitespace
if (c == '}') {
// we should only find these in OBJECT_KEY state
return JSONParseError(jp, cx);
}
case JSON_PARSE_STATE_VALUE:
if (c == '"') {
*jp->statep = JSON_PARSE_STATE_STRING;
break;
@ -1038,56 +1031,77 @@ js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len
break;
}
// fall through in case the value is an object or array
case JSON_PARSE_STATE_OBJECT_VALUE:
if (c == '{') {
*jp->statep = JSON_PARSE_STATE_OBJECT;
if (!OpenObject(cx, jp) || !PushState(cx, jp, JSON_PARSE_STATE_OBJECT_PAIR))
*jp->statep = JSON_PARSE_STATE_OBJECT_AFTER_PAIR;
if (!OpenObject(cx, jp) || !PushState(cx, jp, JSON_PARSE_STATE_OBJECT_INITIAL_PAIR))
return JS_FALSE;
} else if (c == '[') {
*jp->statep = JSON_PARSE_STATE_ARRAY;
if (!OpenArray(cx, jp) || !PushState(cx, jp, JSON_PARSE_STATE_VALUE))
*jp->statep = JSON_PARSE_STATE_ARRAY_AFTER_ELEMENT;
if (!OpenArray(cx, jp) || !PushState(cx, jp, JSON_PARSE_STATE_ARRAY_INITIAL_VALUE))
return JS_FALSE;
} else if (!JS_ISXMLSPACE(c)) {
return JSONParseError(jp, cx);
}
break;
case JSON_PARSE_STATE_OBJECT:
if (c == '}') {
if (!CloseObject(cx, jp) || !PopState(cx, jp))
} else if (JS_ISXMLSPACE(c)) {
// nothing to do
} else if (decodingMode == LEGACY && c == ']') {
if (!PopState(cx, jp))
return JS_FALSE;
} else if (c == ',') {
if (!PushState(cx, jp, JSON_PARSE_STATE_OBJECT_PAIR))
return JS_FALSE;
} else if (c == ']' || !JS_ISXMLSPACE(c)) {
return JSONParseError(jp, cx);
}
break;
case JSON_PARSE_STATE_ARRAY:
if (c == ']') {
JS_ASSERT(*jp->statep == JSON_PARSE_STATE_ARRAY_AFTER_ELEMENT);
if (!CloseArray(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
} else if (c == ',') {
} else {
return JSONParseError(jp, cx);
}
break;
case JSON_PARSE_STATE_ARRAY_AFTER_ELEMENT:
if (c == ',') {
if (!PushState(cx, jp, JSON_PARSE_STATE_VALUE))
return JS_FALSE;
} else if (c == ']') {
if (!CloseArray(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
} else if (!JS_ISXMLSPACE(c)) {
return JSONParseError(jp, cx);
}
break;
case JSON_PARSE_STATE_OBJECT_AFTER_PAIR:
if (c == ',') {
if (!PushState(cx, jp, JSON_PARSE_STATE_OBJECT_PAIR))
return JS_FALSE;
} else if (c == '}') {
if (!CloseObject(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
} else if (!JS_ISXMLSPACE(c)) {
return JSONParseError(jp, cx);
}
break;
case JSON_PARSE_STATE_OBJECT_INITIAL_PAIR:
if (c == '}') {
if (!PopState(cx, jp))
return JS_FALSE;
JS_ASSERT(*jp->statep == JSON_PARSE_STATE_OBJECT_AFTER_PAIR);
if (!CloseObject(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
break;
}
// fall through if non-empty object or whitespace
case JSON_PARSE_STATE_OBJECT_PAIR:
if (c == '"') {
// we want to be waiting for a : when the string has been read
*jp->statep = JSON_PARSE_STATE_OBJECT_IN_PAIR;
if (!PushState(cx, jp, JSON_PARSE_STATE_STRING))
return JS_FALSE;
} else if (c == '}') {
// pop off the object pair state and the object state
if (!CloseObject(cx, jp) || !PopState(cx, jp) || !PopState(cx, jp))
} else if (JS_ISXMLSPACE(c)) {
// nothing to do
} else if (decodingMode == LEGACY && c == '}') {
if (!PopState(cx, jp))
return JS_FALSE;
} else if (c == ']' || !JS_ISXMLSPACE(c)) {
JS_ASSERT(*jp->statep == JSON_PARSE_STATE_OBJECT_AFTER_PAIR);
if (!CloseObject(cx, jp) || !PopState(cx, jp))
return JS_FALSE;
} else {
return JSONParseError(jp, cx);
}
break;
@ -1114,7 +1128,7 @@ js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len
return JS_FALSE;
} else if (c == '\\') {
*jp->statep = JSON_PARSE_STATE_STRING_ESCAPE;
} else if (c < 31) {
} else if (c <= 0x1F) {
// The JSON lexical grammer does not allow a JSONStringCharacter to be
// any of the Unicode characters U+0000 thru U+001F (control characters).
return JSONParseError(jp, cx);

View File

@ -40,7 +40,10 @@
/*
* JS JSON functions.
*/
#include "jsscan.h"
#include "jsprvtd.h"
#include "jspubtd.h"
#include "jsvalue.h"
#include "jsvector.h"
#define JSON_MAX_DEPTH 2048
#define JSON_PARSER_BUFSIZE 1024
@ -64,23 +67,26 @@ enum JSONParserState {
/* JSON fully processed, expecting only trailing whitespace. */
JSON_PARSE_STATE_FINISHED,
/* Unused: to be removed in bug 564621. */
JSON_PARSE_STATE_OBJECT_VALUE,
/* Start of JSON value. */
JSON_PARSE_STATE_VALUE,
/* In object, at start of pair, at comma, or at closing brace. */
JSON_PARSE_STATE_OBJECT,
/* Start of first key/value pair in object, or at }. */
JSON_PARSE_STATE_OBJECT_INITIAL_PAIR,
/* At start of pair within object, or at closing brace. */
/* Start of subsequent key/value pair in object, after delimiting comma. */
JSON_PARSE_STATE_OBJECT_PAIR,
/* At : in key/value pair in object. */
JSON_PARSE_STATE_OBJECT_IN_PAIR,
/* In array, at start of element, at comma, or at closing bracket. */
JSON_PARSE_STATE_ARRAY,
/* Immediately after key/value pair in object: at , or }. */
JSON_PARSE_STATE_OBJECT_AFTER_PAIR,
/* Start of first element of array or at ]. */
JSON_PARSE_STATE_ARRAY_INITIAL_VALUE,
/* Immediately after element in array: at , or ]. */
JSON_PARSE_STATE_ARRAY_AFTER_ELEMENT,
/* The following states allow no leading whitespace. */
@ -113,8 +119,18 @@ struct JSONParser;
extern JSONParser *
js_BeginJSONParse(JSContext *cx, js::Value *rootVal, bool suppressErrors = false);
extern JSBool
js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len);
/*
* The type of JSON decoding to perform. Strict decoding is to-the-spec;
* legacy decoding accepts a few non-JSON syntaxes historically accepted by the
* implementation. (Full description of these deviations is deliberately
* omitted.) New users should use strict decoding rather than legacy decoding,
* as legacy decoding might be removed at a future time.
*/
enum DecodingMode { STRICT, LEGACY };
extern JS_FRIEND_API(JSBool)
js_ConsumeJSONText(JSContext *cx, JSONParser *jp, const jschar *data, uint32 len,
DecodingMode decodingMode = STRICT);
extern bool
js_FinishJSONParse(JSContext *cx, JSONParser *jp, const js::Value &reviver);

View File

@ -1,3 +1,5 @@
url-prefix ../../jsreftest.html?test=ecma_5/JSON/
script cyclic-stringify.js
script small-codepoints.js
script trailing-comma.js
script stringify-gap.js

View File

@ -0,0 +1,29 @@
gTestsubsuite='JSON';
function testJSON(str, expectSyntaxError)
{
try
{
JSON.parse(str);
reportCompare(false, expectSyntaxError,
"string <" + str + "> " +
"should" + (expectSyntaxError ? "n't" : "") + " " +
"have parsed as JSON");
}
catch (e)
{
if (!(e instanceof SyntaxError))
{
reportCompare(true, false,
"parsing string <" + str + "> threw a non-SyntaxError " +
"exception: " + e);
}
else
{
reportCompare(true, expectSyntaxError,
"string <" + str + "> " +
"should" + (expectSyntaxError ? "n't" : "") + " " +
"have parsed as JSON, exception: " + e);
}
}
}

View File

@ -0,0 +1,16 @@
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/
var gTestfile = 'small-codepoints.js';
//-----------------------------------------------------------------------------
var BUGNUMBER = 554079;
var summary = 'JSON.parse should reject U+0000 through U+001F';
print(BUGNUMBER + ": " + summary);
/**************
* BEGIN TEST *
**************/
for (var i = 0; i <= 0x1F; i++)
testJSON('["a' + String.fromCharCode(i) + 'c"]', true);

View File

@ -0,0 +1,32 @@
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/
var gTestfile = 'trailing-comma.js';
//-----------------------------------------------------------------------------
var BUGNUMBER = 564621;
var summary = 'JSON.parse should reject {"a" : "b",} or [1,]';
print(BUGNUMBER + ": " + summary);
/**************
* BEGIN TEST *
**************/
testJSON('[]', false);
testJSON('[1]', false);
testJSON('["a"]', false);
testJSON('{}', false);
testJSON('{"a":1}', false);
testJSON('{"a":"b"}', false);
testJSON('{"a":true}', false);
testJSON('[{}]', false);
testJSON('[1,]', true);
testJSON('["a",]', true);
testJSON('{,}', true);
testJSON('{"a":1,}', true);
testJSON('{"a":"b",}', true);
testJSON('{"a":true,}', true);
testJSON('[{,}]', true);
testJSON('[[1,]]', true);
testJSON('[{"a":"b",}]', true);

View File

@ -722,8 +722,13 @@ var PlacesUtils = {
case this.TYPE_X_MOZ_PLACE:
case this.TYPE_X_MOZ_PLACE_SEPARATOR:
case this.TYPE_X_MOZ_PLACE_CONTAINER:
var JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
nodes = JSON.decode("[" + blob + "]");
var json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
// Old profiles (pre-Firefox 4) may contain bookmarks.json files with
// trailing commas, which we once accepted but no longer do -- except
// when decoded using the legacy decoder. This can be reverted to
// json.decode (better yet, to the ECMA-standard JSON.parse) when we no
// longer support upgrades from pre-Firefox 4 profiles.
nodes = json.legacyDecode("[" + blob + "]");
break;
case this.TYPE_X_MOZ_URL:
var parts = blob.split("\n");