summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/JSON.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin/JSON.cpp')
-rw-r--r--js/src/builtin/JSON.cpp2163
1 files changed, 2163 insertions, 0 deletions
diff --git a/js/src/builtin/JSON.cpp b/js/src/builtin/JSON.cpp
new file mode 100644
index 0000000000..dbfab8b43a
--- /dev/null
+++ b/js/src/builtin/JSON.cpp
@@ -0,0 +1,2163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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 "builtin/JSON.h"
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Range.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Variant.h"
+
+#include <algorithm>
+
+#include "jsnum.h"
+#include "jstypes.h"
+
+#include "builtin/Array.h"
+#include "builtin/BigInt.h"
+#include "builtin/ParseRecordObject.h"
+#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
+#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit
+#include "js/Object.h" // JS::GetBuiltinClass
+#include "js/PropertySpec.h"
+#include "js/StableStringChars.h"
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "util/StringBuffer.h"
+#include "vm/BooleanObject.h" // js::BooleanObject
+#include "vm/Interpreter.h"
+#include "vm/Iteration.h"
+#include "vm/JSAtomUtils.h" // ToAtom
+#include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/JSONParser.h"
+#include "vm/NativeObject.h"
+#include "vm/NumberObject.h" // js::NumberObject
+#include "vm/PlainObject.h" // js::PlainObject
+#include "vm/StringObject.h" // js::StringObject
+#ifdef ENABLE_RECORD_TUPLE
+# include "builtin/RecordObject.h"
+# include "builtin/TupleObject.h"
+# include "vm/RecordType.h"
+#endif
+
+#include "builtin/Array-inl.h"
+#include "vm/GeckoProfiler-inl.h"
+#include "vm/JSAtomUtils-inl.h" // AtomToId, PrimitiveValueToId, IndexToId, IdToString,
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+using mozilla::AsVariant;
+using mozilla::CheckedInt;
+using mozilla::Maybe;
+using mozilla::RangedPtr;
+using mozilla::Variant;
+
+using JS::AutoStableStringChars;
+
+/* https://262.ecma-international.org/14.0/#sec-quotejsonstring
+ * Requires that the destination has enough space allocated for src after
+ * escaping (that is, `2 + 6 * (srcEnd - srcBegin)` characters).
+ */
+template <typename SrcCharT, typename DstCharT>
+static MOZ_ALWAYS_INLINE RangedPtr<DstCharT> InfallibleQuoteJSONString(
+ RangedPtr<const SrcCharT> srcBegin, RangedPtr<const SrcCharT> srcEnd,
+ RangedPtr<DstCharT> dstPtr) {
+ // Maps characters < 256 to the value that must follow the '\\' in the quoted
+ // string. Entries with 'u' are handled as \\u00xy, and entries with 0 are not
+ // escaped in any way. Characters >= 256 are all assumed to be unescaped.
+ static const Latin1Char escapeLookup[256] = {
+ // clang-format off
+ 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'b', 't',
+ 'n', 'u', 'f', 'r', 'u', 'u', 'u', 'u', 'u', 'u',
+ 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u',
+ 'u', 'u', 0, 0, '\"', 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, '\\', // rest are all zeros
+ // clang-format on
+ };
+
+ /* Step 1. */
+ *dstPtr++ = '"';
+
+ auto ToLowerHex = [](uint8_t u) {
+ MOZ_ASSERT(u <= 0xF);
+ return "0123456789abcdef"[u];
+ };
+
+ /* Step 2. */
+ while (srcBegin != srcEnd) {
+ const SrcCharT c = *srcBegin++;
+
+ // Handle the Latin-1 cases.
+ if (MOZ_LIKELY(c < sizeof(escapeLookup))) {
+ Latin1Char escaped = escapeLookup[c];
+
+ // Directly copy non-escaped code points.
+ if (escaped == 0) {
+ *dstPtr++ = c;
+ continue;
+ }
+
+ // Escape the rest, elaborating Unicode escapes when needed.
+ *dstPtr++ = '\\';
+ *dstPtr++ = escaped;
+ if (escaped == 'u') {
+ *dstPtr++ = '0';
+ *dstPtr++ = '0';
+
+ uint8_t x = c >> 4;
+ MOZ_ASSERT(x < 10);
+ *dstPtr++ = '0' + x;
+
+ *dstPtr++ = ToLowerHex(c & 0xF);
+ }
+
+ continue;
+ }
+
+ // Non-ASCII non-surrogates are directly copied.
+ if (!unicode::IsSurrogate(c)) {
+ *dstPtr++ = c;
+ continue;
+ }
+
+ // So too for complete surrogate pairs.
+ if (MOZ_LIKELY(unicode::IsLeadSurrogate(c) && srcBegin < srcEnd &&
+ unicode::IsTrailSurrogate(*srcBegin))) {
+ *dstPtr++ = c;
+ *dstPtr++ = *srcBegin++;
+ continue;
+ }
+
+ // But lone surrogates are Unicode-escaped.
+ char32_t as32 = char32_t(c);
+ *dstPtr++ = '\\';
+ *dstPtr++ = 'u';
+ *dstPtr++ = ToLowerHex(as32 >> 12);
+ *dstPtr++ = ToLowerHex((as32 >> 8) & 0xF);
+ *dstPtr++ = ToLowerHex((as32 >> 4) & 0xF);
+ *dstPtr++ = ToLowerHex(as32 & 0xF);
+ }
+
+ /* Steps 3-4. */
+ *dstPtr++ = '"';
+ return dstPtr;
+}
+
+template <typename SrcCharT, typename DstCharT>
+static size_t QuoteJSONStringHelper(const JSLinearString& linear,
+ StringBuffer& sb, size_t sbOffset) {
+ size_t len = linear.length();
+
+ JS::AutoCheckCannotGC nogc;
+ RangedPtr<const SrcCharT> srcBegin{linear.chars<SrcCharT>(nogc), len};
+ RangedPtr<DstCharT> dstBegin{sb.begin<DstCharT>(), sb.begin<DstCharT>(),
+ sb.end<DstCharT>()};
+ RangedPtr<DstCharT> dstEnd =
+ InfallibleQuoteJSONString(srcBegin, srcBegin + len, dstBegin + sbOffset);
+
+ return dstEnd - dstBegin;
+}
+
+static bool QuoteJSONString(JSContext* cx, StringBuffer& sb, JSString* str) {
+ JSLinearString* linear = str->ensureLinear(cx);
+ if (!linear) {
+ return false;
+ }
+
+ if (linear->hasTwoByteChars() && !sb.ensureTwoByteChars()) {
+ return false;
+ }
+
+ // We resize the backing buffer to the maximum size we could possibly need,
+ // write the escaped string into it, and shrink it back to the size we ended
+ // up needing.
+
+ size_t len = linear->length();
+ size_t sbInitialLen = sb.length();
+
+ CheckedInt<size_t> reservedLen = CheckedInt<size_t>(len) * 6 + 2;
+ if (MOZ_UNLIKELY(!reservedLen.isValid())) {
+ ReportAllocationOverflow(cx);
+ return false;
+ }
+
+ if (!sb.growByUninitialized(reservedLen.value())) {
+ return false;
+ }
+
+ size_t newSize;
+
+ if (linear->hasTwoByteChars()) {
+ newSize =
+ QuoteJSONStringHelper<char16_t, char16_t>(*linear, sb, sbInitialLen);
+ } else if (sb.isUnderlyingBufferLatin1()) {
+ newSize = QuoteJSONStringHelper<Latin1Char, Latin1Char>(*linear, sb,
+ sbInitialLen);
+ } else {
+ newSize =
+ QuoteJSONStringHelper<Latin1Char, char16_t>(*linear, sb, sbInitialLen);
+ }
+
+ sb.shrinkTo(newSize);
+
+ return true;
+}
+
+namespace {
+
+using ObjectVector = GCVector<JSObject*, 8>;
+
+class StringifyContext {
+ public:
+ StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
+ HandleObject replacer, const RootedIdVector& propertyList,
+ bool maybeSafely)
+ : sb(sb),
+ gap(gap),
+ replacer(cx, replacer),
+ stack(cx, ObjectVector(cx)),
+ propertyList(propertyList),
+ depth(0),
+ maybeSafely(maybeSafely) {
+ MOZ_ASSERT_IF(maybeSafely, !replacer);
+ MOZ_ASSERT_IF(maybeSafely, gap.empty());
+ }
+
+ StringBuffer& sb;
+ const StringBuffer& gap;
+ RootedObject replacer;
+ Rooted<ObjectVector> stack;
+ const RootedIdVector& propertyList;
+ uint32_t depth;
+ bool maybeSafely;
+};
+
+} /* anonymous namespace */
+
+static bool SerializeJSONProperty(JSContext* cx, const Value& v,
+ StringifyContext* scx);
+
+static bool WriteIndent(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 */
+
+/*
+ * https://262.ecma-international.org/14.0/#sec-serializejsonproperty, steps
+ * 2-4, extracted to enable preprocessing of property values when stringifying
+ * objects in SerializeJSONObject.
+ */
+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. Modified by BigInt spec 6.1 to check for a toJSON method on the
+ // BigInt prototype when the value is a BigInt, and to pass the BigInt
+ // primitive value as receiver.
+ if (vp.isObject() || vp.isBigInt()) {
+ RootedValue toJSON(cx);
+ RootedObject obj(cx, JS::ToObject(cx, vp));
+ if (!obj) {
+ return false;
+ }
+
+ if (!GetProperty(cx, obj, vp, 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()) {
+ MOZ_ASSERT(holder != nullptr,
+ "holder object must be present when replacer is callable");
+
+ 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 (!JS::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 || cls == ESClass::BigInt ||
+ IF_RECORD_TUPLE(
+ obj->is<RecordObject>() || obj->is<TupleObject>(), false)) {
+ if (!Unbox(cx, obj, vp)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+/*
+ * Determines whether a value which has passed by
+ * https://262.ecma-international.org/14.0/#sec-serializejsonproperty steps
+ * 1-4's gauntlet will result in SerializeJSONProperty 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) {
+ MOZ_ASSERT_IF(v.isMagic(), v.isMagic(JS_ELEMENTS_HOLE));
+ return v.isUndefined() || v.isSymbol() || v.isMagic() || IsCallable(v);
+}
+
+class CycleDetector {
+ public:
+ CycleDetector(StringifyContext* scx, HandleObject obj)
+ : stack_(&scx->stack), obj_(obj), appended_(false) {}
+
+ MOZ_ALWAYS_INLINE bool foundCycle(JSContext* cx) {
+ JSObject* obj = obj_;
+ for (JSObject* obj2 : stack_) {
+ if (MOZ_UNLIKELY(obj == obj2)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_JSON_CYCLIC_VALUE);
+ return false;
+ }
+ }
+ appended_ = stack_.append(obj);
+ return appended_;
+ }
+
+ ~CycleDetector() {
+ if (MOZ_LIKELY(appended_)) {
+ MOZ_ASSERT(stack_.back() == obj_);
+ stack_.popBack();
+ }
+ }
+
+ private:
+ MutableHandle<ObjectVector> stack_;
+ HandleObject obj_;
+ bool appended_;
+};
+
+#ifdef ENABLE_RECORD_TUPLE
+enum class JOType { Record, Object };
+template <JOType type = JOType::Object>
+#endif
+/* https://262.ecma-international.org/14.0/#sec-serializejsonobject */
+static bool SerializeJSONObject(JSContext* cx, HandleObject obj,
+ StringifyContext* scx) {
+ /*
+ * This method implements the SerializeJSONObject algorithm, 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 algorithm specifies it. This
+ * requires moving portions of the SerializeJSONProperty call in 8a into
+ * this algorithm (and in SerializeJSONArray as well).
+ */
+
+#ifdef ENABLE_RECORD_TUPLE
+ RecordType* rec;
+
+ if constexpr (type == JOType::Record) {
+ MOZ_ASSERT(obj->is<RecordType>());
+ rec = &obj->as<RecordType>();
+ } else {
+ MOZ_ASSERT(!IsExtendedPrimitive(*obj));
+ }
+#endif
+ 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<RootedIdVector> ids;
+ const RootedIdVector* 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 RootedIdVector& 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 SerializeJSONProperty 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) {
+ PropertyResult prop;
+ if (!NativeLookupOwnPropertyNoResolve(cx, &obj->as<NativeObject>(), id,
+ &prop)) {
+ return false;
+ }
+ MOZ_ASSERT(prop.isNativeProperty() &&
+ prop.propertyInfo().isDataDescriptor());
+ }
+#endif // DEBUG
+
+#ifdef ENABLE_RECORD_TUPLE
+ if constexpr (type == JOType::Record) {
+ MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, id, &outputValue));
+ } else
+#endif
+ {
+ RootedValue objValue(cx, ObjectValue(*obj));
+ if (!GetProperty(cx, obj, objValue, 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(scx, scx->depth)) {
+ return false;
+ }
+
+ JSString* s = IdToString(cx, id);
+ if (!s) {
+ return false;
+ }
+
+ if (!QuoteJSONString(cx, scx->sb, s) || !scx->sb.append(':') ||
+ !(scx->gap.empty() || scx->sb.append(' ')) ||
+ !SerializeJSONProperty(cx, outputValue, scx)) {
+ return false;
+ }
+ }
+
+ if (wroteMember && !WriteIndent(scx, scx->depth - 1)) {
+ return false;
+ }
+
+ return scx->sb.append('}');
+}
+
+// For JSON.stringify and JSON.parse with a reviver function, we need to know
+// the length of an object for which JS::IsArray returned true. This must be
+// either an ArrayObject or a proxy wrapping one.
+static MOZ_ALWAYS_INLINE bool GetLengthPropertyForArrayLike(JSContext* cx,
+ HandleObject obj,
+ uint32_t* lengthp) {
+ if (MOZ_LIKELY(obj->is<ArrayObject>())) {
+ *lengthp = obj->as<ArrayObject>().length();
+ return true;
+ }
+#ifdef ENABLE_RECORD_TUPLE
+ if (obj->is<TupleType>()) {
+ *lengthp = obj->as<TupleType>().length();
+ return true;
+ }
+#endif
+
+ MOZ_ASSERT(obj->is<ProxyObject>());
+
+ uint64_t len = 0;
+ if (!GetLengthProperty(cx, obj, &len)) {
+ return false;
+ }
+
+ // A scripted proxy wrapping an array can return a length value larger than
+ // UINT32_MAX. Stringification will likely report an OOM in this case. Match
+ // other JS engines and report an early error in this case, although
+ // technically this is observable, for example when stringifying with a
+ // replacer function.
+ if (len > UINT32_MAX) {
+ ReportAllocationOverflow(cx);
+ return false;
+ }
+
+ *lengthp = uint32_t(len);
+ return true;
+}
+
+/* https://262.ecma-international.org/14.0/#sec-serializejsonarray */
+static bool SerializeJSONArray(JSContext* cx, HandleObject obj,
+ StringifyContext* scx) {
+ /*
+ * This method implements the SerializeJSONArray algorithm, 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 algorithm specifies it. This
+ * requires moving portions of the SerializeJSONProperty call in 8a into
+ * this algorithm (and in SerializeJSONObject 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 (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
+ return false;
+ }
+
+ /* Steps 7-10. */
+ if (length != 0) {
+ /* Steps 4, 10b(i). */
+ if (!WriteIndent(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
+ * SerializeJSONProperty 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>());
+ Rooted<NativeObject*> nativeObj(cx, &obj->as<NativeObject>());
+ if (i <= PropertyKey::IntMax) {
+ 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(nativeObj->isIndexed());
+ }
+ }
+#endif
+ if (!GetElement(cx, 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 (!SerializeJSONProperty(cx, outputValue, scx)) {
+ return false;
+ }
+ }
+
+ /* Steps 3, 4, 10b(i). */
+ if (i < length - 1) {
+ if (!scx->sb.append(',')) {
+ return false;
+ }
+ if (!WriteIndent(scx, scx->depth)) {
+ return false;
+ }
+ }
+ }
+
+ /* Step 10(b)(iii). */
+ if (!WriteIndent(scx, scx->depth - 1)) {
+ return false;
+ }
+ }
+
+ return scx->sb.append(']');
+}
+
+/* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
+static bool SerializeJSONProperty(JSContext* cx, const Value& v,
+ StringifyContext* scx) {
+ /* Step 12 must be handled by the caller. */
+ MOZ_ASSERT(!IsFilteredValue(v));
+
+ /*
+ * This method implements the SerializeJSONProperty algorithm, 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 SerializeJSONObject and SerializeJSONArray to use this
+ * method. While SerializeJSONArray could use it without this move,
+ * SerializeJSONObject must omit any |undefined|-valued property per so it
+ * can't stream out a value using the SerializeJSONProperty method exactly as
+ * defined by the spec.
+ * * We move step 12 into callers, again to ease streaming.
+ */
+
+ /* Step 8. */
+ if (v.isString()) {
+ return QuoteJSONString(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 (!std::isfinite(v.toDouble())) {
+ MOZ_ASSERT(!scx->maybeSafely,
+ "input JS::ToJSONMaybeSafely must not include "
+ "reachable non-finite numbers");
+ return scx->sb.append("null");
+ }
+ }
+
+ return NumberValueToStringBuffer(v, scx->sb);
+ }
+
+ /* Step 10. */
+ if (v.isBigInt()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_BIGINT_NOT_SERIALIZABLE);
+ return false;
+ }
+
+ AutoCheckRecursionLimit recursion(cx);
+ if (!recursion.check(cx)) {
+ return false;
+ }
+
+ /* Step 11. */
+ MOZ_ASSERT(v.hasObjectPayload());
+ RootedObject obj(cx, &v.getObjectPayload());
+
+ 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--; });
+
+#ifdef ENABLE_RECORD_TUPLE
+ if (v.isExtendedPrimitive()) {
+ if (obj->is<RecordType>()) {
+ return SerializeJSONObject<JOType::Record>(cx, obj, scx);
+ }
+ if (obj->is<TupleType>()) {
+ return SerializeJSONArray(cx, obj, scx);
+ }
+ MOZ_CRASH("Unexpected extended primitive - boxes cannot be stringified.");
+ }
+#endif
+
+ bool isArray;
+ if (!IsArray(cx, obj, &isArray)) {
+ return false;
+ }
+
+ return isArray ? SerializeJSONArray(cx, obj, scx)
+ : SerializeJSONObject(cx, obj, scx);
+}
+
+static bool CanFastStringifyObject(NativeObject* obj) {
+ if (ClassCanHaveExtraEnumeratedProperties(obj->getClass())) {
+ return false;
+ }
+
+ if (obj->is<ArrayObject>()) {
+ // Arrays will look up all keys [0..length) so disallow anything that could
+ // find those keys anywhere but in the dense elements.
+ if (!IsPackedArray(obj) && ObjectMayHaveExtraIndexedProperties(obj)) {
+ return false;
+ }
+ } else {
+ // Non-Arrays will only look at own properties, but still disallow any
+ // indexed properties other than in the dense elements because they would
+ // require sorting.
+ if (ObjectMayHaveExtraIndexedOwnProperties(obj)) {
+ return false;
+ }
+ }
+
+ // Only used for internal environment objects that should never be passed to
+ // JSON.stringify.
+ MOZ_ASSERT(!obj->getOpsLookupProperty());
+
+#ifdef ENABLE_RECORD_TUPLE
+ if (ObjectValue(*obj).isExtendedPrimitive()) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+#define FOR_EACH_STRINGIFY_BAIL_REASON(MACRO) \
+ MACRO(NO_REASON) \
+ MACRO(INELIGIBLE_OBJECT) \
+ MACRO(DEEP_RECURSION) \
+ MACRO(NON_DATA_PROPERTY) \
+ MACRO(TOO_MANY_PROPERTIES) \
+ MACRO(BIGINT) \
+ MACRO(API) \
+ MACRO(HAVE_REPLACER) \
+ MACRO(HAVE_SPACE) \
+ MACRO(PRIMITIVE) \
+ MACRO(HAVE_TOJSON) \
+ MACRO(IMPURE_LOOKUP) \
+ MACRO(INTERRUPT)
+
+enum class BailReason : uint8_t {
+#define DECLARE_ENUM(name) name,
+ FOR_EACH_STRINGIFY_BAIL_REASON(DECLARE_ENUM)
+#undef DECLARE_ENUM
+};
+
+static const char* DescribeStringifyBailReason(BailReason whySlow) {
+ switch (whySlow) {
+#define ENUM_NAME(name) \
+ case BailReason::name: \
+ return #name;
+ FOR_EACH_STRINGIFY_BAIL_REASON(ENUM_NAME)
+#undef ENUM_NAME
+ default:
+ return "Unknown";
+ }
+}
+
+// Iterator over all the dense elements of an object. Used
+// for both Arrays and non-Arrays.
+class DenseElementsIteratorForJSON {
+ HeapSlotArray elements;
+ uint32_t element;
+
+ // Arrays can have a length less than getDenseInitializedLength(), in which
+ // case the remaining Array elements are treated as UndefinedValue.
+ uint32_t numElements;
+ uint32_t length;
+
+ public:
+ explicit DenseElementsIteratorForJSON(NativeObject* nobj)
+ : elements(nobj->getDenseElements()),
+ element(0),
+ numElements(nobj->getDenseInitializedLength()) {
+ length = nobj->is<ArrayObject>() ? nobj->as<ArrayObject>().length()
+ : numElements;
+ }
+
+ bool done() const { return element == length; }
+
+ Value next() {
+ // For Arrays, steps 6-8 of
+ // https://262.ecma-international.org/14.0/#sec-serializejsonarray. For
+ // non-Arrays, step 6a of
+ // https://262.ecma-international.org/14.0/#sec-serializejsonobject
+ // following the order from
+ // https://262.ecma-international.org/14.0/#sec-ordinaryownpropertykeys
+
+ MOZ_ASSERT(!done());
+ auto i = element++;
+ // Consider specializing the iterator for Arrays vs non-Arrays to avoid this
+ // branch.
+ return i < numElements ? elements.begin()[i] : UndefinedValue();
+ }
+
+ uint32_t getIndex() const { return element; }
+};
+
+// An iterator over the non-element properties of a Shape, returned in forward
+// (creation) order. Note that it is fallible, so after iteration is complete
+// isOverflowed() should be called to verify that the results are actually
+// complete.
+
+class ShapePropertyForwardIterNoGC {
+ // Pointer to the current PropMap with length and an index within it.
+ PropMap* map_;
+ uint32_t mapLength_;
+ uint32_t i_ = 0;
+
+ // Stack of PropMaps to iterate through, oldest properties on top. The current
+ // map (map_, above) is never on this stack.
+ mozilla::Vector<PropMap*> stack_;
+
+ const NativeShape* shape_;
+
+ MOZ_ALWAYS_INLINE void settle() {
+ while (true) {
+ if (MOZ_UNLIKELY(i_ == mapLength_)) {
+ i_ = 0;
+ if (stack_.empty()) {
+ mapLength_ = 0; // Done
+ return;
+ }
+ map_ = stack_.back();
+ stack_.popBack();
+ mapLength_ =
+ stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
+ } else if (MOZ_UNLIKELY(shape_->isDictionary() && !map_->hasKey(i_))) {
+ // Dictionary maps can have "holes" for removed properties, so keep
+ // going until we find a non-hole slot.
+ i_++;
+ } else {
+ return;
+ }
+ }
+ }
+
+ public:
+ explicit ShapePropertyForwardIterNoGC(NativeShape* shape) : shape_(shape) {
+ // Set map_ to the PropMap containing the first property (the deepest map in
+ // the previous() chain). Push pointers to all other PropMaps onto stack_.
+ map_ = shape->propMap();
+ if (!map_) {
+ // No properties.
+ i_ = mapLength_ = 0;
+ return;
+ }
+ while (map_->hasPrevious()) {
+ if (!stack_.append(map_)) {
+ // Overflowed.
+ i_ = mapLength_ = UINT32_MAX;
+ return;
+ }
+ map_ = map_->asLinked()->previous();
+ }
+
+ // Set mapLength_ to the number of properties in map_ (including dictionary
+ // holes, if any.)
+ mapLength_ = stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
+
+ settle();
+ }
+
+ bool done() const { return i_ == mapLength_; }
+ bool isOverflowed() const { return i_ == UINT32_MAX; }
+
+ void operator++(int) {
+ MOZ_ASSERT(!done());
+ i_++;
+ settle();
+ }
+
+ PropertyInfoWithKey get() const {
+ MOZ_ASSERT(!done());
+ return map_->getPropertyInfoWithKey(i_);
+ }
+
+ PropertyInfoWithKey operator*() const { return get(); }
+
+ // Fake pointer struct to make operator-> work.
+ // See https://stackoverflow.com/a/52856349.
+ struct FakePtr {
+ PropertyInfoWithKey val_;
+ const PropertyInfoWithKey* operator->() const { return &val_; }
+ };
+ FakePtr operator->() const { return {get()}; }
+};
+
+// Iterator over EnumerableOwnProperties
+// https://262.ecma-international.org/14.0/#sec-enumerableownproperties
+// that fails if it encounters any accessor properties, as they are not handled
+// by JSON FastSerializeJSONProperty, or if it sees too many properties on one
+// object.
+class OwnNonIndexKeysIterForJSON {
+ ShapePropertyForwardIterNoGC shapeIter;
+ bool done_ = false;
+ BailReason fastFailed_ = BailReason::NO_REASON;
+
+ void settle() {
+ // Skip over any non-enumerable or Symbol properties, and permanently fail
+ // if any enumerable non-data properties are encountered.
+ for (; !shapeIter.done(); shapeIter++) {
+ if (!shapeIter->enumerable()) {
+ continue;
+ }
+ if (!shapeIter->isDataProperty()) {
+ fastFailed_ = BailReason::NON_DATA_PROPERTY;
+ done_ = true;
+ return;
+ }
+ PropertyKey id = shapeIter->key();
+ if (!id.isSymbol()) {
+ return;
+ }
+ }
+ done_ = true;
+ }
+
+ public:
+ explicit OwnNonIndexKeysIterForJSON(const NativeObject* nobj)
+ : shapeIter(nobj->shape()) {
+ if (MOZ_UNLIKELY(shapeIter.isOverflowed())) {
+ fastFailed_ = BailReason::TOO_MANY_PROPERTIES;
+ done_ = true;
+ return;
+ }
+ if (!nobj->hasEnumerableProperty()) {
+ // Non-Arrays with no enumerable properties can just be skipped.
+ MOZ_ASSERT(!nobj->is<ArrayObject>());
+ done_ = true;
+ return;
+ }
+ settle();
+ }
+
+ bool done() const { return done_ || shapeIter.done(); }
+ BailReason cannotFastStringify() const { return fastFailed_; }
+
+ PropertyInfoWithKey next() {
+ MOZ_ASSERT(!done());
+ PropertyInfoWithKey prop = shapeIter.get();
+ shapeIter++;
+ settle();
+ return prop;
+ }
+};
+
+// Steps from https://262.ecma-international.org/14.0/#sec-serializejsonproperty
+static bool EmitSimpleValue(JSContext* cx, StringBuffer& sb, const Value& v) {
+ /* Step 8. */
+ if (v.isString()) {
+ return QuoteJSONString(cx, sb, v.toString());
+ }
+
+ /* Step 5. */
+ if (v.isNull()) {
+ return sb.append("null");
+ }
+
+ /* Steps 6-7. */
+ if (v.isBoolean()) {
+ return v.toBoolean() ? sb.append("true") : sb.append("false");
+ }
+
+ /* Step 9. */
+ if (v.isNumber()) {
+ if (v.isDouble()) {
+ if (!std::isfinite(v.toDouble())) {
+ return sb.append("null");
+ }
+ }
+
+ return NumberValueToStringBuffer(v, sb);
+ }
+
+ // Unrepresentable values.
+ if (v.isUndefined() || v.isMagic()) {
+ MOZ_ASSERT_IF(v.isMagic(), v.isMagic(JS_ELEMENTS_HOLE));
+ return sb.append("null");
+ }
+
+ /* Step 10. */
+ MOZ_CRASH("should have validated printable simple value already");
+}
+
+// https://262.ecma-international.org/14.0/#sec-serializejsonproperty step 8b
+// where K is an integer index.
+static bool EmitQuotedIndexColon(StringBuffer& sb, uint32_t index) {
+ Int32ToCStringBuf cbuf;
+ size_t cstrlen;
+ const char* cstr = ::Int32ToCString(&cbuf, index, &cstrlen);
+ if (!sb.reserve(sb.length() + 1 + cstrlen + 1 + 1)) {
+ return false;
+ }
+ sb.infallibleAppend('"');
+ sb.infallibleAppend(cstr, cstrlen);
+ sb.infallibleAppend('"');
+ sb.infallibleAppend(':');
+ return true;
+}
+
+// Similar to PreprocessValue: replace the value with a simpler one to
+// stringify, but also detect whether the value is compatible with the fast
+// path. If not, bail out by setting *whySlow and returning true.
+static bool PreprocessFastValue(JSContext* cx, Value* vp, StringifyContext* scx,
+ BailReason* whySlow) {
+ MOZ_ASSERT(!scx->maybeSafely);
+
+ // Steps are from
+ // https://262.ecma-international.org/14.0/#sec-serializejsonproperty
+
+ // Disallow BigInts to avoid caring about BigInt.prototype.toJSON.
+ if (vp->isBigInt()) {
+ *whySlow = BailReason::BIGINT;
+ return true;
+ }
+
+ if (!vp->isObject()) {
+ return true;
+ }
+
+ if (!vp->toObject().is<NativeObject>()) {
+ *whySlow = BailReason::INELIGIBLE_OBJECT;
+ return true;
+ }
+
+ // Step 2: lookup a .toJSON property (and bail if found).
+ NativeObject* obj = &vp->toObject().as<NativeObject>();
+ PropertyResult toJSON;
+ NativeObject* holder;
+ PropertyKey id = NameToId(cx->names().toJSON);
+ if (!NativeLookupPropertyInline<NoGC, LookupResolveMode::CheckMayResolve>(
+ cx, obj, id, &holder, &toJSON)) {
+ // Looking up this property would require a side effect.
+ *whySlow = BailReason::IMPURE_LOOKUP;
+ return true;
+ }
+ if (toJSON.isFound()) {
+ *whySlow = BailReason::HAVE_TOJSON;
+ return true;
+ }
+
+ // Step 4: convert primitive wrapper objects to primitives. Disallowed for
+ // fast path.
+ if (obj->is<NumberObject>() || obj->is<StringObject>() ||
+ obj->is<BooleanObject>() || obj->is<BigIntObject>() ||
+ IF_RECORD_TUPLE(obj->is<RecordObject>() || obj->is<TupleObject>(),
+ false)) {
+ // Primitive wrapper objects can invoke arbitrary code when being coerced to
+ // their primitive values (eg via @@toStringTag).
+ *whySlow = BailReason::INELIGIBLE_OBJECT;
+ return true;
+ }
+
+ if (obj->isCallable()) {
+ // Steps 11,12: Callable objects are treated as undefined.
+ vp->setUndefined();
+ return true;
+ }
+
+ if (!CanFastStringifyObject(obj)) {
+ *whySlow = BailReason::INELIGIBLE_OBJECT;
+ return true;
+ }
+
+ return true;
+}
+
+// FastSerializeJSONProperty maintains an explicit stack to handle nested
+// objects. For each object, first the dense elements are iterated, then the
+// named properties (included sparse indexes, which will cause
+// FastSerializeJSONProperty to bail out.)
+//
+// The iterators for each of those parts are not merged into a single common
+// iterator because the interface is different for the two parts, and they are
+// handled separately in the FastSerializeJSONProperty code.
+struct FastStackEntry {
+ NativeObject* nobj;
+ Variant<DenseElementsIteratorForJSON, OwnNonIndexKeysIterForJSON> iter;
+ bool isArray; // Cached nobj->is<ArrayObject>()
+
+ // Given an object, a FastStackEntry starts with the dense elements. The
+ // caller is expected to inspect the variant to use it differently based on
+ // which iterator is active.
+ explicit FastStackEntry(NativeObject* obj)
+ : nobj(obj),
+ iter(AsVariant(DenseElementsIteratorForJSON(obj))),
+ isArray(obj->is<ArrayObject>()) {}
+
+ // Called by Vector when moving data around.
+ FastStackEntry(FastStackEntry&& other) noexcept
+ : nobj(other.nobj), iter(std::move(other.iter)), isArray(other.isArray) {}
+
+ // Move assignment, called when updating the `top` entry.
+ void operator=(FastStackEntry&& other) noexcept {
+ nobj = other.nobj;
+ iter = std::move(other.iter);
+ isArray = other.isArray;
+ }
+
+ // Advance from dense elements to the named properties.
+ void advanceToProperties() {
+ iter = AsVariant(OwnNonIndexKeysIterForJSON(nobj));
+ }
+};
+
+/* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
+static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v,
+ StringifyContext* scx,
+ BailReason* whySlow) {
+ MOZ_ASSERT(*whySlow == BailReason::NO_REASON);
+ MOZ_ASSERT(v.isObject());
+
+ /*
+ * FastSerializeJSONProperty is an optimistic fast path for the
+ * SerializeJSONProperty algorithm that applies in limited situations. It
+ * falls back to SerializeJSONProperty() if:
+ *
+ * * Any externally visible code attempts to run: getter, enumerate
+ * hook, toJSON property.
+ * * Sparse index found (this would require accumulating props and sorting.)
+ * * Max stack depth is reached. (This will also detect self-referential
+ * input.)
+ *
+ * Algorithm:
+ *
+ * stack = []
+ * top = iter(obj)
+ * wroteMember = false
+ * OUTER: while true:
+ * if !wroteMember:
+ * emit("[" or "{")
+ * while !top.done():
+ * key, value = top.next()
+ * if top is a non-Array and value is skippable:
+ * continue
+ * if wroteMember:
+ * emit(",")
+ * wroteMember = true
+ * if value is object:
+ * emit(key + ":") if top is iterating a non-Array
+ * stack.push(top)
+ * top <- value
+ * wroteMember = false
+ * continue OUTER
+ * else:
+ * emit(value) or emit(key + ":" + value)
+ * emit("]" or "}")
+ * if stack is empty: done!
+ * top <- stack.pop()
+ * wroteMember = true
+ *
+ * except:
+ *
+ * * The `while !top.done()` loop is split into the dense element portion
+ * and the slot portion. Each is iterated to completion before advancing
+ * or finishing.
+ *
+ * * For Arrays, the named properties are not output, but they are still
+ * scanned to bail if any numeric keys are found that could be indexes.
+ */
+
+ // FastSerializeJSONProperty will bail if an interrupt is requested in the
+ // middle of an operation, so handle any interrupts now before starting. Note:
+ // this can GC, but after this point nothing should be able to GC unless
+ // something fails, so rooting is unnecessary.
+ if (!CheckForInterrupt(cx)) {
+ return false;
+ }
+
+ constexpr size_t MAX_STACK_DEPTH = 20;
+ Vector<FastStackEntry> stack(cx);
+ if (!stack.reserve(MAX_STACK_DEPTH - 1)) {
+ return false;
+ }
+ // Construct an iterator for the object,
+ // https://262.ecma-international.org/14.0/#sec-serializejsonobject step 6:
+ // EnumerableOwnPropertyNames or
+ // https://262.ecma-international.org/14.0/#sec-serializejsonarray step 7-8.
+ FastStackEntry top(&v.toObject().as<NativeObject>());
+ bool wroteMember = false;
+
+ if (!CanFastStringifyObject(top.nobj)) {
+ *whySlow = BailReason::INELIGIBLE_OBJECT;
+ return true;
+ }
+
+ while (true) {
+ if (!wroteMember) {
+ if (!scx->sb.append(top.isArray ? '[' : '{')) {
+ return false;
+ }
+ }
+
+ if (top.iter.is<DenseElementsIteratorForJSON>()) {
+ auto& iter = top.iter.as<DenseElementsIteratorForJSON>();
+ bool nestedObject = false;
+ while (!iter.done()) {
+ // Interrupts can GC and we are working with unrooted pointers.
+ if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
+ cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
+ *whySlow = BailReason::INTERRUPT;
+ return true;
+ }
+
+ uint32_t index = iter.getIndex();
+ Value val = iter.next();
+
+ if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
+ return false;
+ }
+ if (*whySlow != BailReason::NO_REASON) {
+ return true;
+ }
+ if (IsFilteredValue(val)) {
+ if (top.isArray) {
+ // Arrays convert unrepresentable values to "null".
+ val = UndefinedValue();
+ } else {
+ // Objects skip unrepresentable values.
+ continue;
+ }
+ }
+
+ if (wroteMember && !scx->sb.append(',')) {
+ return false;
+ }
+ wroteMember = true;
+
+ if (!top.isArray) {
+ if (!EmitQuotedIndexColon(scx->sb, index)) {
+ return false;
+ }
+ }
+
+ if (val.isObject()) {
+ if (stack.length() >= MAX_STACK_DEPTH - 1) {
+ *whySlow = BailReason::DEEP_RECURSION;
+ return true;
+ }
+ // Save the current iterator position on the stack and
+ // switch to processing the nested value.
+ stack.infallibleAppend(std::move(top));
+ top = FastStackEntry(&val.toObject().as<NativeObject>());
+ wroteMember = false;
+ nestedObject = true; // Break out to the outer loop.
+ break;
+ }
+ if (!EmitSimpleValue(cx, scx->sb, val)) {
+ return false;
+ }
+ }
+
+ if (nestedObject) {
+ continue; // Break out to outer loop.
+ }
+
+ MOZ_ASSERT(iter.done());
+ if (top.isArray) {
+ MOZ_ASSERT(!top.nobj->isIndexed() || IsPackedArray(top.nobj));
+ } else {
+ top.advanceToProperties();
+ }
+ }
+
+ if (top.iter.is<OwnNonIndexKeysIterForJSON>()) {
+ auto& iter = top.iter.as<OwnNonIndexKeysIterForJSON>();
+ bool nesting = false;
+ while (!iter.done()) {
+ // Interrupts can GC and we are working with unrooted pointers.
+ if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
+ cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
+ *whySlow = BailReason::INTERRUPT;
+ return true;
+ }
+
+ PropertyInfoWithKey prop = iter.next();
+
+ // A non-Array with indexed elements would need to sort the indexes
+ // numerically, which this code does not support. These objects are
+ // skipped when obj->isIndexed(), so no index properties should be found
+ // here.
+ mozilla::DebugOnly<uint32_t> index = -1;
+ MOZ_ASSERT(!IdIsIndex(prop.key(), &index));
+
+ Value val = top.nobj->getSlot(prop.slot());
+ if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
+ return false;
+ }
+ if (*whySlow != BailReason::NO_REASON) {
+ return true;
+ }
+ if (IsFilteredValue(val)) {
+ // Undefined check in
+ // https://262.ecma-international.org/14.0/#sec-serializejsonobject
+ // step 8b, covering undefined, symbol
+ continue;
+ }
+
+ if (wroteMember && !scx->sb.append(",")) {
+ return false;
+ }
+ wroteMember = true;
+
+ MOZ_ASSERT(prop.key().isString());
+ if (!QuoteJSONString(cx, scx->sb, prop.key().toString())) {
+ return false;
+ }
+
+ if (!scx->sb.append(':')) {
+ return false;
+ }
+ if (val.isObject()) {
+ if (stack.length() >= MAX_STACK_DEPTH - 1) {
+ *whySlow = BailReason::DEEP_RECURSION;
+ return true;
+ }
+ // Save the current iterator position on the stack and
+ // switch to processing the nested value.
+ stack.infallibleAppend(std::move(top));
+ top = FastStackEntry(&val.toObject().as<NativeObject>());
+ wroteMember = false;
+ nesting = true; // Break out to the outer loop.
+ break;
+ }
+ if (!EmitSimpleValue(cx, scx->sb, val)) {
+ return false;
+ }
+ }
+ *whySlow = iter.cannotFastStringify();
+ if (*whySlow != BailReason::NO_REASON) {
+ return true;
+ }
+ if (nesting) {
+ continue; // Break out to outer loop.
+ }
+ MOZ_ASSERT(iter.done());
+ }
+
+ if (!scx->sb.append(top.isArray ? ']' : '}')) {
+ return false;
+ }
+ if (stack.empty()) {
+ return true; // Success!
+ }
+ top = std::move(stack.back());
+
+ stack.popBack();
+ wroteMember = true;
+ }
+}
+
+/* https://262.ecma-international.org/14.0/#sec-json.stringify */
+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::RestrictedSafe ||
+ vp.toObject().is<PlainObject>() ||
+ vp.toObject().is<ArrayObject>(),
+ "input to JS::ToJSONMaybeSafely must be a plain object or array");
+
+ /* Step 5. */
+ RootedIdVector propertyList(cx);
+ BailReason whySlow = BailReason::NO_REASON;
+ if (stringifyBehavior == StringifyBehavior::SlowOnly ||
+ stringifyBehavior == StringifyBehavior::RestrictedSafe) {
+ whySlow = BailReason::API;
+ }
+ if (replacer) {
+ whySlow = BailReason::HAVE_REPLACER;
+ bool isArray;
+ if (replacer->isCallable()) {
+ /* Step 5a(i): use replacer to transform values. */
+ } else if (!IsArray(cx, replacer, &isArray)) {
+ return false;
+ } else if (isArray) {
+ /* Step 5b(ii). */
+
+ /* Step 5b(ii)(2). */
+ uint32_t len;
+ if (!GetLengthPropertyForArrayLike(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, std::min(len, MaxInitialSize)));
+
+ /* Step 5b(ii)(3). */
+ uint32_t k = 0;
+
+ /* Step 5b(ii)(4). */
+ RootedValue item(cx);
+ for (; k < len; k++) {
+ if (!CheckForInterrupt(cx)) {
+ return false;
+ }
+
+ /* Step 5b(ii)(4)(a-b). */
+ if (!GetElement(cx, replacer, k, &item)) {
+ return false;
+ }
+
+ /* Step 5b(ii)(4)(c-g). */
+ RootedId id(cx);
+ if (item.isNumber() || item.isString()) {
+ if (!PrimitiveValueToId<CanGC>(cx, item, &id)) {
+ return false;
+ }
+ } else {
+ ESClass cls;
+ if (!GetClassOfValue(cx, item, &cls)) {
+ return false;
+ }
+
+ if (cls != ESClass::String && cls != ESClass::Number) {
+ continue;
+ }
+
+ JSAtom* atom = ToAtom<CanGC>(cx, item);
+ if (!atom) {
+ return false;
+ }
+
+ id.set(AtomToId(atom));
+ }
+
+ /* Step 5b(ii)(4)(g). */
+ auto p = idSet.lookupForAdd(id);
+ if (!p) {
+ /* Step 5b(ii)(4)(g)(i). */
+ if (!idSet.add(p, id) || !propertyList.append(id)) {
+ return false;
+ }
+ }
+ }
+ } else {
+ replacer = nullptr;
+ }
+ }
+
+ /* Step 6. */
+ if (space.isObject()) {
+ RootedObject spaceObj(cx, &space.toObject());
+
+ ESClass cls;
+ if (!JS::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 7. */
+ double d;
+ MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
+ d = std::min(10.0, d);
+ if (d >= 1 && !gap.appendN(' ', uint32_t(d))) {
+ return false;
+ }
+ } else if (space.isString()) {
+ /* Step 8. */
+ JSLinearString* str = space.toString()->ensureLinear(cx);
+ if (!str) {
+ return false;
+ }
+ size_t len = std::min(size_t(10), str->length());
+ if (!gap.appendSubstring(str, 0, len)) {
+ return false;
+ }
+ } else {
+ /* Step 9. */
+ MOZ_ASSERT(gap.empty());
+ }
+ if (!gap.empty()) {
+ whySlow = BailReason::HAVE_SPACE;
+ }
+
+ Rooted<PlainObject*> wrapper(cx);
+ RootedId emptyId(cx, NameToId(cx->names().empty_));
+ if (replacer && replacer->isCallable()) {
+ // We can skip creating the initial wrapper object if no replacer
+ // function is present.
+
+ /* Step 10. */
+ wrapper = NewPlainObject(cx);
+ if (!wrapper) {
+ return false;
+ }
+
+ /* Step 11. */
+ if (!NativeDefineDataProperty(cx, wrapper, emptyId, vp, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+ /* Step 13. */
+ Rooted<JSAtom*> fastJSON(cx);
+ if (whySlow == BailReason::NO_REASON) {
+ MOZ_ASSERT(propertyList.empty());
+ MOZ_ASSERT(stringifyBehavior != StringifyBehavior::RestrictedSafe);
+ StringifyContext scx(cx, sb, gap, nullptr, propertyList, false);
+ if (!PreprocessFastValue(cx, vp.address(), &scx, &whySlow)) {
+ return false;
+ }
+ if (!vp.isObject()) {
+ // "Fast" stringify of primitives would create a wrapper object and thus
+ // be slower than regular stringify.
+ whySlow = BailReason::PRIMITIVE;
+ }
+ if (whySlow == BailReason::NO_REASON) {
+ if (!FastSerializeJSONProperty(cx, vp, &scx, &whySlow)) {
+ return false;
+ }
+ if (whySlow == BailReason::NO_REASON) {
+ // Fast stringify succeeded!
+ if (stringifyBehavior != StringifyBehavior::Compare) {
+ return true;
+ }
+ fastJSON = scx.sb.finishAtom();
+ if (!fastJSON) {
+ return false;
+ }
+ }
+ scx.sb.clear(); // Preserves allocated space.
+ }
+ }
+
+ if (MOZ_UNLIKELY((stringifyBehavior == StringifyBehavior::FastOnly) &&
+ (whySlow != BailReason::NO_REASON))) {
+ JS_ReportErrorASCII(cx, "JSON stringify failed mandatory fast path: %s",
+ DescribeStringifyBailReason(whySlow));
+ return false;
+ }
+
+ // Slow, general path.
+
+ StringifyContext scx(cx, sb, gap, replacer, propertyList,
+ stringifyBehavior == StringifyBehavior::RestrictedSafe);
+ if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx)) {
+ return false;
+ }
+ if (IsFilteredValue(vp)) {
+ return true;
+ }
+
+ if (!SerializeJSONProperty(cx, vp, &scx)) {
+ return false;
+ }
+
+ // For StringBehavior::Compare, when the fast path succeeded.
+ if (MOZ_UNLIKELY(fastJSON)) {
+ JSAtom* slowJSON = scx.sb.finishAtom();
+ if (!slowJSON) {
+ return false;
+ }
+ if (fastJSON != slowJSON) {
+ MOZ_CRASH("JSON.stringify mismatch between fast and slow paths");
+ }
+ // Put the JSON back into the StringBuffer for returning.
+ if (!sb.append(slowJSON)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* https://262.ecma-international.org/14.0/#sec-internalizejsonproperty */
+static bool InternalizeJSONProperty(
+ JSContext* cx, HandleObject holder, HandleId name, HandleValue reviver,
+ MutableHandle<ParseRecordObject> parseRecord, MutableHandleValue vp) {
+ AutoCheckRecursionLimit recursion(cx);
+ if (!recursion.check(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 2b(i). */
+ uint32_t length;
+ if (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
+ return false;
+ }
+
+ /* Steps 2b(ii-iii). */
+ 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). */
+ Rooted<ParseRecordObject> elementRecord(cx);
+ if (!InternalizeJSONProperty(cx, obj, id, reviver, &elementRecord,
+ &newElement)) {
+ return false;
+ }
+
+ ObjectOpResult ignored;
+ if (newElement.isUndefined()) {
+ /* Step 2b(iii)(3). The spec deliberately ignores strict failure. */
+ if (!DeleteProperty(cx, obj, id, ignored)) {
+ return false;
+ }
+ } else {
+ /* Step 2b(iii)(4). The spec deliberately ignores strict failure. */
+ Rooted<PropertyDescriptor> desc(
+ cx, PropertyDescriptor::Data(newElement,
+ {JS::PropertyAttribute::Configurable,
+ JS::PropertyAttribute::Enumerable,
+ JS::PropertyAttribute::Writable}));
+ if (!DefineProperty(cx, obj, id, desc, ignored)) {
+ return false;
+ }
+ }
+ }
+ } else {
+ /* Step 2c(i). */
+ RootedIdVector keys(cx);
+ if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) {
+ return false;
+ }
+
+ /* Step 2c(ii). */
+ RootedId id(cx);
+ RootedValue newElement(cx);
+ for (size_t i = 0, len = keys.length(); i < len; i++) {
+ if (!CheckForInterrupt(cx)) {
+ return false;
+ }
+
+ /* Step 2c(ii)(1). */
+ id = keys[i];
+ Rooted<ParseRecordObject> entryRecord(cx);
+ if (!InternalizeJSONProperty(cx, obj, id, reviver, &entryRecord,
+ &newElement)) {
+ return false;
+ }
+
+ ObjectOpResult ignored;
+ if (newElement.isUndefined()) {
+ /* Step 2c(ii)(2). The spec deliberately ignores strict failure. */
+ if (!DeleteProperty(cx, obj, id, ignored)) {
+ return false;
+ }
+ } else {
+ /* Step 2c(ii)(3). The spec deliberately ignores strict failure. */
+ Rooted<PropertyDescriptor> desc(
+ cx, PropertyDescriptor::Data(newElement,
+ {JS::PropertyAttribute::Configurable,
+ JS::PropertyAttribute::Enumerable,
+ JS::PropertyAttribute::Writable}));
+ 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));
+#ifdef ENABLE_JSON_PARSE_WITH_SOURCE
+ if (cx->realm()->creationOptions().getJSONParseWithSource()) {
+ RootedObject context(cx, NewPlainObject(cx));
+ if (!context) {
+ return false;
+ }
+ Rooted<Value> parseNode(cx, StringValue(parseRecord.get().parseNode));
+ if (!DefineDataProperty(cx, context, cx->names().source, parseNode)) {
+ return false;
+ }
+ RootedValue contextVal(cx, ObjectValue(*context));
+ return js::Call(cx, reviver, holder, keyVal, val, contextVal, vp);
+ }
+#endif
+ return js::Call(cx, reviver, holder, keyVal, val, vp);
+}
+
+static bool Revive(JSContext* cx, HandleValue reviver,
+ MutableHandle<ParseRecordObject> pro,
+ MutableHandleValue vp) {
+ Rooted<PlainObject*> obj(cx, NewPlainObject(cx));
+ if (!obj) {
+ return false;
+ }
+
+ if (!DefineDataProperty(cx, obj, cx->names().empty_, vp)) {
+ return false;
+ }
+
+#ifdef ENABLE_JSON_PARSE_WITH_SOURCE
+ MOZ_ASSERT_IF(cx->realm()->creationOptions().getJSONParseWithSource(),
+ pro.get().value == vp.get());
+#endif
+ Rooted<jsid> id(cx, NameToId(cx->names().empty_));
+ return InternalizeJSONProperty(cx, obj, id, reviver, pro, vp);
+}
+
+template <typename CharT>
+bool ParseJSON(JSContext* cx, const mozilla::Range<const CharT> chars,
+ MutableHandleValue vp, MutableHandle<ParseRecordObject> pro) {
+ Rooted<JSONParser<CharT>> parser(cx, cx, chars,
+ JSONParser<CharT>::ParseType::JSONParse);
+ return parser.parse(vp, pro);
+}
+
+template <typename CharT>
+bool js::ParseJSONWithReviver(JSContext* cx,
+ const mozilla::Range<const CharT> chars,
+ HandleValue reviver, MutableHandleValue vp) {
+ /* https://262.ecma-international.org/14.0/#sec-json.parse steps 2-10. */
+ Rooted<ParseRecordObject> pro(cx);
+ if (!ParseJSON(cx, chars, vp, &pro)) {
+ return false;
+ }
+
+ /* Steps 11-12. */
+ if (IsCallable(reviver)) {
+ return Revive(cx, reviver, &pro, 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);
+
+static bool json_toSource(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ args.rval().setString(cx->names().JSON);
+ return true;
+}
+
+/* https://262.ecma-international.org/14.0/#sec-json.parse */
+static bool json_parse(JSContext* cx, unsigned argc, Value* vp) {
+ AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parse");
+ 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-12. */
+ return linearChars.isLatin1()
+ ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver,
+ args.rval())
+ : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver,
+ args.rval());
+}
+
+#ifdef ENABLE_RECORD_TUPLE
+bool BuildImmutableProperty(JSContext* cx, HandleValue value, HandleId name,
+ HandleValue reviver,
+ MutableHandleValue immutableRes) {
+ MOZ_ASSERT(!name.isSymbol());
+
+ // Step 1
+ if (value.isObject()) {
+ RootedValue childValue(cx), newElement(cx);
+ RootedId childName(cx);
+
+ // Step 1.a-1.b
+ if (value.toObject().is<ArrayObject>()) {
+ Rooted<ArrayObject*> arr(cx, &value.toObject().as<ArrayObject>());
+
+ // Step 1.b.iii
+ uint32_t len = arr->length();
+
+ TupleType* tup = TupleType::createUninitialized(cx, len);
+ if (!tup) {
+ return false;
+ }
+ immutableRes.setExtendedPrimitive(*tup);
+
+ // Step 1.b.iv
+ for (uint32_t i = 0; i < len; i++) {
+ // Step 1.b.iv.1
+ childName.set(PropertyKey::Int(i));
+
+ // Step 1.b.iv.2
+ if (!GetProperty(cx, arr, value, childName, &childValue)) {
+ return false;
+ }
+
+ // Step 1.b.iv.3
+ if (!BuildImmutableProperty(cx, childValue, childName, reviver,
+ &newElement)) {
+ return false;
+ }
+ MOZ_ASSERT(newElement.isPrimitive());
+
+ // Step 1.b.iv.5
+ if (!tup->initializeNextElement(cx, newElement)) {
+ return false;
+ }
+ }
+
+ // Step 1.b.v
+ tup->finishInitialization(cx);
+ } else {
+ RootedObject obj(cx, &value.toObject());
+
+ // Step 1.c.i - We only get the property keys rather than the
+ // entries, but the difference is not observable from user code
+ // because `obj` is a plan object not exposed externally
+ RootedIdVector props(cx);
+ if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &props)) {
+ return false;
+ }
+
+ RecordType* rec = RecordType::createUninitialized(cx, props.length());
+ if (!rec) {
+ return false;
+ }
+ immutableRes.setExtendedPrimitive(*rec);
+
+ for (uint32_t i = 0; i < props.length(); i++) {
+ // Step 1.c.iii.1
+ childName.set(props[i]);
+
+ // Step 1.c.iii.2
+ if (!GetProperty(cx, obj, value, childName, &childValue)) {
+ return false;
+ }
+
+ // Step 1.c.iii.3
+ if (!BuildImmutableProperty(cx, childValue, childName, reviver,
+ &newElement)) {
+ return false;
+ }
+ MOZ_ASSERT(newElement.isPrimitive());
+
+ // Step 1.c.iii.5
+ if (!newElement.isUndefined()) {
+ // Step 1.c.iii.5.a-b
+ rec->initializeNextProperty(cx, childName, newElement);
+ }
+ }
+
+ // Step 1.c.iv
+ rec->finishInitialization(cx);
+ }
+ } else {
+ // Step 2.a
+ immutableRes.set(value);
+ }
+
+ // Step 3
+ if (IsCallable(reviver)) {
+ RootedValue keyVal(cx, StringValue(IdToString(cx, name)));
+
+ // Step 3.a
+ if (!Call(cx, reviver, UndefinedHandleValue, keyVal, immutableRes,
+ immutableRes)) {
+ return false;
+ }
+
+ // Step 3.b
+ if (!immutableRes.isPrimitive()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_RECORD_TUPLE_NO_OBJECT);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) {
+ AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parseImmutable");
+ 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);
+ RootedValue unfiltered(cx);
+ Rooted<ParseRecordObject> pro(cx);
+
+ if (linearChars.isLatin1()) {
+ if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered, &pro)) {
+ return false;
+ }
+ } else {
+ if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered, &pro)) {
+ return false;
+ }
+ }
+
+ RootedId id(cx, NameToId(cx->names().empty_));
+ return BuildImmutableProperty(cx, unfiltered, id, reviver, args.rval());
+}
+#endif
+
+/* https://262.ecma-international.org/14.0/#sec-json.stringify */
+bool json_stringify(JSContext* cx, unsigned argc, Value* vp) {
+ AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "stringify");
+ 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));
+
+#ifdef DEBUG
+ StringifyBehavior behavior = StringifyBehavior::Compare;
+#else
+ StringifyBehavior behavior = StringifyBehavior::Normal;
+#endif
+
+ JSStringBuilder sb(cx);
+ if (!Stringify(cx, &value, replacer, space, sb, behavior)) {
+ 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[] = {
+ JS_FN("toSource", json_toSource, 0, 0), JS_FN("parse", json_parse, 2, 0),
+ JS_FN("stringify", json_stringify, 3, 0),
+#ifdef ENABLE_RECORD_TUPLE
+ JS_FN("parseImmutable", json_parseImmutable, 2, 0),
+#endif
+ JS_FS_END};
+
+static const JSPropertySpec json_static_properties[] = {
+ JS_STRING_SYM_PS(toStringTag, "JSON", JSPROP_READONLY), JS_PS_END};
+
+static JSObject* CreateJSONObject(JSContext* cx, JSProtoKey key) {
+ RootedObject proto(cx, &cx->global()->getObjectPrototype());
+ return NewTenuredObjectWithGivenProto(cx, &JSONClass, proto);
+}
+
+static const ClassSpec JSONClassSpec = {
+ CreateJSONObject, nullptr, json_static_methods, json_static_properties};
+
+const JSClass js::JSONClass = {"JSON", JSCLASS_HAS_CACHED_PROTO(JSProto_JSON),
+ JS_NULL_CLASS_OPS, &JSONClassSpec};