From 43999e482bf703c85f81e7dcc371785e7397bd35 Mon Sep 17 00:00:00 2001 From: "sayrer@gmail.com" Date: Thu, 27 Dec 2007 13:34:03 -0800 Subject: [PATCH] Bug 387522. Native JSON support. r=crowder/jst, sr=brendan --- dom/public/idl/Makefile.in | 1 + dom/public/idl/json/Makefile.in | 57 + dom/public/idl/json/nsIJSON.idl | 66 + dom/src/Makefile.in | 2 +- dom/src/json/Makefile.in | 80 ++ dom/src/json/nsJSON.cpp | 1245 +++++++++++++++++ dom/src/json/nsJSON.h | 164 +++ dom/src/json/test/Makefile.in | 49 + dom/src/json/test/json2.js | 263 ++++ dom/src/json/test/pass1.json | 58 + dom/src/json/test/pass3.json | 7 + dom/src/json/test/unit/head_json.js | 19 + dom/src/json/test/unit/test_decode.js | 161 +++ dom/src/json/test/unit/test_encode.js | 237 ++++ js/src/Makefile.in | 1 + js/src/jsiter.c | 6 +- js/src/jsiter.h | 6 +- layout/build/Makefile.in | 2 + layout/build/nsLayoutCID.h | 3 + layout/build/nsLayoutModule.cpp | 6 + layout/style/nsCSSLoader.cpp | 59 +- .../test-harness/xpcshell-simple/test_all.sh | 2 +- 22 files changed, 2449 insertions(+), 45 deletions(-) create mode 100644 dom/public/idl/json/Makefile.in create mode 100644 dom/public/idl/json/nsIJSON.idl create mode 100644 dom/src/json/Makefile.in create mode 100644 dom/src/json/nsJSON.cpp create mode 100644 dom/src/json/nsJSON.h create mode 100644 dom/src/json/test/Makefile.in create mode 100644 dom/src/json/test/json2.js create mode 100644 dom/src/json/test/pass1.json create mode 100644 dom/src/json/test/pass3.json create mode 100644 dom/src/json/test/unit/head_json.js create mode 100644 dom/src/json/test/unit/test_decode.js create mode 100644 dom/src/json/test/unit/test_encode.js diff --git a/dom/public/idl/Makefile.in b/dom/public/idl/Makefile.in index 7352bb2e713..e0f8dbb6299 100644 --- a/dom/public/idl/Makefile.in +++ b/dom/public/idl/Makefile.in @@ -58,6 +58,7 @@ DIRS = \ ls \ xul \ storage \ + json \ offline ifdef MOZ_SVG diff --git a/dom/public/idl/json/Makefile.in b/dom/public/idl/json/Makefile.in new file mode 100644 index 00000000000..386233ac765 --- /dev/null +++ b/dom/public/idl/json/Makefile.in @@ -0,0 +1,57 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is Mozilla Corporation. +# Portions created by the Initial Developer are Copyright (C) 2007 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Robert Sayre +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +XPIDL_MODULE = dom_json +GRE_MODULE = 1 + +XPIDLSRCS = \ + nsIJSON.idl \ + $(NULL) + +SDK_XPIDLSRCS = \ + nsIJSON.idl \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/dom/public/idl/json/nsIJSON.idl b/dom/public/idl/json/nsIJSON.idl new file mode 100644 index 00000000000..c4a4370451c --- /dev/null +++ b/dom/public/idl/json/nsIJSON.idl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Robert Sayre + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "domstubs.idl" + +interface nsIInputStream; +interface nsIOutputStream; +interface nsIScriptGlobalObject; + +/** + * Encode and decode JSON text. + */ +[scriptable, uuid(45464c36-efde-4cb5-8e00-07480533ff35)] +interface nsIJSON : nsISupports +{ + AString encode(/* in JSObject value, + /* [optional] in JSObject whitelist */); + + void encodeToStream(in nsIOutputStream stream, + in string charset, + in boolean writeBOM + /* in JSObject value, + /* [optional] in JSObject optFilter */); + + void /* JSObject */ decode(in AString str + /* , [optional] in JSObject whitelist */); + + void /* JSObject */ decodeFromStream(in nsIInputStream stream, + in long contentLength + /*[optional] in JSObject optFilter */); +}; diff --git a/dom/src/Makefile.in b/dom/src/Makefile.in index 7d95e0e96e6..d5f2a792e15 100644 --- a/dom/src/Makefile.in +++ b/dom/src/Makefile.in @@ -42,7 +42,7 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk -DIRS = base jsurl events storage offline +DIRS = base jsurl events storage offline json include $(topsrcdir)/config/rules.mk diff --git a/dom/src/json/Makefile.in b/dom/src/json/Makefile.in new file mode 100644 index 00000000000..fc532d18e03 --- /dev/null +++ b/dom/src/json/Makefile.in @@ -0,0 +1,80 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is Mozilla Corporation. +# Portions created by the Initial Developer are Copyright (C) 2007 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Robert Sayre +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +LIBRARY_NAME = json_s +LIBXUL_LIBRARY = 1 + +REQUIRES = xpcom \ + string \ + content \ + caps \ + js \ + locale \ + layout \ + necko \ + pref \ + uconv \ + unicharutil \ + widget \ + xpconnect \ + $(NULL) + +CPPSRCS = \ + nsJSON.cpp \ + $(NULL) + +FORCE_STATIC_LIB = 1 + +LOCAL_INCLUDES = \ + -I$(srcdir)/../base \ + -I$(topsrcdir)/content/events/src + +DEFINES += -D_IMPL_NS_LAYOUT + +ifdef ENABLE_TESTS + DIRS += test +endif + +include $(topsrcdir)/config/rules.mk diff --git a/dom/src/json/nsJSON.cpp b/dom/src/json/nsJSON.cpp new file mode 100644 index 00000000000..1582d46fb31 --- /dev/null +++ b/dom/src/json/nsJSON.cpp @@ -0,0 +1,1245 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dave Camp + * Robert Sayre + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "jsapi.h" +#include "jsdtoa.h" +#include "jsnum.h" +#include "jsbool.h" +#include "jsarena.h" +#include "jscntxt.h" +#include "jsinterp.h" +#include "jsiter.h" +#include "jstypes.h" +#include "nsIServiceManager.h" +#include "nsJSON.h" +#include "nsIXPConnect.h" +#include "nsIXPCScriptable.h" +#include "nsStreamUtils.h" +#include "nsIInputStream.h" +#include "nsStringStream.h" +#include "nsICharsetConverterManager.h" +#include "nsXPCOMStrings.h" +#include "nsNetUtil.h" +#include "nsContentUtils.h" +#include "nsCRTGlue.h" +#include "nsAutoPtr.h" + +static const char kXPConnectServiceCID[] = "@mozilla.org/js/xpc/XPConnect;1"; + +NS_INTERFACE_MAP_BEGIN(nsJSON) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIJSON) + NS_INTERFACE_MAP_ENTRY(nsIJSON) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsJSON) +NS_IMPL_RELEASE(nsJSON) + +nsJSON::nsJSON() +{ +} + +nsJSON::~nsJSON() +{ +} + +// +// AString encode(in JSObject value, [optional] in JSObject whitelist); +// +NS_IMETHODIMP +nsJSON::Encode(nsAString &aJSON) +{ + // This function should only be called from JS. + nsresult rv; + + nsAutoPtr writer(new nsJSONWriter()); + if (!writer) + return NS_ERROR_OUT_OF_MEMORY; + + rv = EncodeInternal(writer); + + // FIXME: bug 408838. Get exception types sorted out + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_INVALID_ARG) { + rv = NS_OK; + // if we didn't consume anything, it's not JSON, so return null + if (writer->mBuffer.IsEmpty()) { + aJSON.Truncate(); + aJSON.SetIsVoid(PR_TRUE); + } else { + aJSON.Append(writer->mBuffer); + } + } + + return rv; +} + +static const char UTF8BOM[] = "\xEF\xBB\xBF"; +static const char UTF16LEBOM[] = "\xFF\xFE"; +static const char UTF16BEBOM[] = "\xFE\xFF"; +static const char UTF32LEBOM[] = "\xFF\xFE\0\0"; +static const char UTF32BEBOM[] = "\0\0\xFE\xFF"; + +static nsresult CheckCharset(const char* aCharset) +{ + // Check that the charset is permissible + if (!(strcmp(aCharset, "UTF-8") == 0 || + strcmp(aCharset, "UTF-16LE") == 0 || + strcmp(aCharset, "UTF-16BE") == 0 || + strcmp(aCharset, "UTF-32LE") == 0 || + strcmp(aCharset, "UTF-32BE") == 0)) { + return NS_ERROR_INVALID_ARG; + } + + return NS_OK; +} + +// +// void EncodeToStream(in nsIOutputStream stream +// /* in JSObject value, +// /* [optional] in JSObject whitelist */); +// +NS_IMETHODIMP +nsJSON::EncodeToStream(nsIOutputStream *aStream, + const char* aCharset, + const PRBool aWriteBOM) +{ + // This function should only be called from JS. + NS_ENSURE_ARG(aStream); + nsresult rv; + + rv = CheckCharset(aCharset); + NS_ENSURE_SUCCESS(rv, rv); + + // Check to see if we have a buffered stream + nsCOMPtr bufferedStream; + // FIXME: bug 408514. + // NS_OutputStreamIsBuffered(aStream) asserts on file streams... + //if (!NS_OutputStreamIsBuffered(aStream)) { + rv = NS_NewBufferedOutputStream(getter_AddRefs(bufferedStream), + aStream, 4096); + NS_ENSURE_SUCCESS(rv, rv); + // aStream = bufferedStream; + //} + + PRUint32 ignored; + if (aWriteBOM) { + if (strcmp(aCharset, "UTF-8") == 0) + rv = aStream->Write(UTF8BOM, 3, &ignored); + else if (strcmp(aCharset, "UTF-16LE") == 0) + rv = aStream->Write(UTF16LEBOM, 2, &ignored); + else if (strcmp(aCharset, "UTF-16BE") == 0) + rv = aStream->Write(UTF16BEBOM, 2, &ignored); + else if (strcmp(aCharset, "UTF-32LE") == 0) + rv = aStream->Write(UTF32LEBOM, 4, &ignored); + else if (strcmp(aCharset, "UTF-32BE") == 0) + rv = aStream->Write(UTF32BEBOM, 4, &ignored); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoPtr writer(new nsJSONWriter(bufferedStream)); + if (!writer) + return NS_ERROR_OUT_OF_MEMORY; + rv = writer->SetCharset(aCharset); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EncodeInternal(writer); + NS_ENSURE_SUCCESS(rv, rv); + + rv = bufferedStream->Flush(); + + return rv; +} + +nsresult +nsJSON::EncodeInternal(nsJSONWriter *writer) +{ + nsresult rv; + nsIXPConnect *xpc = nsContentUtils::XPConnect(); + if (!xpc) + return NS_ERROR_FAILURE; + + nsCOMPtr cc; + rv = xpc->GetCurrentNativeCallContext(getter_AddRefs(cc)); + NS_ENSURE_SUCCESS(rv, rv); + + JSContext *cx = nsnull; + rv = cc->GetJSContext(&cx); + NS_ENSURE_SUCCESS(rv, rv); + + JSAutoRequest ar(cx); + + PRUint32 argc = 0; + rv = cc->GetArgc(&argc); + NS_ENSURE_SUCCESS(rv, rv); + + // Now fish for the JS arguments. If it's a call to encode, we'll + // want the first two arguments. If it's a call to encodeToStream, + // we'll want the fourth and fifth; + PRUint32 firstArg = writer->mStream ? 3 : 0; + + // Get the object we're going to serialize. + JSObject *inputObj = nsnull; + jsval *argv = nsnull; + rv = cc->GetArgvPtr(&argv); + NS_ENSURE_SUCCESS(rv, rv); + + if (argc <= firstArg || + !(JSVAL_IS_OBJECT(argv[firstArg]) && + (inputObj = JSVAL_TO_OBJECT(argv[firstArg])))) { + // return if it's not something we can deal with + return NS_ERROR_INVALID_ARG; + } + + JSObject *whitelist = nsnull; + + // If there's a second argument here, it should be an array. + if (argc >= firstArg + 2 && + !(JSVAL_IS_OBJECT(argv[firstArg + 1]) && + (whitelist = JSVAL_TO_OBJECT(argv[firstArg + 1])) && + JS_IsArrayObject(cx, whitelist))) { + whitelist = nsnull; // bogus whitelists are ignored + } + + jsval *vp = &argv[firstArg]; + + JSBool ok = ToJSON(cx, vp); + JSType type; + if (!(ok && !JSVAL_IS_PRIMITIVE(*vp) && + (type = JS_TypeOfValue(cx, *vp)) != JSTYPE_FUNCTION && + type != JSTYPE_XML)) { + return NS_ERROR_INVALID_ARG; + } + + return EncodeObject(cx, vp, writer, whitelist, 0); +} + +// N.B: vp must be rooted. +nsresult +nsJSON::EncodeObject(JSContext *cx, jsval *vp, nsJSONWriter *writer, + JSObject *whitelist, PRUint32 depth) +{ + NS_ENSURE_ARG(vp); + NS_ENSURE_ARG(writer); + + if (depth > JSON_MAX_DEPTH) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + JSObject *obj = JSVAL_TO_OBJECT(*vp); + JSBool isArray = JS_IsArrayObject(cx, obj); + PRUnichar output = PRUnichar(isArray ? '[' : '{'); + rv = writer->Write(&output, 1); + NS_ENSURE_SUCCESS(rv, rv); + + JSBool ok = JS_TRUE; + + ok = js_ValueToIterator(cx, JSITER_ENUMERATE, vp); + if (!ok) + return NS_ERROR_FAILURE; + + JSObject *iterObj = JSVAL_TO_OBJECT(*vp); + + jsval outputValue = JSVAL_VOID; + JSAutoTempValueRooter tvr(cx, 1, &outputValue); + + jsval key; + PRBool memberWritten = PR_FALSE; + do { + outputValue = JSVAL_VOID; + ok = js_CallIteratorNext(cx, iterObj, &key); + + if (!ok) + break; + + if (key == JSVAL_HOLE) + break; + + JSString *ks; + if (JSVAL_IS_STRING(key)) { + ks = JSVAL_TO_STRING(key); + } else { + ks = JS_ValueToString(cx, key); + if (!ks) { + ok = JS_FALSE; + break; + } + } + + ok = JS_GetUCProperty(cx, obj, JS_GetStringChars(ks), + JS_GetStringLength(ks), &outputValue); + if (!ok) + break; + + // if this is an array, holes are transmitted as null + if (isArray && outputValue == JSVAL_VOID) { + outputValue = JSVAL_NULL; + } else if (JSVAL_IS_OBJECT(outputValue)) { + ok = ToJSON(cx, &outputValue); + if (!ok) + break; + } + + // elide undefined values + if (outputValue == JSVAL_VOID) + continue; + + // output a comma unless this is the first member to write + if (memberWritten) { + output = PRUnichar(','); + rv = writer->Write(&output, 1); + } + memberWritten = PR_TRUE; + + JSType type = JS_TypeOfValue(cx, outputValue); + + // Can't encode these types, so drop them + if (type == JSTYPE_FUNCTION || type == JSTYPE_XML) + break; + + // Be careful below, this string is weakly rooted. + JSString *s; + + // If this isn't an array, we need to output a key + if (!isArray) { + nsAutoString keyOutput; + s = JS_ValueToString(cx, key); + if (!s) { + ok = JS_FALSE; + break; + } + + rv = writer->WriteString((PRUnichar*)JS_GetStringChars(s), + JS_GetStringLength(s)); + if (NS_FAILED(rv)) + break; + output = PRUnichar(':'); + rv = writer->Write(&output, 1); + if (NS_FAILED(rv)) + break; + } + + if (!JSVAL_IS_PRIMITIVE(outputValue)) { + // recurse + rv = EncodeObject(cx, &outputValue, writer, whitelist, depth + 1); + if (NS_FAILED(rv)) + break; + } else { + nsAutoString valueOutput; + s = JS_ValueToString(cx, outputValue); + if (!s) { + ok = JS_FALSE; + break; + } + + if (type == JSTYPE_STRING) { + rv = writer->WriteString((PRUnichar*)JS_GetStringChars(s), + JS_GetStringLength(s)); + continue; + } + + if (type == JSTYPE_NUMBER) { + if (JSVAL_IS_DOUBLE(outputValue)) { + jsdouble d = *JSVAL_TO_DOUBLE(outputValue); + if (!JSDOUBLE_IS_FINITE(d)) + valueOutput.Append(NS_LITERAL_STRING("null")); + else + valueOutput.Append((PRUnichar*)JS_GetStringChars(s)); + } else { + valueOutput.Append((PRUnichar*)JS_GetStringChars(s)); + } + } else if (type == JSTYPE_BOOLEAN) { + valueOutput.Append((PRUnichar*)JS_GetStringChars(s)); + } else if (JSVAL_IS_NULL(outputValue)) { + valueOutput.Append(NS_LITERAL_STRING("null")); + } else { + rv = NS_ERROR_FAILURE; // encoding error + break; + } + + rv = writer->Write(valueOutput.get(), valueOutput.Length()); + } + + } while (NS_SUCCEEDED(rv)); + + // Always close the iterator, but make sure not to stomp on OK + ok &= js_CloseIterator(cx, *vp); + + if (!ok) + rv = NS_ERROR_FAILURE; // encoding error or propagate? FIXME: Bug 408838. + NS_ENSURE_SUCCESS(rv, rv); + + output = PRUnichar(isArray ? ']' : '}'); + rv = writer->Write(&output, 1); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +JSBool +nsJSON::ToJSON(JSContext *cx, jsval *vp) +{ + // Now we check to see whether the return value implements toJSON() + JSBool ok = JS_TRUE; + char *toJSON = "toJSON"; + + if (!JSVAL_IS_PRIMITIVE(*vp)) { + JSObject *obj = JSVAL_TO_OBJECT(*vp); + jsval toJSONVal = nsnull; + ok = JS_GetProperty(cx, obj, toJSON, &toJSONVal); + if (ok && JS_TypeOfValue(cx, toJSONVal) == JSTYPE_FUNCTION) { + ok = JS_CallFunctionValue(cx, obj, toJSONVal, 0, nsnull, vp); + } + } + + return ok; +} + +nsJSONWriter::nsJSONWriter() : mStream(nsnull), mEncoder(nsnull) +{ +} + +nsJSONWriter::nsJSONWriter(nsIOutputStream *aStream) : mStream(aStream), + mEncoder(nsnull) +{ +} + +nsJSONWriter::~nsJSONWriter() +{ +} + +nsresult +nsJSONWriter::SetCharset(const char* aCharset) +{ + nsresult rv = NS_OK; + if (mStream) { + nsCOMPtr ccm = + do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = ccm->GetUnicodeEncoder(aCharset, getter_AddRefs(mEncoder)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mEncoder->SetOutputErrorBehavior(nsIUnicodeEncoder::kOnError_Signal, + nsnull, nsnull); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +static const PRUnichar quote = PRUnichar('"'); +static const PRUnichar backslash = PRUnichar('\\'); +static const PRUnichar unicodeEscape[] = {'\\', 'u', '0', '0', '\0'}; + +nsresult +nsJSONWriter::WriteString(const PRUnichar *aBuffer, PRUint32 aLength) +{ + nsresult rv; + rv = Write("e, 1); + NS_ENSURE_SUCCESS(rv, rv); + + PRUint32 mark = 0; + PRUint32 i; + for (i = 0; i < aLength; ++i) { + if (aBuffer[i] == quote || aBuffer[i] == backslash) { + rv = Write(&aBuffer[mark], i - mark); + NS_ENSURE_SUCCESS(rv, rv); + rv = Write(&backslash, 1); + NS_ENSURE_SUCCESS(rv, rv); + rv = Write(&aBuffer[i], 1); + NS_ENSURE_SUCCESS(rv, rv); + mark = i + 1; + } else if (aBuffer[i] <= 31 || aBuffer[i] == 127) { + rv = Write(&aBuffer[mark], i - mark); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString unicode; + unicode.Append(unicodeEscape); + nsAutoString charCode; + charCode.AppendInt(aBuffer[i], 16); + if (charCode.Length() == 1) + unicode.Append('0'); + unicode.Append(charCode); + rv = Write(unicode.get(), unicode.Length()); + NS_ENSURE_SUCCESS(rv, rv); + mark = i + 1; + } + } + + if (mark < aLength) { + rv = Write(&aBuffer[mark], aLength - mark); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = Write("e, 1); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +nsresult +nsJSONWriter::Write(const PRUnichar *aBuffer, PRUint32 aLength) +{ + nsresult rv = NS_OK; + if (mStream) { + rv = WriteToStream(mStream, mEncoder, aBuffer, aLength); + } else { + mBuffer.Append(aBuffer, aLength); + } + + return rv; +} + +nsresult +nsJSONWriter::WriteToStream(nsIOutputStream *aStream, + nsIUnicodeEncoder *encoder, + const PRUnichar *aBuffer, + PRUint32 aLength) +{ + nsresult rv; + PRInt32 srcLength = aLength; + PRUint32 bytesWritten; + + // The bytes written to the stream might differ from the PRUnichar size + PRInt32 aDestLength; + rv = encoder->GetMaxLength(aBuffer, srcLength, &aDestLength); + NS_ENSURE_SUCCESS(rv, rv); + + // create the buffer we need + char* destBuf = (char *) NS_Alloc(aDestLength); + if (!destBuf) + return NS_ERROR_OUT_OF_MEMORY; + + rv = encoder->Convert(aBuffer, &srcLength, destBuf, &aDestLength); + if (NS_SUCCEEDED(rv)) + rv = aStream->Write(destBuf, aDestLength, &bytesWritten); + + NS_Free(destBuf); + + return rv; +} + +NS_IMETHODIMP +nsJSON::Decode(const nsAString& json) +{ + const PRUnichar *data; + PRUint32 len = NS_StringGetData(json, &data); + nsCOMPtr 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); +} + +NS_IMETHODIMP +nsJSON::DecodeFromStream(nsIInputStream *aStream, PRInt32 aContentLength) +{ + return DecodeInternal(aStream, aContentLength, PR_TRUE); +} + +nsresult +nsJSON::DecodeInternal(nsIInputStream *aStream, + PRInt32 aContentLength, + PRBool aNeedsConverter) +{ + nsresult rv; + nsIXPConnect *xpc = nsContentUtils::XPConnect(); + if (!xpc) + return NS_ERROR_FAILURE; + + nsCOMPtr cc; + rv = xpc->GetCurrentNativeCallContext(getter_AddRefs(cc)); + NS_ENSURE_SUCCESS(rv, rv); + + jsval *retvalPtr; + rv = cc->GetRetValPtr(&retvalPtr); + NS_ENSURE_SUCCESS(rv, rv); + + JSContext *cx = nsnull; + rv = cc->GetJSContext(&cx); + NS_ENSURE_SUCCESS(rv, rv); + + JSAutoRequest ar(cx); + + // Consume the stream + nsCOMPtr jsonChannel; + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("about:blank"), 0, 0 ); + if (!uri) + return NS_ERROR_OUT_OF_MEMORY; + rv = NS_NewInputStreamChannel(getter_AddRefs(jsonChannel), uri, aStream, + NS_LITERAL_CSTRING("application/json")); + if (!jsonChannel || NS_FAILED(rv)) + return NS_ERROR_FAILURE; + + nsRefPtr + jsonListener(new nsJSONListener(cx, retvalPtr, aNeedsConverter)); + + if (!jsonListener) + return NS_ERROR_OUT_OF_MEMORY; + + //XXX this stream pattern should be consolidated in netwerk + rv = jsonListener->OnStartRequest(jsonChannel, nsnull); + if (NS_FAILED(rv)) { + jsonChannel->Cancel(rv); + return rv; + } + + nsresult status; + jsonChannel->GetStatus(&status); + PRUint32 offset = 0; + while (NS_SUCCEEDED(status)) { + PRUint32 available; + rv = aStream->Available(&available); + if (rv == NS_BASE_STREAM_CLOSED) { + rv = NS_OK; + break; + } + if (NS_FAILED(rv)) { + jsonChannel->Cancel(rv); + break; + } + if (!available) + break; // blocking input stream has none available when done + + rv = jsonListener->OnDataAvailable(jsonChannel, nsnull, + aStream, offset, available); + if (NS_FAILED(rv)) { + jsonChannel->Cancel(rv); + break; + } + + offset += available; + jsonChannel->GetStatus(&status); + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = jsonListener->OnStopRequest(jsonChannel, nsnull, status); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cc->SetReturnValueWasSet(PR_TRUE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +NS_NewJSON(nsISupports* aOuter, REFNSIID aIID, void** aResult) +{ + nsJSON* json = new nsJSON(); + if (!json) + return NS_ERROR_OUT_OF_MEMORY; + + NS_ADDREF(json); + *aResult = json; + + return NS_OK; +} + + +JS_STATIC_DLL_CALLBACK(void) +trace_json_stack(JSTracer *trc, JSTempValueRooter *tvr) +{ + nsJSONObjectStack *tmp = static_cast(tvr); + + for (PRUint32 i = 0; i < tmp->Length(); ++i) { + JS_CALL_OBJECT_TRACER(trc, tmp->ElementAt(i), + "JSON decoder stack member"); + } +} + +nsJSONListener::nsJSONListener(JSContext *cx, jsval *rootVal, + PRBool needsConverter) + : mLineNum(0), + mColumn(0), + mHexChar(0), + mNumHex(0), + mCx(cx), + mRootVal(rootVal), + mNeedsConverter(needsConverter), + mStatep(mStateStack) +{ + NS_ASSERTION(mCx, "Must have a JSContext"); + *mStatep = JSON_PARSE_STATE_INIT; + JS_PUSH_TEMP_ROOT_TRACE(cx, trace_json_stack, &mObjectStack); +} + +nsJSONListener::~nsJSONListener() +{ + JS_POP_TEMP_ROOT(mCx, &mObjectStack); +} + +NS_INTERFACE_MAP_BEGIN(nsJSONListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsJSONListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(nsJSONListener) +NS_IMPL_RELEASE(nsJSONListener) + +NS_IMETHODIMP +nsJSONListener::OnStartRequest(nsIRequest *aRequest, nsISupports *aContext) +{ + mHexChar = 0; + mNumHex = 0; + mSniffBuffer.Truncate(); + mDecoder = nsnull; + return NS_OK; +} + +NS_IMETHODIMP +nsJSONListener::OnStopRequest(nsIRequest *aRequest, nsISupports *aContext, + nsresult aStatusCode) +{ + nsresult rv; + + // This can happen with short UTF-8 messages + if (!mSniffBuffer.IsEmpty()) { + rv = ProcessBytes(mSniffBuffer.get(), mSniffBuffer.Length()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!mObjectStack.IsEmpty() || *mStatep != JSON_PARSE_STATE_FINISHED) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +NS_IMETHODIMP +nsJSONListener::OnDataAvailable(nsIRequest *aRequest, nsISupports *aContext, + nsIInputStream *aStream, + PRUint32 aOffset, PRUint32 aLength) +{ + PRUint32 contentLength; + aStream->Available(&contentLength); + nsresult rv; + + if (mNeedsConverter && mSniffBuffer.Length() < 4) { + PRUint32 readCount = (aLength < 4) ? aLength : 4; + rv = NS_ConsumeStream(aStream, readCount, mSniffBuffer); + NS_ENSURE_SUCCESS(rv, rv); + + if (mSniffBuffer.Length() < 4) + return NS_OK; + } + + char buffer[JSON_PARSER_BUFSIZE]; + unsigned long bytesRemaining = aLength - mSniffBuffer.Length(); + while (bytesRemaining) { + unsigned int bytesRead; + rv = aStream->Read(buffer, + PR_MIN(sizeof(buffer), bytesRemaining), + &bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + rv = ProcessBytes(buffer, bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + bytesRemaining -= bytesRead; + } + + return rv; +} + +nsresult +nsJSONListener::ProcessBytes(const char* aBuffer, PRUint32 aByteLength) +{ + nsresult rv; + // Check for BOM, or sniff charset + nsCAutoString charset; + if (mNeedsConverter && !mDecoder) { + if (!nsContentUtils::CheckForBOM((const unsigned char*) mSniffBuffer.get(), + mSniffBuffer.Length(), charset)) { + // OK, found no BOM, sniff the first character to see what this is + // See section 3 of RFC4627 for details on why this works. + const char *buffer = mSniffBuffer.get(); + if (mSniffBuffer.Length() >= 4) { + if (buffer[0] == 0x00 && buffer[1] == 0x00 && + buffer[2] == 0x00 && buffer[3] != 0x00) { + charset = "UTF-32BE"; + } else if (buffer[0] == 0x00 && buffer[1] != 0x00 && + buffer[2] == 0x00 && buffer[3] != 0x00) { + charset = "UTF-16BE"; + } else if (buffer[0] != 0x00 && buffer[1] == 0x00 && + buffer[2] == 0x00 && buffer[3] == 0x00) { + charset = "UTF-32LE"; + } else if (buffer[0] != 0x00 && buffer[1] == 0x00 && + buffer[2] != 0x00 && buffer[3] == 0x00) { + charset = "UTF-16LE"; + } else if (buffer[0] != 0x00 && buffer[1] != 0x00 && + buffer[2] != 0x00 && buffer[3] != 0x00) { + charset = "UTF-8"; + } + } + } + + // We should have a unicode charset by now + rv = CheckCharset(charset.get()); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr ccm = + do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = ccm->GetUnicodeDecoderRaw(charset.get(), getter_AddRefs(mDecoder)); + NS_ENSURE_SUCCESS(rv, rv); + + // consume the sniffed bytes + rv = ConsumeConverted(mSniffBuffer.get(), mSniffBuffer.Length()); + NS_ENSURE_SUCCESS(rv, rv); + mSniffBuffer.Truncate(); + } + + if (mNeedsConverter) { + rv = ConsumeConverted(aBuffer, aByteLength); + } else { + PRUint32 unichars = aByteLength / sizeof(PRUnichar); + rv = Consume((PRUnichar *) aBuffer, unichars); + } + + return rv; +} + +nsresult +nsJSONListener::ConsumeConverted(const char* aBuffer, PRUint32 aByteLength) +{ + nsresult rv; + PRInt32 unicharLength = 0; + PRInt32 srcLen = aByteLength; + + rv = mDecoder->GetMaxLength(aBuffer, srcLen, &unicharLength); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoArrayPtr ustr(new PRUnichar[unicharLength]); + NS_ENSURE_TRUE(ustr, NS_ERROR_OUT_OF_MEMORY); + rv = mDecoder->Convert(aBuffer, &srcLen, ustr, &unicharLength); + NS_ENSURE_SUCCESS(rv, rv); + + rv = Consume(ustr.get(), unicharLength); + + return rv; +} + +nsresult +nsJSONListener::PopState() +{ + mStatep--; + if (mStatep < mStateStack) { + mStatep = mStateStack; + return NS_ERROR_FAILURE; + } else if (*mStatep == JSON_PARSE_STATE_INIT) { + *mStatep = JSON_PARSE_STATE_FINISHED; + } + + return NS_OK; +} + +nsresult +nsJSONListener::PushState(JSONParserState state) +{ + if (*mStatep == JSON_PARSE_STATE_FINISHED) + return NS_ERROR_FAILURE; // extra input + + mStatep++; + if ((uint32)(mStatep - mStateStack) >= JS_ARRAY_LENGTH(mStateStack)) + return NS_ERROR_FAILURE; // too deep + + *mStatep = state; + + return NS_OK; +} + +nsresult +nsJSONListener::Consume(const PRUnichar *data, PRUint32 len) +{ + nsresult rv; + nsString numchars = NS_LITERAL_STRING("-+0123456789eE."); + PRUint32 i; + + if (*mStatep == JSON_PARSE_STATE_INIT) { + PushState(JSON_PARSE_STATE_VALUE); + } + + for (i = 0; i < len; i++) { + PRUnichar c = data[i]; + if (c == '\n') { + mLineNum++; + mColumn = 0; + } else { + mColumn++; + } + + switch (*mStatep) { + case JSON_PARSE_STATE_VALUE : + if (c == '{') { + *mStatep = JSON_PARSE_STATE_OBJECT; + rv = this->OpenObject(); + NS_ENSURE_SUCCESS(rv, rv); + rv = PushState(JSON_PARSE_STATE_OBJECT_PAIR); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == '[') { + *mStatep = JSON_PARSE_STATE_ARRAY; + rv = this->OpenArray(); + NS_ENSURE_SUCCESS(rv, rv); + rv = PushState(JSON_PARSE_STATE_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == ']') { + // empty array + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + if (*mStatep != JSON_PARSE_STATE_ARRAY) { + return NS_ERROR_FAILURE; // unexpected char + } + rv = this->CloseArray(); + NS_ENSURE_SUCCESS(rv, rv); + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == '}') { + // we should only find these in OBJECT_KEY state + return NS_ERROR_FAILURE; // unexpected failure + } else if (c == '"') { + *mStatep = JSON_PARSE_STATE_STRING; + } else if (numchars.FindChar(c) >= 0) { + *mStatep = JSON_PARSE_STATE_NUMBER; + mStringBuffer.Append(c); + } else if (NS_IsAsciiAlpha(c)) { + *mStatep = JSON_PARSE_STATE_KEYWORD; + mStringBuffer.Append(c); + } else if (!NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // unexpected + } + break; + case JSON_PARSE_STATE_OBJECT : + if (c == '}') { + rv = this->CloseObject(); + NS_ENSURE_SUCCESS(rv, rv); + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == ',') { + rv = PushState(JSON_PARSE_STATE_OBJECT_PAIR); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == ']') { + return NS_ERROR_FAILURE; // unexpected + } else if (!NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // unexpected + } + break; + case JSON_PARSE_STATE_ARRAY : + if (c == ']') { + rv = this->CloseArray(); + NS_ENSURE_SUCCESS(rv, rv); + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == ',') { + rv = PushState(JSON_PARSE_STATE_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + } else if (!NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // unexpected + } + break; + case JSON_PARSE_STATE_OBJECT_PAIR : + if (c == '"') { + // we want to be waiting for a : when the string has been read + *mStatep = JSON_PARSE_STATE_OBJECT_IN_PAIR; + PushState(JSON_PARSE_STATE_STRING); + } else if (c == '}') { + rv = this->CloseObject(); + NS_ENSURE_SUCCESS(rv, rv); + // pop off the object_pair state + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + // pop off the object state + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == ']' || !NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // unexpected + } + break; + case JSON_PARSE_STATE_OBJECT_IN_PAIR: + if (c == ':') { + *mStatep = JSON_PARSE_STATE_VALUE; + } else if (!NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // unexpected + } + break; + case JSON_PARSE_STATE_STRING: + if (c == '"') { + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + rv = HandleString(); + NS_ENSURE_SUCCESS(rv, rv); + } else if (c == '\\') { + *mStatep = JSON_PARSE_STATE_STRING_ESCAPE; + } else { + mStringBuffer.Append(c); + } + break; + case JSON_PARSE_STATE_STRING_ESCAPE: + switch(c) { + case '"': + case '\\': + case '/': + break; + case 'b' : c = '\b'; break; + case 'f' : c = '\f'; break; + case 'n' : c = '\n'; break; + case 'r' : c = '\r'; break; + case 't' : c = '\t'; break; + default : + if (c == 'u') { + mNumHex = 0; + mHexChar = 0; + *mStatep = JSON_PARSE_STATE_STRING_HEX; + continue; + } else { + return NS_ERROR_FAILURE; // unexpected + } + } + + mStringBuffer.Append(c); + *mStatep = JSON_PARSE_STATE_STRING; + break; + case JSON_PARSE_STATE_STRING_HEX: + if (('0' <= c) && (c <= '9')) { + mHexChar = (mHexChar << 4) | (c - '0'); + } else if (('a' <= c) && (c <= 'f')) { + mHexChar = (mHexChar << 4) | (c - 'a' + 0x0a); + } else if (('A' <= c) && (c <= 'F')) { + mHexChar = (mHexChar << 4) | (c - 'A' + 0x0a); + } else { + return NS_ERROR_FAILURE; // unexpected + } + + if (++(mNumHex) == 4) { + mStringBuffer.Append(mHexChar); + mHexChar = 0; + mNumHex = 0; + *mStatep = JSON_PARSE_STATE_STRING; + } + break; + case JSON_PARSE_STATE_KEYWORD: + if (NS_IsAsciiAlpha(c)) { + mStringBuffer.Append(c); + } else { + // this character isn't part of the keyword, process it again + i--; + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + rv = HandleKeyword(); + NS_ENSURE_SUCCESS(rv, rv); + } + break; + case JSON_PARSE_STATE_NUMBER: + if (numchars.FindChar(c) >= 0) { + mStringBuffer.Append(c); + } else { + // this character isn't part of the number, process it again + i--; + rv = PopState(); + NS_ENSURE_SUCCESS(rv, rv); + rv = HandleNumber(); + NS_ENSURE_SUCCESS(rv, rv); + } + break; + case JSON_PARSE_STATE_FINISHED: + if (!NS_IsAsciiWhitespace(c)) { + return NS_ERROR_FAILURE; // extra input + } + break; + default: + NS_NOTREACHED("Invalid JSON parser state"); + } + } + + return NS_OK; +} + +nsresult +nsJSONListener::PushValue(JSObject *aParent, jsval aValue) +{ + JSAutoTempValueRooter tvr(mCx, 1, &aValue); + + JSBool ok; + if (JS_IsArrayObject(mCx, aParent)) { + jsuint len; + ok = JS_GetArrayLength(mCx, aParent, &len); + if (ok) { + ok = JS_SetElement(mCx, aParent, len, &aValue); + } + } else { + ok = JS_DefineUCProperty(mCx, aParent, (jschar *) mObjectKey.get(), + mObjectKey.Length(), aValue, + NULL, NULL, JSPROP_ENUMERATE); + } + + return ok ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsJSONListener::PushObject(JSObject *aObj) +{ + if (mObjectStack.Length() >= JSON_MAX_DEPTH) + return NS_ERROR_FAILURE; // decoding error + + // Check if this is the root object + if (mObjectStack.IsEmpty()) { + *mRootVal = OBJECT_TO_JSVAL(aObj); + if (!mObjectStack.AppendElement(aObj)) + return NS_ERROR_OUT_OF_MEMORY; + return NS_OK; + } + + nsresult rv; + JSObject *parent = mObjectStack.ElementAt(mObjectStack.Length() - 1); + rv = PushValue(parent, OBJECT_TO_JSVAL(aObj)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mObjectStack.AppendElement(aObj)) + return NS_ERROR_OUT_OF_MEMORY; + + return rv; +} + +nsresult +nsJSONListener::OpenObject() +{ + if (*mStatep != JSON_PARSE_STATE_VALUE && + *mStatep != JSON_PARSE_STATE_OBJECT) { + return NS_ERROR_FAILURE; + } + + JSObject *obj = JS_NewObject(mCx, NULL, NULL, NULL); + if (!obj) + return NS_ERROR_OUT_OF_MEMORY; + + return PushObject(obj); +} + +nsresult +nsJSONListener::OpenArray() +{ + if (*mStatep != JSON_PARSE_STATE_VALUE && + *mStatep != JSON_PARSE_STATE_ARRAY) { + return NS_ERROR_FAILURE; + } + + // Add an array to an existing array or object + JSObject *arr = JS_NewArrayObject(mCx, 0, NULL); + if (!arr) + return NS_ERROR_OUT_OF_MEMORY; + + return PushObject(arr); +} + +nsresult +nsJSONListener::CloseObject() +{ + if (!mObjectStack.SetLength(mObjectStack.Length() - 1)) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +nsresult +nsJSONListener::CloseArray() +{ + return this->CloseObject(); +} + +nsresult +nsJSONListener::HandleString() +{ + nsresult rv = NS_OK; + if (*mStatep == JSON_PARSE_STATE_OBJECT_IN_PAIR) { + mObjectKey = mStringBuffer; + } else { + JSObject *obj = mObjectStack.ElementAt(mObjectStack.Length() - 1); + JSString *str = JS_NewUCStringCopyN(mCx, (jschar *) mStringBuffer.get(), + mStringBuffer.Length()); + if (!str) + return NS_ERROR_OUT_OF_MEMORY; + rv = PushValue(obj, STRING_TO_JSVAL(str)); + } + + mStringBuffer.Truncate(); + return rv; +} + +nsresult +nsJSONListener::HandleNumber() +{ + nsresult rv; + JSObject *obj = mObjectStack.ElementAt(mObjectStack.Length() - 1); + + char *estr; + int err; + double val = JS_strtod(NS_ConvertUTF16toUTF8(mStringBuffer).get(), + &estr, &err); + if (err == JS_DTOA_ENOMEM) { + rv = NS_ERROR_OUT_OF_MEMORY; + } else if (err || *estr) { + rv = NS_ERROR_FAILURE; // decode error + } else { + // ok + jsval numVal; + if (JS_NewNumberValue(mCx, val, &numVal)) { + rv = PushValue(obj, numVal); + } else { + rv = NS_ERROR_FAILURE; // decode error + } + } + + mStringBuffer.Truncate(); + + return rv; +} + +nsresult +nsJSONListener::HandleKeyword() +{ + JSObject *obj = mObjectStack.ElementAt(mObjectStack.Length() - 1); + jsval keyword; + if (mStringBuffer.Equals(NS_LITERAL_STRING("null"))) { + keyword = JSVAL_NULL; + } else if (mStringBuffer.Equals(NS_LITERAL_STRING("true"))) { + keyword = JSVAL_TRUE; + } else if (mStringBuffer.Equals(NS_LITERAL_STRING("false"))) { + keyword = JSVAL_FALSE; + } else { + return NS_ERROR_FAILURE; + } + + mStringBuffer.Truncate(); + + return PushValue(obj, keyword); +} diff --git a/dom/src/json/nsJSON.h b/dom/src/json/nsJSON.h new file mode 100644 index 00000000000..05793b11c26 --- /dev/null +++ b/dom/src/json/nsJSON.h @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Robert Sayre + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef nsJSON_h__ +#define nsJSON_h__ + +#include "jsprvtd.h" +#include "nsIJSON.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIOutputStream.h" +#include "nsIUnicodeEncoder.h" +#include "nsIUnicodeDecoder.h" +#include "nsIRequestObserver.h" +#include "nsIStreamListener.h" +#include "nsTArray.h" + +#define JSON_MAX_DEPTH 2048 +#define JSON_PARSER_BUFSIZE 1024 +class nsJSONWriter +{ +public: + nsJSONWriter(); + nsJSONWriter(nsIOutputStream *aStream); + virtual ~nsJSONWriter(); + nsresult SetCharset(const char *aCharset); + nsString mBuffer; + nsCOMPtr mStream; + nsresult WriteString(const PRUnichar* aBuffer, PRUint32); + nsresult Write(const PRUnichar *aBuffer, PRUint32 aLength); + +protected: + nsresult WriteToStream(nsIOutputStream *aStream, nsIUnicodeEncoder *encoder, + const PRUnichar *aBuffer, PRUint32 aLength); + + nsCOMPtr mEncoder; +}; + +class nsJSON : public nsIJSON +{ +public: + nsJSON(); + virtual ~nsJSON(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIJSON + +protected: + JSBool ToJSON(JSContext *cx, jsval *vp); + nsresult EncodeObject(JSContext *cx, jsval *vp, nsJSONWriter *writer, + JSObject *whitelist, PRUint32 depth); + nsresult EncodeInternal(nsJSONWriter *writer); + nsresult DecodeInternal(nsIInputStream *aStream, + PRInt32 aContentLength, + PRBool aNeedsConverter); +}; + +NS_IMETHODIMP +NS_NewJSON(nsISupports* aOuter, REFNSIID aIID, void** aResult); + +enum JSONParserState { + JSON_PARSE_STATE_INIT, + JSON_PARSE_STATE_VALUE, + JSON_PARSE_STATE_OBJECT, + JSON_PARSE_STATE_OBJECT_PAIR, + JSON_PARSE_STATE_OBJECT_IN_PAIR, + JSON_PARSE_STATE_ARRAY, + JSON_PARSE_STATE_STRING, + JSON_PARSE_STATE_STRING_ESCAPE, + JSON_PARSE_STATE_STRING_HEX, + JSON_PARSE_STATE_NUMBER, + JSON_PARSE_STATE_KEYWORD, + JSON_PARSE_STATE_FINISHED +}; + +class nsJSONObjectStack : public nsTArray, + public JSTempValueRooter +{ +}; + +class nsJSONListener : public nsIStreamListener +{ +public: + nsJSONListener(JSContext *cx, jsval *rootVal, PRBool needsConverter); + virtual ~nsJSONListener(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + +protected: + PRUint32 mLineNum; + PRUint32 mColumn; + + /* Used while handling \uNNNN in strings */ + PRUnichar mHexChar; + PRUint8 mNumHex; + + JSContext *mCx; + jsval *mRootVal; + PRBool mNeedsConverter; + nsCOMPtr mDecoder; + JSONParserState *mStatep; + JSONParserState mStateStack[JSON_MAX_DEPTH]; + nsString mStringBuffer; + nsCString mSniffBuffer; + + nsresult PushState(JSONParserState state); + nsresult PopState(); + nsresult ProcessBytes(const char* aBuffer, PRUint32 aByteLength); + nsresult ConsumeConverted(const char* aBuffer, PRUint32 aByteLength); + nsresult Consume(const PRUnichar *data, PRUint32 len); + + // These handle parsed tokens. Could be split to separate interface. + nsJSONObjectStack mObjectStack; + + nsresult PushValue(JSObject *aParent, jsval aValue); + nsresult PushObject(JSObject *aObj); + nsresult OpenObject(); + nsresult CloseObject(); + nsresult OpenArray(); + nsresult CloseArray(); + nsresult HandleString(); + nsresult HandleNumber(); + nsresult HandleKeyword(); + nsString mObjectKey; +}; + +#endif diff --git a/dom/src/json/test/Makefile.in b/dom/src/json/test/Makefile.in new file mode 100644 index 00000000000..373726e771a --- /dev/null +++ b/dom/src/json/test/Makefile.in @@ -0,0 +1,49 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is Mozilla Corporation. +# Portions created by the Initial Developer are Copyright (C) 2007 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Robert Sayre +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = json_test + +XPCSHELL_TESTS = unit + +include $(topsrcdir)/config/rules.mk diff --git a/dom/src/json/test/json2.js b/dom/src/json/test/json2.js new file mode 100644 index 00000000000..33b6783fbe1 --- /dev/null +++ b/dom/src/json/test/json2.js @@ -0,0 +1,263 @@ +/* + json2.js + 2007-11-06 + + Public Domain + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: + + JSON.stringify(value, whitelist) + value any JavaScript value, usually an object or array. + + whitelist an optional that determines how object values are + stringified. + + This method produces a JSON text from a JavaScript value. + There are three possible ways to stringify an object, depending + on the optional whitelist parameter. + + If an object has a toJSON method, then the toJSON() method will be + called. The value returned from the toJSON method will be + stringified. + + Otherwise, if the optional whitelist parameter is an array, then + the elements of the array will be used to select members of the + object for stringification. + + Otherwise, if there is no whitelist parameter, then all of the + members of the object will be stringified. + + Values that do not have JSON representaions, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped, in arrays will be replaced with null. JSON.stringify() + returns undefined. Dates will be stringified as quoted ISO dates. + + Example: + + var text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + JSON.parse(text, filter) + This method parses a JSON text to produce an object or + array. It can throw a SyntaxError exception. + + The optional filter parameter is a function that can filter and + transform the results. It receives each of the keys and values, and + its return value is used instead of the original value. If it + returns what it received, then structure is not modified. If it + returns undefined then the member is deleted. + + Example: + + // Parse the text. If a key contains the string 'date' then + // convert the value to a date. + + myData = JSON.parse(text, function (key, value) { + return key.indexOf('date') >= 0 ? new Date(value) : value; + }); + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + Use your own copy. It is extremely unwise to load third party + code into your pages. +*/ + +/*jslint evil: true */ +/*extern JSON */ + +if (!this.JSON) { + + JSON = function () { + + function f(n) { // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + Date.prototype.toJSON = function () { + +// Eventually, this method will be based on the date.toISOString method. + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + + var m = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + + function stringify(value, whitelist) { + var a, // The array holding the partial texts. + i, // The loop counter. + k, // The member key. + l, // Length. + r = /["\\\x00-\x1f\x7f-\x9f]/g, + v; // The member value. + + switch (typeof value) { + case 'string': + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe sequences. + + return r.test(value) ? + '"' + value.replace(r, function (a) { + var c = m[a]; + if (c) { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"' : + '"' + value + '"'; + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + return String(value); + + case 'object': + +// Due to a specification blunder in ECMAScript, +// typeof null is 'object', so watch out for that case. + + if (!value) { + return 'null'; + } + +// If the object has a toJSON method, call it, and stringify the result. + + if (typeof value.toJSON === 'function') { + return stringify(value.toJSON()); + } + a = []; + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length'))) { + +// The object is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + l = value.length; + for (i = 0; i < l; i += 1) { + a.push(stringify(value[i], whitelist) || 'null'); + } + +// Join all of the elements together and wrap them in brackets. + + return '[' + a.join(',') + ']'; + } + if (whitelist) { + +// If a whitelist (array of keys) is provided, use it to select the components +// of the object. + + l = whitelist.length; + for (i = 0; i < l; i += 1) { + k = whitelist[i]; + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } + +// Join all of the member texts together and wrap them in braces. + + return '{' + a.join(',') + '}'; + } + return undefined; + } + + return { + stringify: stringify, + parse: function (text, filter) { + var j; + + function walk(k, v) { + var i, n; + if (v && typeof v === 'object') { + for (i in v) { + if (Object.prototype.hasOwnProperty.apply(v, [i])) { + n = walk(i, v[i]); + if (n !== undefined) { + v[i] = n; + } + } + } + } + return filter(k, v); + } + + +// Parsing happens in three stages. In the first stage, we run the text against +// regular expressions that look for non-JSON patterns. We are especially +// concerned with '()' and 'new' because they can cause invocation, and '=' +// because it can cause mutation. But just to be safe, we want to reject all +// unexpected forms. + +// We split the first stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace all backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(:?[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the second stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional third stage, we recursively walk the new structure, passing +// each name/value pair to a filter function for possible transformation. + + return typeof filter === 'function' ? walk('', j) : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('parseJSON'); + } + }; + }(); +} diff --git a/dom/src/json/test/pass1.json b/dom/src/json/test/pass1.json new file mode 100644 index 00000000000..2c10f226b17 --- /dev/null +++ b/dom/src/json/test/pass1.json @@ -0,0 +1,58 @@ +[ + "JSON Test Pattern pass1", + {"object with 1 member":["array with 1 element"]}, + {}, + [], + -42, + true, + false, + null, + { + "integer": 1234567890, + "real": -9876.543210, + "e": 0.123456789e-12, + "E": 1.234567890E+34, + "": 23456789012E66, + "zero": 0, + "one": 1, + "space": " ", + "quote": "\"", + "backslash": "\\", + "controls": "\b\f\n\r\t", + "slash": "/ & \/", + "alpha": "abcdefghijklmnopqrstuvwyz", + "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", + "digit": "0123456789", + "0123456789": "digit", + "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", + "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", + "true": true, + "false": false, + "null": null, + "array":[ ], + "object":{ }, + "address": "50 St. James Street", + "url": "http://www.JSON.org/", + "comment": "// /* */": " ", + " s p a c e d " :[1,2 , 3 + +, + +4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], + "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", + "quotes": "" \u0022 %22 0x22 034 "", + "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" +: "A key can be any string" + }, + 0.5 ,98.6 +, +99.44 +, + +1066, +1e1, +0.1e1, +1e-1, +1e00,2e+00,2e-00 +,"rosebud"] diff --git a/dom/src/json/test/pass3.json b/dom/src/json/test/pass3.json new file mode 100644 index 00000000000..258658753a0 --- /dev/null +++ b/dom/src/json/test/pass3.json @@ -0,0 +1,7 @@ +{ + "JSON Test Pattern pass3": { + "The outermost value": "must be an object or array.", + "In this test": "It is an object." + } +} + diff --git a/dom/src/json/test/unit/head_json.js b/dom/src/json/test/unit/head_json.js new file mode 100644 index 00000000000..6578092bbd6 --- /dev/null +++ b/dom/src/json/test/unit/head_json.js @@ -0,0 +1,19 @@ +var Ci = Components.interfaces; +var Cc = Components.classes; + +var nativeJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); +var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); +var workingDir = dirSvc.get("TmpD", Ci.nsIFile); + +var outputName = "json-test-output"; +var outputDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); +outputDir.initWithFile(workingDir); +outputDir.append(outputName); + +if (!outputDir.exists()) { + outputDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777); +} else if (!outputDir.isDirectory()) { + do_throw(outputName + " is not a directory?") +} +var JSON = null; +do_import_script("dom/src/json/test/json2.js"); diff --git a/dom/src/json/test/unit/test_decode.js b/dom/src/json/test/unit/test_decode.js new file mode 100644 index 00000000000..1d4d284c729 --- /dev/null +++ b/dom/src/json/test/unit/test_decode.js @@ -0,0 +1,161 @@ +function decode_strings() { + // empty object + var x = nativeJSON.decode("{}"); + do_check_eq(typeof x, "object"); + + // empty array + x = nativeJSON.decode("[]"); + do_check_eq(typeof x, "object"); + do_check_eq(x.length, 0); + do_check_eq(x.constructor, Array); + + // one element array + x = nativeJSON.decode("[[]]"); + do_check_eq(typeof x, "object"); + do_check_eq(x.length, 1); + do_check_eq(x.constructor, Array); + do_check_eq(x[0].constructor, Array); + + // multiple arrays + x = nativeJSON.decode("[[],[],[]]"); + do_check_eq(typeof x, "object"); + do_check_eq(x.length, 3); + do_check_eq(x.constructor, Array); + do_check_eq(x[0].constructor, Array); + do_check_eq(x[1].constructor, Array); + do_check_eq(x[2].constructor, Array); + + // array key/value + x = nativeJSON.decode('{"foo":[]}'); + do_check_eq(typeof x, "object"); + do_check_eq(typeof x.foo, "object"); + do_check_eq(x.foo.constructor, Array); + x = nativeJSON.decode('{"foo":[], "bar":[]}'); + do_check_eq(typeof x, "object"); + do_check_eq(typeof x.foo, "object"); + do_check_eq(x.foo.constructor, Array); + do_check_eq(typeof x.bar, "object"); + do_check_eq(x.bar.constructor, Array); + + // nesting + x = nativeJSON.decode('{"foo":[{}]}'); + do_check_eq(x.foo.constructor, Array); + do_check_eq(x.foo.length, 1); + do_check_eq(typeof x.foo[0], "object"); + x = nativeJSON.decode('{"foo":[{"foo":[{"foo":{}}]}]}'); + do_check_eq(x.foo[0].foo[0].foo.constructor, Object); + x = nativeJSON.decode('{"foo":[{"foo":[{"foo":[]}]}]}'); + do_check_eq(x.foo[0].foo[0].foo.constructor, Array); + + // strings + x = nativeJSON.decode('{"foo":"bar"}'); + do_check_eq(x.foo, "bar"); + x = nativeJSON.decode('["foo", "bar", "baz"]'); + do_check_eq(x[0], "foo"); + do_check_eq(x[1], "bar"); + do_check_eq(x[2], "baz"); + + // numbers + x = nativeJSON.decode('{"foo":5.5, "bar":5}'); + do_check_eq(x.foo, 5.5); + do_check_eq(x.bar, 5); + + // keywords + x = nativeJSON.decode('{"foo": true, "bar":false, "baz":null}'); + do_check_eq(x.foo, true); + do_check_eq(x.bar, false); + do_check_eq(x.baz, null); + + // short escapes + x = nativeJSON.decode('{"foo": "\\"", "bar":"\\\\", "baz":"\\b","qux":"\\f", "quux":"\\n", "quuux":"\\r","quuuux":"\\t"}'); + do_check_eq(x.foo, '"'); + do_check_eq(x.bar, '\\'); + do_check_eq(x.baz, '\b'); + do_check_eq(x.qux, '\f'); + do_check_eq(x.quux, "\n"); + do_check_eq(x.quuux, "\r"); + do_check_eq(x.quuuux, "\t"); + + // unicode escape + x = nativeJSON.decode('{"foo":"hmm\\u006dmm"}'); + do_check_eq("hmm\u006dmm", x.foo); + + x = nativeJSON.decode('{"JSON Test Pattern pass3": {"The outermost value": "must be an object or array.","In this test": "It is an object." }}'); +} + +function test_files() { + function read_file(path) { + try { + var f = do_get_file(path); + var istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + istream.init(f, -1, -1, false); + var x = nativeJSON.decodeFromStream(istream, istream.available()); + } finally { + istream.close(); + } + return x; + } + + var x = read_file("/dom/src/json/test/pass3.json"); + do_check_eq(x["JSON Test Pattern pass3"]["The outermost value"], "must be an object or array."); + do_check_eq(x["JSON Test Pattern pass3"]["In this test"], "It is an object."); + + x = read_file("/dom/src/json/test/pass1.json"); + do_check_eq(x[0], "JSON Test Pattern pass1"); + do_check_eq(x[1]["object with 1 member"][0], "array with 1 element"); + do_check_eq(x[2].constructor, Object); + do_check_eq(x[3].constructor, Array); + do_check_eq(x[4], -42); + do_check_eq(x[5], true); + do_check_eq(x[6], false); + do_check_eq(x[7], null); + do_check_eq(x[8].constructor, Object); + do_check_eq(x[8]["integer"], 1234567890); + do_check_eq(x[8]["real"], -9876.543210); + do_check_eq(x[8]["e"], 0.123456789e-12); + do_check_eq(x[8]["E"], 1.234567890E+34); + do_check_eq(x[8][""], 23456789012E66); + do_check_eq(x[8]["zero"], 0); + do_check_eq(x[8]["one"], 1); + do_check_eq(x[8]["space"], " "); + do_check_eq(x[8]["quote"], "\""); + do_check_eq(x[8]["backslash"], "\\"); + do_check_eq(x[8]["controls"], "\b\f\n\r\t"); + do_check_eq(x[8]["slash"], "/ & /"); + do_check_eq(x[8]["alpha"], "abcdefghijklmnopqrstuvwyz"); + do_check_eq(x[8]["ALPHA"], "ABCDEFGHIJKLMNOPQRSTUVWYZ"); + do_check_eq(x[8]["digit"], "0123456789"); + do_check_eq(x[8]["0123456789"], "digit"); + do_check_eq(x[8]["special"], "`1~!@#$%^&*()_+-={':[,]}|;.?"); + do_check_eq(x[8]["hex"], "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A"); + do_check_eq(x[8]["true"], true); + do_check_eq(x[8]["false"], false); + do_check_eq(x[8]["null"], null); + do_check_eq(x[8]["array"].length, 0); + do_check_eq(x[8]["object"].constructor, Object); + do_check_eq(x[8]["address"], "50 St. James Street"); + do_check_eq(x[8]["url"], "http://www.JSON.org/"); + do_check_eq(x[8]["comment"], "// /* */"], " "); + do_check_eq(x[8][" s p a c e d "].length, 7); + do_check_eq(x[8]["compact"].length, 7); + do_check_eq(x[8]["jsontext"], "{\"object with 1 member\":[\"array with 1 element\"]}"); + do_check_eq(x[8]["quotes"], "" \u0022 %22 0x22 034 ""); + do_check_eq(x[8]["\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"], "A key can be any string"); + do_check_eq(x[9], 0.5); + do_check_eq(x[10], 98.6); + do_check_eq(x[11], 99.44); + do_check_eq(x[12], 1066); + do_check_eq(x[13], 1e1); + do_check_eq(x[14], 0.1e1); + do_check_eq(x[15], 1e-1); + do_check_eq(x[16], 1e00); + do_check_eq(x[17], 2e+00); + do_check_eq(x[18], 2e-00); + do_check_eq(x[19], "rosebud"); +} + +function run_test() { + decode_strings(); + test_files(); +} diff --git a/dom/src/json/test/unit/test_encode.js b/dom/src/json/test/unit/test_encode.js new file mode 100644 index 00000000000..9e5ec3a0eaf --- /dev/null +++ b/dom/src/json/test/unit/test_encode.js @@ -0,0 +1,237 @@ +// returns a list of [string, object] pairs to test encoding +function getTestPairs() { + var testPairs = [ + ["{}", {}], + ["[]", []], + ['{"foo":"bar"}', {"foo":"bar"}], + ['{"null":null}', {"null":null}], + ['{"five":5}', {"five":5}], + ['{"five":5,"six":6}', {"five":5, "six":6}], + ['{"x":{"y":"z"}}', {"x":{"y":"z"}}], + ['{"w":{"x":{"y":"z"}}}', {"w":{"x":{"y":"z"}}}], + ['[1,2,3]', [1,2,3]], + ['{"w":{"x":{"y":[1,2,3]}}}', {"w":{"x":{"y":[1,2,3]}}}], + ['{"false":false}', {"false":false}], + ['{"true":true}', {"true":true}], + ['{"child has two members":{"this":"one","2":"and this one"}}', + {"child has two members": {"this":"one", 2:"and this one"}}], + ['{"x":{"a":"b","c":{"y":"z"},"f":"g"}}', + {"x":{"a":"b","c":{"y":"z"},"f":"g"}}], + ['{"x":[1,{"y":"z"},3]}', {"x":[1,{"y":"z"},3]}], + //['{"0":"h","1":"m","2":"m"}', new String("hmm")], + ['[1,null,3]',[1,,3]], + [null, function test(){}], + [null, dump], + ['{"mm\\\"mm":"hmm"}',{"mm\"mm":"hmm"}], + ['{"mm\\\"mm\\\"mm":"hmm"}',{"mm\"mm\"mm":"hmm"}], + ['{"\\\"":"hmm"}',{'"':"hmm"}], + ['{"\\\\":"hmm"}',{'\\':"hmm"}], + ['{"mmm\\\\mmm":"hmm"}',{'mmm\\mmm':"hmm"}], + ['{"mmm\\\\mmm\\\\mmm":"hmm"}',{'mmm\\mmm\\mmm':"hmm"}], + ['{"mm\\u000bmm":"hmm"}',{"mm\u000bmm":"hmm"}], + ['{"mm\\u0000mm":"hmm"}',{"mm\u0000mm":"hmm"}] + ]; + + var x = {"free":"variable"} + testPairs.push(['{"free":"variable"}', x]); + testPairs.push(['{"y":{"free":"variable"}}', {"y":x}]); + + // array prop + var x = { + a: [1,2,3] + } + testPairs.push(['{"a":[1,2,3]}', x]) + + var y = { + foo: function(hmm) { return hmm; } + } + testPairs.push(['{"y":{}}',{"y":y}]); + + // test toJSON + var hmm = { + toJSON: function() { return {"foo":"bar"}} + } + testPairs.push(['{"hmm":{"foo":"bar"}}', {"hmm":hmm}]); + testPairs.push(['{"foo":"bar"}', hmm]); // on the root + + // toJSON on prototype + var Y = function() { + this.d = "e"; + } + Y.prototype = { + not:"there?", + toJSON: function() { return {"foo":"bar"}} + }; + var y = new Y(); + testPairs.push(['{"foo":"bar"}', y.toJSON()]); + testPairs.push(['{"foo":"bar"}', y]); + + // return undefined from toJSON + var hmm = { + toJSON: function() { return; } + } + testPairs.push(['{}', {"hmm":hmm}]); + + // array with named prop + var x= new Array(); + x[0] = 1; + x.foo = "bar"; + //testPairs.push(['[1]', x]); + + // prototype + var X = function() { this.a = "b" } + X.prototype = {c:"d"} + var y = new X(); + testPairs.push(['{"a":"b","c":"d"}', y]); + + // useless roots will be dropped + testPairs.push([null, null]); + testPairs.push([null, ""]); + testPairs.push([null, undefined]); + testPairs.push([null, 5]); + + // custom iterator: JS 1.7+ + var x = { + "a": "foo", + b: "not included", + c: "bar", + "4": "qux", + __iterator__: function() { return (function() { yield "a"; yield "c"; yield 4; })() } + } + do_check_eq('{"a":"foo","c":"bar","4":"qux"}', nativeJSON.encode(x)); + + return testPairs; +} + +function testStringEncode() { + // test empty arg + do_check_eq(null, nativeJSON.encode()); + + var pairs = getTestPairs(); + for each(pair in pairs) { + var nativeResult = nativeJSON.encode(pair[1]); + var crockfordResult = JSON.stringify(pair[1]); + do_check_eq(pair[0], nativeResult); + + // Don't follow json2.js handling of non-objects + if (pair[1] && (typeof pair[1] == "object")) { + do_check_eq(crockfordResult, nativeResult); + } + } +} + +function testOutputStreams() { + function writeToFile(obj, charset, writeBOM) { + var jsonFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + jsonFile.initWithFile(outputDir); + jsonFile.append("test.json"); + jsonFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); + var stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + try { + stream.init(jsonFile, 0x04 | 0x08 | 0x20, 0600, 0); // write, create, truncate + nativeJSON.encodeToStream(stream, charset, writeBOM, obj); + } finally { + stream.close(); + } + return jsonFile; + } + + var pairs = getTestPairs(); + for each(pair in pairs) { + if (pair[1] && (typeof pair[1] == "object")) { + var utf8File = writeToFile(pair[1], "UTF-8", false); + var utf16LEFile = writeToFile(pair[1], "UTF-16LE", false); + var utf16BEFile = writeToFile(pair[1], "UTF-16BE", false); + var utf32LEFile = writeToFile(pair[1], "UTF-32LE", false); + var utf32BEFile = writeToFile(pair[1], "UTF-32BE", false); + + // all ascii with no BOMs, so this will work + do_check_eq(utf16LEFile.fileSize / 2, utf8File.fileSize); + do_check_eq(utf32LEFile.fileSize / 4, utf8File.fileSize); + do_check_eq(utf16LEFile.fileSize, utf16BEFile.fileSize); + do_check_eq(utf32LEFile.fileSize, utf32BEFile.fileSize); + } + } + + // check BOMs + var f = writeToFile({},"UTF-8", true); + do_check_eq(f.fileSize, 5); + var f = writeToFile({},"UTF-16LE", true); + do_check_eq(f.fileSize, 6); + var f = writeToFile({},"UTF-16BE", true); + do_check_eq(f.fileSize, 6); + var f = writeToFile({},"UTF-32LE", true); + do_check_eq(f.fileSize, 12); + var f = writeToFile({},"UTF-32BE", true); + do_check_eq(f.fileSize, 12); + + outputDir.remove(true); +} + +function throwingToJSON() { + var a = { + "b": 1, + "c": 2, + toJSON: function() { throw("uh oh"); } + } + try { + var y = nativeJSON.encode(a); + } catch (ex) {} +} + +function throwingIterator() { + var a = { + "b": 1, + "c": 2, + __iterator__: function() { yield "b"; throw("uh oh"); } + } + try { + var y = nativeJSON.encode(a); + } catch (ex) {} +} + +function deletingIter(x) { + return function() { + yield "dd"; + print("after first yield"); + delete x["a"]["c"]; + gc(); + print("about to yield second"); + yield "ddddd"; + } +} + +function deleteDuringEncode() { + var x = {}; + x.a = { + b: 1, + bb: 2, + bbb: 3, + c: { + cc: 2, + ccc: 3, + d: { + dd: 2, + ddd: 3, + __iterator__: deletingIter(x), + dddd: 4, + ddddd: 5 + }, + cccc: 4, + ccccc: 5 + }, + bbbb: 4, + bbbbb: 5, + bbbbbb: 6 + }; + var z = nativeJSON.encode(x); + print(z); +} + +function run_test() { + testStringEncode(); + testOutputStreams(); + throwingToJSON(); + throwingIterator(); + deleteDuringEncode(); +} diff --git a/js/src/Makefile.in b/js/src/Makefile.in index 1199b819291..8b3b0887289 100644 --- a/js/src/Makefile.in +++ b/js/src/Makefile.in @@ -136,6 +136,7 @@ EXPORTS = \ jsdate.h \ jsdbgapi.h \ jsdhash.h \ + jsdtoa.h \ jsemit.h \ jsfun.h \ jsgc.h \ diff --git a/js/src/jsiter.c b/js/src/jsiter.c index 7b4a38bc4ba..f81554b212b 100644 --- a/js/src/jsiter.c +++ b/js/src/jsiter.c @@ -328,7 +328,7 @@ js_GetNativeIteratorFlags(JSContext *cx, JSObject *iterobj) * Call ToObject(v).__iterator__(keyonly) if ToObject(v).__iterator__ exists. * Otherwise construct the defualt iterator. */ -JSBool +JS_FRIEND_API(JSBool) js_ValueToIterator(JSContext *cx, uintN flags, jsval *vp) { JSObject *obj; @@ -439,7 +439,7 @@ js_ValueToIterator(JSContext *cx, uintN flags, jsval *vp) goto out; } -JSBool +JS_FRIEND_API(JSBool) js_CloseIterator(JSContext *cx, jsval v) { JSObject *obj; @@ -593,7 +593,7 @@ CallEnumeratorNext(JSContext *cx, JSObject *iterobj, uintN flags, jsval *rval) return JS_TRUE; } -JSBool +JS_FRIEND_API(JSBool) js_CallIteratorNext(JSContext *cx, JSObject *iterobj, jsval *rval) { uintN flags; diff --git a/js/src/jsiter.h b/js/src/jsiter.h index f04cab2aec1..a09c9ff6fb9 100644 --- a/js/src/jsiter.h +++ b/js/src/jsiter.h @@ -58,17 +58,17 @@ JS_BEGIN_EXTERN_C * for-in semantics are required, and when the caller can guarantee that the * iterator will never be exposed to scripts. */ -extern JSBool +extern JS_FRIEND_API(JSBool) js_ValueToIterator(JSContext *cx, uintN flags, jsval *vp); -extern JSBool +extern JS_FRIEND_API(JSBool) js_CloseIterator(JSContext *cx, jsval v); /* * Given iterobj, call iterobj.next(). If the iterator stopped, set *rval to * JSVAL_HOLE. Otherwise set it to the result of the next call. */ -extern JSBool +extern JS_FRIEND_API(JSBool) js_CallIteratorNext(JSContext *cx, JSObject *iterobj, jsval *rval); /* diff --git a/layout/build/Makefile.in b/layout/build/Makefile.in index 87e950029d0..32184a3be0d 100644 --- a/layout/build/Makefile.in +++ b/layout/build/Makefile.in @@ -141,6 +141,7 @@ SHARED_LIBRARY_LIBS = \ $(DEPTH)/view/src/$(LIB_PREFIX)gkview_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/base/$(LIB_PREFIX)jsdombase_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/events/$(LIB_PREFIX)jsdomevents_s.$(LIB_SUFFIX) \ + $(DEPTH)/dom/src/json/$(LIB_PREFIX)json_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/jsurl/$(LIB_PREFIX)jsurl_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/storage/$(LIB_PREFIX)jsdomstorage_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/src/offline/$(LIB_PREFIX)jsdomoffline_s.$(LIB_SUFFIX) \ @@ -277,6 +278,7 @@ LOCAL_INCLUDES += -I$(srcdir)/../base \ -I$(topsrcdir)/content/xbl/src \ -I$(topsrcdir)/view/src \ -I$(topsrcdir)/dom/src/base \ + -I$(topsrcdir)/dom/src/json \ -I$(topsrcdir)/dom/src/jsurl \ -I$(topsrcdir)/dom/src/storage \ -I$(topsrcdir)/dom/src/offline \ diff --git a/layout/build/nsLayoutCID.h b/layout/build/nsLayoutCID.h index e0fb2ec8b16..c99b330077c 100644 --- a/layout/build/nsLayoutCID.h +++ b/layout/build/nsLayoutCID.h @@ -222,4 +222,7 @@ #define NS_XULPOPUPMANAGER_CID \ { 0x14632191, 0xac21, 0x4bdf, { 0x83, 0xe7, 0x23, 0x63, 0xad, 0x17, 0xe8, 0x38 } } +// {93ad72a6-02cd-4716-9626-d47d5ec275ec} +#define NS_DOMJSON_CID \ +{ 0x93ad72a6, 0x02cd, 0x4716, { 0x96, 0x26, 0xd4, 0x7d, 0x5e, 0xc2, 0x75, 0xec } } #endif /* nsLayoutCID_h__ */ diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp index 581b68bd372..40b8e9cdd1b 100644 --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -125,6 +125,7 @@ #include "nsIControllerContext.h" #include "nsDOMScriptObjectFactory.h" #include "nsDOMStorage.h" +#include "nsJSON.h" // Editor stuff #include "nsEditorCID.h" @@ -1321,6 +1322,11 @@ static const nsModuleComponentInfo gComponents[] = { "@mozilla.org/dom/storagemanager;1", nsDOMStorageManagerConstructor }, + { "DOM JSON", + NS_DOMJSON_CID, + "@mozilla.org/dom/json;1", + NS_NewJSON }, + { "Text Editor", NS_TEXTEDITOR_CID, "@mozilla.org/editor/texteditor;1", diff --git a/layout/style/nsCSSLoader.cpp b/layout/style/nsCSSLoader.cpp index e72accd51d9..b304ca11c40 100644 --- a/layout/style/nsCSSLoader.cpp +++ b/layout/style/nsCSSLoader.cpp @@ -463,34 +463,8 @@ static nsresult GetCharsetFromData(const unsigned char* aStyleSheetData, step = 1; pos = 0; } - else if (aStyleSheetData[0] == 0xEF && - aStyleSheetData[1] == 0xBB && - aStyleSheetData[2] == 0xBF) { - // UTF-8 BOM - step = 1; - pos = 3; - aCharset = "UTF-8"; - } // Check for a 4-byte encoding BOM before checking for a 2-byte one, // since the latter can be a proper subset of the former. - else if (aStyleSheetData[0] == 0x00 && - aStyleSheetData[1] == 0x00 && - aStyleSheetData[2] == 0xFE && - aStyleSheetData[3] == 0xFF) { - // big-endian 4-byte encoding BOM - step = 4; - pos = 7; - aCharset = "UTF-32BE"; - } - else if (aStyleSheetData[0] == 0xFF && - aStyleSheetData[1] == 0xFE && - aStyleSheetData[2] == 0x00 && - aStyleSheetData[3] == 0x00) { - // little-endian 4-byte encoding BOM - step = 4; - pos = 4; - aCharset = "UTF-32LE"; - } else if (aStyleSheetData[0] == 0x00 && aStyleSheetData[1] == 0x00 && aStyleSheetData[2] == 0xFF && @@ -511,17 +485,28 @@ static nsresult GetCharsetFromData(const unsigned char* aStyleSheetData, pos = 5; aCharset = "UTF-32"; } - else if (aStyleSheetData[0] == 0xFE && aStyleSheetData[1] == 0xFF) { - // big-endian 2-byte encoding BOM - step = 2; - pos = 3; - aCharset = "UTF-16BE"; - } - else if (aStyleSheetData[0] == 0xFF && aStyleSheetData[1] == 0xFE) { - // little-endian 2-byte encoding BOM - step = 2; - pos = 2; - aCharset = "UTF-16LE"; + else if (nsContentUtils::CheckForBOM(aStyleSheetData, + aDataLength, aCharset)) { + if (aCharset.Equals("UTF-8")) { + step = 1; + pos = 3; + } + else if (aCharset.Equals("UTF-32BE")) { + step = 4; + pos = 7; + } + else if (aCharset.Equals("UTF-32LE")) { + step = 4; + pos = 4; + } + else if (aCharset.Equals("UTF-16BE")) { + step = 2; + pos = 3; + } + else if (aCharset.Equals("UTF-16LE")) { + step = 2; + pos = 2; + } } else if (aStyleSheetData[0] == 0x00 && aStyleSheetData[1] == 0x00 && diff --git a/tools/test-harness/xpcshell-simple/test_all.sh b/tools/test-harness/xpcshell-simple/test_all.sh index 9f74a86fc21..f195c70f437 100755 --- a/tools/test-harness/xpcshell-simple/test_all.sh +++ b/tools/test-harness/xpcshell-simple/test_all.sh @@ -111,7 +111,7 @@ done for t in $testdir/test_*.js do echo -n "$t: " - NATIVE_TOPSRCDIR="$native_topsrcdir" TOPSRCDIR="$topsrcdir" $xpcshell -s $headfiles -f $t $tailfiles 2> $t.log 1>&2 + NATIVE_TOPSRCDIR="$native_topsrcdir" TOPSRCDIR="$topsrcdir" $xpcshell -v 180 -s $headfiles -f $t $tailfiles 2> $t.log 1>&2 rv="$?" if [ ! "$rv" = "0" -o \ `grep -c '\*\*\* PASS' $t.log` = 0 ]