summaryrefslogtreecommitdiffstats
path: root/js/src/json.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/json.cpp')
-rw-r--r--js/src/json.cpp998
1 files changed, 998 insertions, 0 deletions
diff --git a/js/src/json.cpp b/js/src/json.cpp
new file mode 100644
index 000000000..425a2f117
--- /dev/null
+++ b/js/src/json.cpp
@@ -0,0 +1,998 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "json.h"
+
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Range.h"
+#include "mozilla/ScopeExit.h"
+
+#include "jsarray.h"
+#include "jsatom.h"
+#include "jscntxt.h"
+#include "jsnum.h"
+#include "jsobj.h"
+#include "jsstr.h"
+#include "jstypes.h"
+#include "jsutil.h"
+
+#include "vm/Interpreter.h"
+#include "vm/JSONParser.h"
+#include "vm/StringBuffer.h"
+
+#include "jsatominlines.h"
+#include "jsboolinlines.h"
+
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+using namespace js::gc;
+
+using mozilla::IsFinite;
+using mozilla::Maybe;
+using mozilla::RangedPtr;
+
+const Class js::JSONClass = {
+ js_JSON_str,
+ JSCLASS_HAS_CACHED_PROTO(JSProto_JSON)
+};
+
+static inline bool
+IsQuoteSpecialCharacter(char16_t c)
+{
+ static_assert('\b' < ' ', "'\\b' must be treated as special below");
+ static_assert('\f' < ' ', "'\\f' must be treated as special below");
+ static_assert('\n' < ' ', "'\\n' must be treated as special below");
+ static_assert('\r' < ' ', "'\\r' must be treated as special below");
+ static_assert('\t' < ' ', "'\\t' must be treated as special below");
+
+ return c == '"' || c == '\\' || c < ' ';
+}
+
+/* ES5 15.12.3 Quote. */
+template <typename CharT>
+static bool
+Quote(StringBuffer& sb, JSLinearString* str)
+{
+ size_t len = str->length();
+
+ /* Step 1. */
+ if (!sb.append('"'))
+ return false;
+
+ /* Step 2. */
+ JS::AutoCheckCannotGC nogc;
+ const RangedPtr<const CharT> buf(str->chars<CharT>(nogc), len);
+ for (size_t i = 0; i < len; ++i) {
+ /* Batch-append maximal character sequences containing no escapes. */
+ size_t mark = i;
+ do {
+ if (IsQuoteSpecialCharacter(buf[i]))
+ break;
+ } while (++i < len);
+ if (i > mark) {
+ if (!sb.appendSubstring(str, mark, i - mark))
+ return false;
+ if (i == len)
+ break;
+ }
+
+ char16_t c = buf[i];
+ if (c == '"' || c == '\\') {
+ if (!sb.append('\\') || !sb.append(c))
+ return false;
+ } else if (c == '\b' || c == '\f' || c == '\n' || c == '\r' || c == '\t') {
+ char16_t abbrev = (c == '\b')
+ ? 'b'
+ : (c == '\f')
+ ? 'f'
+ : (c == '\n')
+ ? 'n'
+ : (c == '\r')
+ ? 'r'
+ : 't';
+ if (!sb.append('\\') || !sb.append(abbrev))
+ return false;
+ } else {
+ MOZ_ASSERT(c < ' ');
+ if (!sb.append("\\u00"))
+ return false;
+ MOZ_ASSERT((c >> 4) < 10);
+ uint8_t x = c >> 4, y = c % 16;
+ if (!sb.append(Latin1Char('0' + x)) ||
+ !sb.append(Latin1Char(y < 10 ? '0' + y : 'a' + (y - 10))))
+ {
+ return false;
+ }
+ }
+ }
+
+ /* Steps 3-4. */
+ return sb.append('"');
+}
+
+static bool
+Quote(JSContext* cx, StringBuffer& sb, JSString* str)
+{
+ JSLinearString* linear = str->ensureLinear(cx);
+ if (!linear)
+ return false;
+
+ return linear->hasLatin1Chars()
+ ? Quote<Latin1Char>(sb, linear)
+ : Quote<char16_t>(sb, linear);
+}
+
+namespace {
+
+using ObjectSet = GCHashSet<JSObject*, MovableCellHasher<JSObject*>, SystemAllocPolicy>;
+
+class StringifyContext
+{
+ public:
+ StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
+ HandleObject replacer, const AutoIdVector& propertyList,
+ bool maybeSafely)
+ : sb(sb),
+ gap(gap),
+ replacer(cx, replacer),
+ stack(cx),
+ propertyList(propertyList),
+ depth(0),
+ maybeSafely(maybeSafely)
+ {
+ MOZ_ASSERT_IF(maybeSafely, !replacer);
+ MOZ_ASSERT_IF(maybeSafely, gap.empty());
+ }
+
+ bool init() {
+ return stack.init(8);
+ }
+
+ StringBuffer& sb;
+ const StringBuffer& gap;
+ RootedObject replacer;
+ Rooted<ObjectSet> stack;
+ const AutoIdVector& propertyList;
+ uint32_t depth;
+ bool maybeSafely;
+};
+
+} /* anonymous namespace */
+
+static bool Str(JSContext* cx, const Value& v, StringifyContext* scx);
+
+static bool
+WriteIndent(JSContext* cx, StringifyContext* scx, uint32_t limit)
+{
+ if (!scx->gap.empty()) {
+ if (!scx->sb.append('\n'))
+ return false;
+
+ if (scx->gap.isUnderlyingBufferLatin1()) {
+ for (uint32_t i = 0; i < limit; i++) {
+ if (!scx->sb.append(scx->gap.rawLatin1Begin(), scx->gap.rawLatin1End()))
+ return false;
+ }
+ } else {
+ for (uint32_t i = 0; i < limit; i++) {
+ if (!scx->sb.append(scx->gap.rawTwoByteBegin(), scx->gap.rawTwoByteEnd()))
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+namespace {
+
+template<typename KeyType>
+class KeyStringifier {
+};
+
+template<>
+class KeyStringifier<uint32_t> {
+ public:
+ static JSString* toString(JSContext* cx, uint32_t index) {
+ return IndexToString(cx, index);
+ }
+};
+
+template<>
+class KeyStringifier<HandleId> {
+ public:
+ static JSString* toString(JSContext* cx, HandleId id) {
+ return IdToString(cx, id);
+ }
+};
+
+} /* anonymous namespace */
+
+/*
+ * ES5 15.12.3 Str, steps 2-4, extracted to enable preprocessing of property
+ * values when stringifying objects in JO.
+ */
+template<typename KeyType>
+static bool
+PreprocessValue(JSContext* cx, HandleObject holder, KeyType key, MutableHandleValue vp, StringifyContext* scx)
+{
+ // We don't want to do any preprocessing here if scx->maybeSafely,
+ // since the stuff we do here can have side-effects.
+ if (scx->maybeSafely)
+ return true;
+
+ RootedString keyStr(cx);
+
+ /* Step 2. */
+ if (vp.isObject()) {
+ RootedValue toJSON(cx);
+ RootedObject obj(cx, &vp.toObject());
+ if (!GetProperty(cx, obj, obj, cx->names().toJSON, &toJSON))
+ return false;
+
+ if (IsCallable(toJSON)) {
+ keyStr = KeyStringifier<KeyType>::toString(cx, key);
+ if (!keyStr)
+ return false;
+
+ RootedValue arg0(cx, StringValue(keyStr));
+ if (!js::Call(cx, toJSON, vp, arg0, vp))
+ return false;
+ }
+ }
+
+ /* Step 3. */
+ if (scx->replacer && scx->replacer->isCallable()) {
+ if (!keyStr) {
+ keyStr = KeyStringifier<KeyType>::toString(cx, key);
+ if (!keyStr)
+ return false;
+ }
+
+ RootedValue arg0(cx, StringValue(keyStr));
+ RootedValue replacerVal(cx, ObjectValue(*scx->replacer));
+ if (!js::Call(cx, replacerVal, holder, arg0, vp, vp))
+ return false;
+ }
+
+ /* Step 4. */
+ if (vp.get().isObject()) {
+ RootedObject obj(cx, &vp.get().toObject());
+
+ ESClass cls;
+ if (!GetBuiltinClass(cx, obj, &cls))
+ return false;
+
+ if (cls == ESClass::Number) {
+ double d;
+ if (!ToNumber(cx, vp, &d))
+ return false;
+ vp.setNumber(d);
+ } else if (cls == ESClass::String) {
+ JSString* str = ToStringSlow<CanGC>(cx, vp);
+ if (!str)
+ return false;
+ vp.setString(str);
+ } else if (cls == ESClass::Boolean) {
+ if (!Unbox(cx, obj, vp))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/*
+ * Determines whether a value which has passed by ES5 150.2.3 Str steps 1-4's
+ * gauntlet will result in Str returning |undefined|. This function is used to
+ * properly omit properties resulting in such values when stringifying objects,
+ * while properly stringifying such properties as null when they're encountered
+ * in arrays.
+ */
+static inline bool
+IsFilteredValue(const Value& v)
+{
+ return v.isUndefined() || v.isSymbol() || IsCallable(v);
+}
+
+class CycleDetector
+{
+ public:
+ CycleDetector(StringifyContext* scx, HandleObject obj)
+ : stack(&scx->stack), obj_(obj) {
+ }
+
+ bool foundCycle(JSContext* cx) {
+ auto addPtr = stack.lookupForAdd(obj_);
+ if (addPtr) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_JSON_CYCLIC_VALUE);
+ return false;
+ }
+ if (!stack.add(addPtr, obj_)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+ return true;
+ }
+
+ ~CycleDetector() {
+ stack.remove(obj_);
+ }
+
+ private:
+ MutableHandle<ObjectSet> stack;
+ HandleObject obj_;
+};
+
+/* ES5 15.12.3 JO. */
+static bool
+JO(JSContext* cx, HandleObject obj, StringifyContext* scx)
+{
+ /*
+ * This method implements the JO algorithm in ES5 15.12.3, but:
+ *
+ * * The algorithm is somewhat reformulated to allow the final string to
+ * be streamed into a single buffer, rather than be created and copied
+ * into place incrementally as the ES5 algorithm specifies it. This
+ * requires moving portions of the Str call in 8a into this algorithm
+ * (and in JA as well).
+ */
+
+ MOZ_ASSERT_IF(scx->maybeSafely, obj->is<PlainObject>());
+
+ /* Steps 1-2, 11. */
+ CycleDetector detect(scx, obj);
+ if (!detect.foundCycle(cx))
+ return false;
+
+ if (!scx->sb.append('{'))
+ return false;
+
+ /* Steps 5-7. */
+ Maybe<AutoIdVector> ids;
+ const AutoIdVector* props;
+ if (scx->replacer && !scx->replacer->isCallable()) {
+ // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
+ // might have been a revocable proxy to an array. Such a proxy
+ // satisfies |IsArray|, but any side effect of JSON.stringify
+ // could revoke the proxy so that |!IsArray(scx->replacer)|. See
+ // bug 1196497.
+ props = &scx->propertyList;
+ } else {
+ MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0);
+ ids.emplace(cx);
+ if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr()))
+ return false;
+ props = ids.ptr();
+ }
+
+ /* My kingdom for not-quite-initialized-from-the-start references. */
+ const AutoIdVector& propertyList = *props;
+
+ /* Steps 8-10, 13. */
+ bool wroteMember = false;
+ RootedId id(cx);
+ for (size_t i = 0, len = propertyList.length(); i < len; i++) {
+ if (!CheckForInterrupt(cx))
+ return false;
+
+ /*
+ * Steps 8a-8b. Note that the call to Str is broken up into 1) getting
+ * the property; 2) processing for toJSON, calling the replacer, and
+ * handling boxed Number/String/Boolean objects; 3) filtering out
+ * values which process to |undefined|, and 4) stringifying all values
+ * which pass the filter.
+ */
+ id = propertyList[i];
+ RootedValue outputValue(cx);
+#ifdef DEBUG
+ if (scx->maybeSafely) {
+ RootedNativeObject nativeObj(cx, &obj->as<NativeObject>());
+ RootedShape prop(cx);
+ NativeLookupOwnPropertyNoResolve(cx, nativeObj, id, &prop);
+ MOZ_ASSERT(prop && prop->isDataDescriptor());
+ }
+#endif // DEBUG
+ if (!GetProperty(cx, obj, obj, id, &outputValue))
+ return false;
+ if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx))
+ return false;
+ if (IsFilteredValue(outputValue))
+ continue;
+
+ /* Output a comma unless this is the first member to write. */
+ if (wroteMember && !scx->sb.append(','))
+ return false;
+ wroteMember = true;
+
+ if (!WriteIndent(cx, scx, scx->depth))
+ return false;
+
+ JSString* s = IdToString(cx, id);
+ if (!s)
+ return false;
+
+ if (!Quote(cx, scx->sb, s) ||
+ !scx->sb.append(':') ||
+ !(scx->gap.empty() || scx->sb.append(' ')) ||
+ !Str(cx, outputValue, scx))
+ {
+ return false;
+ }
+ }
+
+ if (wroteMember && !WriteIndent(cx, scx, scx->depth - 1))
+ return false;
+
+ return scx->sb.append('}');
+}
+
+/* ES5 15.12.3 JA. */
+static bool
+JA(JSContext* cx, HandleObject obj, StringifyContext* scx)
+{
+ /*
+ * This method implements the JA algorithm in ES5 15.12.3, but:
+ *
+ * * The algorithm is somewhat reformulated to allow the final string to
+ * be streamed into a single buffer, rather than be created and copied
+ * into place incrementally as the ES5 algorithm specifies it. This
+ * requires moving portions of the Str call in 8a into this algorithm
+ * (and in JO as well).
+ */
+
+ /* Steps 1-2, 11. */
+ CycleDetector detect(scx, obj);
+ if (!detect.foundCycle(cx))
+ return false;
+
+ if (!scx->sb.append('['))
+ return false;
+
+ /* Step 6. */
+ uint32_t length;
+ if (!GetLengthProperty(cx, obj, &length))
+ return false;
+
+ /* Steps 7-10. */
+ if (length != 0) {
+ /* Steps 4, 10b(i). */
+ if (!WriteIndent(cx, scx, scx->depth))
+ return false;
+
+ /* Steps 7-10. */
+ RootedValue outputValue(cx);
+ for (uint32_t i = 0; i < length; i++) {
+ if (!CheckForInterrupt(cx))
+ return false;
+
+ /*
+ * Steps 8a-8c. Again note how the call to the spec's Str method
+ * is broken up into getting the property, running it past toJSON
+ * and the replacer and maybe unboxing, and interpreting some
+ * values as |null| in separate steps.
+ */
+#ifdef DEBUG
+ if (scx->maybeSafely) {
+ /*
+ * Trying to do a JS_AlreadyHasOwnElement runs the risk of
+ * hitting OOM on jsid creation. Let's just assert sanity for
+ * small enough indices.
+ */
+ MOZ_ASSERT(obj->is<ArrayObject>());
+ MOZ_ASSERT(obj->is<NativeObject>());
+ RootedNativeObject nativeObj(cx, &obj->as<NativeObject>());
+ if (i <= JSID_INT_MAX) {
+ MOZ_ASSERT(nativeObj->containsDenseElement(i) != nativeObj->isIndexed(),
+ "the array must either be small enough to remain "
+ "fully dense (and otherwise un-indexed), *or* "
+ "all its initially-dense elements were sparsified "
+ "and the object is indexed");
+ } else {
+ MOZ_ASSERT(obj->isIndexed());
+ }
+ }
+#endif
+ if (!GetElement(cx, obj, obj, i, &outputValue))
+ return false;
+ if (!PreprocessValue(cx, obj, i, &outputValue, scx))
+ return false;
+ if (IsFilteredValue(outputValue)) {
+ if (!scx->sb.append("null"))
+ return false;
+ } else {
+ if (!Str(cx, outputValue, scx))
+ return false;
+ }
+
+ /* Steps 3, 4, 10b(i). */
+ if (i < length - 1) {
+ if (!scx->sb.append(','))
+ return false;
+ if (!WriteIndent(cx, scx, scx->depth))
+ return false;
+ }
+ }
+
+ /* Step 10(b)(iii). */
+ if (!WriteIndent(cx, scx, scx->depth - 1))
+ return false;
+ }
+
+ return scx->sb.append(']');
+}
+
+static bool
+Str(JSContext* cx, const Value& v, StringifyContext* scx)
+{
+ /* Step 11 must be handled by the caller. */
+ MOZ_ASSERT(!IsFilteredValue(v));
+
+ JS_CHECK_RECURSION(cx, return false);
+
+ /*
+ * This method implements the Str algorithm in ES5 15.12.3, but:
+ *
+ * * We move property retrieval (step 1) into callers to stream the
+ * stringification process and avoid constantly copying strings.
+ * * We move the preprocessing in steps 2-4 into a helper function to
+ * allow both JO and JA to use this method. While JA could use it
+ * without this move, JO must omit any |undefined|-valued property per
+ * so it can't stream out a value using the Str method exactly as
+ * defined by ES5.
+ * * We move step 11 into callers, again to ease streaming.
+ */
+
+ /* Step 8. */
+ if (v.isString())
+ return Quote(cx, scx->sb, v.toString());
+
+ /* Step 5. */
+ if (v.isNull())
+ return scx->sb.append("null");
+
+ /* Steps 6-7. */
+ if (v.isBoolean())
+ return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false");
+
+ /* Step 9. */
+ if (v.isNumber()) {
+ if (v.isDouble()) {
+ if (!IsFinite(v.toDouble())) {
+ MOZ_ASSERT(!scx->maybeSafely,
+ "input JS::ToJSONMaybeSafely must not include "
+ "reachable non-finite numbers");
+ return scx->sb.append("null");
+ }
+ }
+
+ return NumberValueToStringBuffer(cx, v, scx->sb);
+ }
+
+ /* Step 10. */
+ MOZ_ASSERT(v.isObject());
+ RootedObject obj(cx, &v.toObject());
+
+ MOZ_ASSERT(!scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(),
+ "input to JS::ToJSONMaybeSafely must not include reachable "
+ "objects that are neither arrays nor plain objects");
+
+ scx->depth++;
+ auto dec = mozilla::MakeScopeExit([&] { scx->depth--; });
+
+ bool isArray;
+ if (!IsArray(cx, obj, &isArray))
+ return false;
+
+ return isArray ? JA(cx, obj, scx) : JO(cx, obj, scx);
+}
+
+/* ES6 24.3.2. */
+bool
+js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_, const Value& space_,
+ StringBuffer& sb, StringifyBehavior stringifyBehavior)
+{
+ RootedObject replacer(cx, replacer_);
+ RootedValue space(cx, space_);
+
+ MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe, space.isNull());
+ MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe, vp.isObject());
+ /**
+ * This uses MOZ_ASSERT, since it's actually asserting something jsapi
+ * consumers could get wrong, so needs a better error message.
+ */
+ MOZ_ASSERT(stringifyBehavior == StringifyBehavior::Normal ||
+ vp.toObject().is<PlainObject>() || vp.toObject().is<ArrayObject>(),
+ "input to JS::ToJSONMaybeSafely must be a plain object or array");
+
+ /* Step 4. */
+ AutoIdVector propertyList(cx);
+ if (replacer) {
+ bool isArray;
+ if (replacer->isCallable()) {
+ /* Step 4a(i): use replacer to transform values. */
+ } else if (!IsArray(cx, replacer, &isArray)) {
+ return false;
+ } else if (isArray) {
+ /* Step 4b(iii). */
+
+ /* Step 4b(iii)(2-3). */
+ uint32_t len;
+ if (!GetLengthProperty(cx, replacer, &len))
+ return false;
+
+ // Cap the initial size to a moderately small value. This avoids
+ // ridiculous over-allocation if an array with bogusly-huge length
+ // is passed in. If we end up having to add elements past this
+ // size, the set will naturally resize to accommodate them.
+ const uint32_t MaxInitialSize = 32;
+ Rooted<GCHashSet<jsid>> idSet(cx, GCHashSet<jsid>(cx));
+ if (!idSet.init(Min(len, MaxInitialSize)))
+ return false;
+
+ /* Step 4b(iii)(4). */
+ uint32_t k = 0;
+
+ /* Step 4b(iii)(5). */
+ RootedValue item(cx);
+ for (; k < len; k++) {
+ if (!CheckForInterrupt(cx))
+ return false;
+
+ /* Step 4b(iii)(5)(a-b). */
+ if (!GetElement(cx, replacer, replacer, k, &item))
+ return false;
+
+ RootedId id(cx);
+
+ /* Step 4b(iii)(5)(c-f). */
+ if (item.isNumber()) {
+ /* Step 4b(iii)(5)(e). */
+ int32_t n;
+ if (ValueFitsInInt32(item, &n) && INT_FITS_IN_JSID(n)) {
+ id = INT_TO_JSID(n);
+ } else {
+ if (!ValueToId<CanGC>(cx, item, &id))
+ return false;
+ }
+ } else {
+ bool shouldAdd = item.isString();
+ if (!shouldAdd) {
+ ESClass cls;
+ if (!GetClassOfValue(cx, item, &cls))
+ return false;
+
+ shouldAdd = cls == ESClass::String || cls == ESClass::Number;
+ }
+
+ if (shouldAdd) {
+ /* Step 4b(iii)(5)(f). */
+ if (!ValueToId<CanGC>(cx, item, &id))
+ return false;
+ } else {
+ /* Step 4b(iii)(5)(g). */
+ continue;
+ }
+ }
+
+ /* Step 4b(iii)(5)(g). */
+ auto p = idSet.lookupForAdd(id);
+ if (!p) {
+ /* Step 4b(iii)(5)(g)(i). */
+ if (!idSet.add(p, id) || !propertyList.append(id))
+ return false;
+ }
+ }
+ } else {
+ replacer = nullptr;
+ }
+ }
+
+ /* Step 5. */
+ if (space.isObject()) {
+ RootedObject spaceObj(cx, &space.toObject());
+
+ ESClass cls;
+ if (!GetBuiltinClass(cx, spaceObj, &cls))
+ return false;
+
+ if (cls == ESClass::Number) {
+ double d;
+ if (!ToNumber(cx, space, &d))
+ return false;
+ space = NumberValue(d);
+ } else if (cls == ESClass::String) {
+ JSString* str = ToStringSlow<CanGC>(cx, space);
+ if (!str)
+ return false;
+ space = StringValue(str);
+ }
+ }
+
+ StringBuffer gap(cx);
+
+ if (space.isNumber()) {
+ /* Step 6. */
+ double d;
+ MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
+ d = Min(10.0, d);
+ if (d >= 1 && !gap.appendN(' ', uint32_t(d)))
+ return false;
+ } else if (space.isString()) {
+ /* Step 7. */
+ JSLinearString* str = space.toString()->ensureLinear(cx);
+ if (!str)
+ return false;
+ size_t len = Min(size_t(10), str->length());
+ if (!gap.appendSubstring(str, 0, len))
+ return false;
+ } else {
+ /* Step 8. */
+ MOZ_ASSERT(gap.empty());
+ }
+
+ /* Step 9. */
+ RootedPlainObject wrapper(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!wrapper)
+ return false;
+
+ /* Steps 10-11. */
+ RootedId emptyId(cx, NameToId(cx->names().empty));
+ if (!NativeDefineProperty(cx, wrapper, emptyId, vp, nullptr, nullptr, JSPROP_ENUMERATE))
+ return false;
+
+ /* Step 12. */
+ StringifyContext scx(cx, sb, gap, replacer, propertyList,
+ stringifyBehavior == StringifyBehavior::RestrictedSafe);
+ if (!scx.init())
+ return false;
+ if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx))
+ return false;
+ if (IsFilteredValue(vp))
+ return true;
+
+ return Str(cx, vp, &scx);
+}
+
+/* ES5 15.12.2 Walk. */
+static bool
+Walk(JSContext* cx, HandleObject holder, HandleId name, HandleValue reviver, MutableHandleValue vp)
+{
+ JS_CHECK_RECURSION(cx, return false);
+
+ /* Step 1. */
+ RootedValue val(cx);
+ if (!GetProperty(cx, holder, holder, name, &val))
+ return false;
+
+ /* Step 2. */
+ if (val.isObject()) {
+ RootedObject obj(cx, &val.toObject());
+
+ bool isArray;
+ if (!IsArray(cx, obj, &isArray))
+ return false;
+
+ if (isArray) {
+ /* Step 2a(ii). */
+ uint32_t length;
+ if (!GetLengthProperty(cx, obj, &length))
+ return false;
+
+ /* Step 2a(i), 2a(iii-iv). */
+ RootedId id(cx);
+ RootedValue newElement(cx);
+ for (uint32_t i = 0; i < length; i++) {
+ if (!CheckForInterrupt(cx))
+ return false;
+
+ if (!IndexToId(cx, i, &id))
+ return false;
+
+ /* Step 2a(iii)(1). */
+ if (!Walk(cx, obj, id, reviver, &newElement))
+ return false;
+
+ ObjectOpResult ignored;
+ if (newElement.isUndefined()) {
+ /* Step 2a(iii)(2). The spec deliberately ignores strict failure. */
+ if (!DeleteProperty(cx, obj, id, ignored))
+ return false;
+ } else {
+ /* Step 2a(iii)(3). The spec deliberately ignores strict failure. */
+ Rooted<PropertyDescriptor> desc(cx);
+ desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
+ if (!DefineProperty(cx, obj, id, desc, ignored))
+ return false;
+ }
+ }
+ } else {
+ /* Step 2b(i). */
+ AutoIdVector keys(cx);
+ if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys))
+ return false;
+
+ /* Step 2b(ii). */
+ RootedId id(cx);
+ RootedValue newElement(cx);
+ for (size_t i = 0, len = keys.length(); i < len; i++) {
+ if (!CheckForInterrupt(cx))
+ return false;
+
+ /* Step 2b(ii)(1). */
+ id = keys[i];
+ if (!Walk(cx, obj, id, reviver, &newElement))
+ return false;
+
+ ObjectOpResult ignored;
+ if (newElement.isUndefined()) {
+ /* Step 2b(ii)(2). The spec deliberately ignores strict failure. */
+ if (!DeleteProperty(cx, obj, id, ignored))
+ return false;
+ } else {
+ /* Step 2b(ii)(3). The spec deliberately ignores strict failure. */
+ Rooted<PropertyDescriptor> desc(cx);
+ desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
+ if (!DefineProperty(cx, obj, id, desc, ignored))
+ return false;
+ }
+ }
+ }
+ }
+
+ /* Step 3. */
+ RootedString key(cx, IdToString(cx, name));
+ if (!key)
+ return false;
+
+ RootedValue keyVal(cx, StringValue(key));
+ return js::Call(cx, reviver, holder, keyVal, val, vp);
+}
+
+static bool
+Revive(JSContext* cx, HandleValue reviver, MutableHandleValue vp)
+{
+ RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+ if (!obj)
+ return false;
+
+ if (!DefineProperty(cx, obj, cx->names().empty, vp))
+ return false;
+
+ Rooted<jsid> id(cx, NameToId(cx->names().empty));
+ return Walk(cx, obj, id, reviver, vp);
+}
+
+template <typename CharT>
+bool
+js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const CharT> chars, HandleValue reviver,
+ MutableHandleValue vp)
+{
+ /* 15.12.2 steps 2-3. */
+ Rooted<JSONParser<CharT>> parser(cx, JSONParser<CharT>(cx, chars));
+ if (!parser.parse(vp))
+ return false;
+
+ /* 15.12.2 steps 4-5. */
+ if (IsCallable(reviver))
+ return Revive(cx, reviver, vp);
+ return true;
+}
+
+template bool
+js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const Latin1Char> chars,
+ HandleValue reviver, MutableHandleValue vp);
+
+template bool
+js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const char16_t> chars, HandleValue reviver,
+ MutableHandleValue vp);
+
+#if JS_HAS_TOSOURCE
+static bool
+json_toSource(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().JSON);
+ return true;
+}
+#endif
+
+/* ES5 15.12.2. */
+static bool
+json_parse(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ /* Step 1. */
+ JSString* str = (args.length() >= 1)
+ ? ToString<CanGC>(cx, args[0])
+ : cx->names().undefined;
+ if (!str)
+ return false;
+
+ JSLinearString* linear = str->ensureLinear(cx);
+ if (!linear)
+ return false;
+
+ AutoStableStringChars linearChars(cx);
+ if (!linearChars.init(cx, linear))
+ return false;
+
+ HandleValue reviver = args.get(1);
+
+ /* Steps 2-5. */
+ return linearChars.isLatin1()
+ ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver, args.rval())
+ : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver, args.rval());
+}
+
+/* ES6 24.3.2. */
+bool
+json_stringify(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ RootedObject replacer(cx, args.get(1).isObject() ? &args[1].toObject() : nullptr);
+ RootedValue value(cx, args.get(0));
+ RootedValue space(cx, args.get(2));
+
+ StringBuffer sb(cx);
+ if (!Stringify(cx, &value, replacer, space, sb, StringifyBehavior::Normal))
+ return false;
+
+ // XXX This can never happen to nsJSON.cpp, but the JSON object
+ // needs to support returning undefined. So this is a little awkward
+ // for the API, because we want to support streaming writers.
+ if (!sb.empty()) {
+ JSString* str = sb.finishString();
+ if (!str)
+ return false;
+ args.rval().setString(str);
+ } else {
+ args.rval().setUndefined();
+ }
+
+ return true;
+}
+
+static const JSFunctionSpec json_static_methods[] = {
+#if JS_HAS_TOSOURCE
+ JS_FN(js_toSource_str, json_toSource, 0, 0),
+#endif
+ JS_FN("parse", json_parse, 2, 0),
+ JS_FN("stringify", json_stringify, 3, 0),
+ JS_FS_END
+};
+
+JSObject*
+js::InitJSONClass(JSContext* cx, HandleObject obj)
+{
+ Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>());
+
+ RootedObject proto(cx, global->getOrCreateObjectPrototype(cx));
+ if (!proto)
+ return nullptr;
+ RootedObject JSON(cx, NewObjectWithGivenProto(cx, &JSONClass, proto, SingletonObject));
+ if (!JSON)
+ return nullptr;
+
+ if (!JS_DefineProperty(cx, global, js_JSON_str, JSON, JSPROP_RESOLVING,
+ JS_STUBGETTER, JS_STUBSETTER))
+ {
+ return nullptr;
+ }
+
+ if (!JS_DefineFunctions(cx, JSON, json_static_methods))
+ return nullptr;
+
+ if (!DefineToStringTag(cx, JSON, cx->names().JSON))
+ return nullptr;
+
+ global->setConstructor(JSProto_JSON, ObjectValue(*JSON));
+
+ return JSON;
+}