Bug 1853305 - Part 2: Use AutoSelectGCHeap in JSON parsing r=sfink
[gecko.git] / js / src / builtin / JSON.cpp
blobc65ed732cd6b310d9823080c70404ee09f66a634
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "builtin/JSON.h"
9 #include "mozilla/CheckedInt.h"
10 #include "mozilla/FloatingPoint.h"
11 #include "mozilla/Range.h"
12 #include "mozilla/ScopeExit.h"
13 #include "mozilla/Variant.h"
15 #include <algorithm>
17 #include "jsnum.h"
18 #include "jstypes.h"
20 #include "builtin/Array.h"
21 #include "builtin/BigInt.h"
22 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
23 #include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit
24 #include "js/Object.h" // JS::GetBuiltinClass
25 #include "js/PropertySpec.h"
26 #include "js/StableStringChars.h"
27 #include "js/TypeDecls.h"
28 #include "js/Value.h"
29 #include "util/StringBuffer.h"
30 #include "vm/BooleanObject.h" // js::BooleanObject
31 #include "vm/Interpreter.h"
32 #include "vm/Iteration.h"
33 #include "vm/JSAtomUtils.h" // ToAtom
34 #include "vm/JSContext.h"
35 #include "vm/JSObject.h"
36 #include "vm/JSONParser.h"
37 #include "vm/NativeObject.h"
38 #include "vm/NumberObject.h" // js::NumberObject
39 #include "vm/PlainObject.h" // js::PlainObject
40 #include "vm/StringObject.h" // js::StringObject
41 #ifdef ENABLE_RECORD_TUPLE
42 # include "builtin/RecordObject.h"
43 # include "builtin/TupleObject.h"
44 # include "vm/RecordType.h"
45 #endif
47 #include "builtin/Array-inl.h"
48 #include "vm/GeckoProfiler-inl.h"
49 #include "vm/JSAtomUtils-inl.h" // AtomToId, PrimitiveValueToId, IndexToId, IdToString,
50 #include "vm/NativeObject-inl.h"
52 using namespace js;
54 using mozilla::AsVariant;
55 using mozilla::CheckedInt;
56 using mozilla::Maybe;
57 using mozilla::RangedPtr;
58 using mozilla::Variant;
60 using JS::AutoStableStringChars;
62 /* ES5 15.12.3 Quote.
63 * Requires that the destination has enough space allocated for src after
64 * escaping (that is, `2 + 6 * (srcEnd - srcBegin)` characters).
66 template <typename SrcCharT, typename DstCharT>
67 static MOZ_ALWAYS_INLINE RangedPtr<DstCharT> InfallibleQuote(
68 RangedPtr<const SrcCharT> srcBegin, RangedPtr<const SrcCharT> srcEnd,
69 RangedPtr<DstCharT> dstPtr) {
70 // Maps characters < 256 to the value that must follow the '\\' in the quoted
71 // string. Entries with 'u' are handled as \\u00xy, and entries with 0 are not
72 // escaped in any way. Characters >= 256 are all assumed to be unescaped.
73 static const Latin1Char escapeLookup[256] = {
74 // clang-format off
75 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'b', 't',
76 'n', 'u', 'f', 'r', 'u', 'u', 'u', 'u', 'u', 'u',
77 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u',
78 'u', 'u', 0, 0, '\"', 0, 0, 0, 0, 0,
79 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
80 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
81 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
82 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
83 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
84 0, 0, '\\', // rest are all zeros
85 // clang-format on
88 /* Step 1. */
89 *dstPtr++ = '"';
91 auto ToLowerHex = [](uint8_t u) {
92 MOZ_ASSERT(u <= 0xF);
93 return "0123456789abcdef"[u];
96 /* Step 2. */
97 while (srcBegin != srcEnd) {
98 const SrcCharT c = *srcBegin++;
100 // Handle the Latin-1 cases.
101 if (MOZ_LIKELY(c < sizeof(escapeLookup))) {
102 Latin1Char escaped = escapeLookup[c];
104 // Directly copy non-escaped code points.
105 if (escaped == 0) {
106 *dstPtr++ = c;
107 continue;
110 // Escape the rest, elaborating Unicode escapes when needed.
111 *dstPtr++ = '\\';
112 *dstPtr++ = escaped;
113 if (escaped == 'u') {
114 *dstPtr++ = '0';
115 *dstPtr++ = '0';
117 uint8_t x = c >> 4;
118 MOZ_ASSERT(x < 10);
119 *dstPtr++ = '0' + x;
121 *dstPtr++ = ToLowerHex(c & 0xF);
124 continue;
127 // Non-ASCII non-surrogates are directly copied.
128 if (!unicode::IsSurrogate(c)) {
129 *dstPtr++ = c;
130 continue;
133 // So too for complete surrogate pairs.
134 if (MOZ_LIKELY(unicode::IsLeadSurrogate(c) && srcBegin < srcEnd &&
135 unicode::IsTrailSurrogate(*srcBegin))) {
136 *dstPtr++ = c;
137 *dstPtr++ = *srcBegin++;
138 continue;
141 // But lone surrogates are Unicode-escaped.
142 char32_t as32 = char32_t(c);
143 *dstPtr++ = '\\';
144 *dstPtr++ = 'u';
145 *dstPtr++ = ToLowerHex(as32 >> 12);
146 *dstPtr++ = ToLowerHex((as32 >> 8) & 0xF);
147 *dstPtr++ = ToLowerHex((as32 >> 4) & 0xF);
148 *dstPtr++ = ToLowerHex(as32 & 0xF);
151 /* Steps 3-4. */
152 *dstPtr++ = '"';
153 return dstPtr;
156 template <typename SrcCharT, typename DstCharT>
157 static size_t QuoteHelper(const JSLinearString& linear, StringBuffer& sb,
158 size_t sbOffset) {
159 size_t len = linear.length();
161 JS::AutoCheckCannotGC nogc;
162 RangedPtr<const SrcCharT> srcBegin{linear.chars<SrcCharT>(nogc), len};
163 RangedPtr<DstCharT> dstBegin{sb.begin<DstCharT>(), sb.begin<DstCharT>(),
164 sb.end<DstCharT>()};
165 RangedPtr<DstCharT> dstEnd =
166 InfallibleQuote(srcBegin, srcBegin + len, dstBegin + sbOffset);
168 return dstEnd - dstBegin;
171 static bool Quote(JSContext* cx, StringBuffer& sb, JSString* str) {
172 JSLinearString* linear = str->ensureLinear(cx);
173 if (!linear) {
174 return false;
177 if (linear->hasTwoByteChars() && !sb.ensureTwoByteChars()) {
178 return false;
181 // We resize the backing buffer to the maximum size we could possibly need,
182 // write the escaped string into it, and shrink it back to the size we ended
183 // up needing.
185 size_t len = linear->length();
186 size_t sbInitialLen = sb.length();
188 CheckedInt<size_t> reservedLen = CheckedInt<size_t>(len) * 6 + 2;
189 if (MOZ_UNLIKELY(!reservedLen.isValid())) {
190 ReportAllocationOverflow(cx);
191 return false;
194 if (!sb.growByUninitialized(reservedLen.value())) {
195 return false;
198 size_t newSize;
200 if (linear->hasTwoByteChars()) {
201 newSize = QuoteHelper<char16_t, char16_t>(*linear, sb, sbInitialLen);
202 } else if (sb.isUnderlyingBufferLatin1()) {
203 newSize = QuoteHelper<Latin1Char, Latin1Char>(*linear, sb, sbInitialLen);
204 } else {
205 newSize = QuoteHelper<Latin1Char, char16_t>(*linear, sb, sbInitialLen);
208 sb.shrinkTo(newSize);
210 return true;
213 namespace {
215 using ObjectVector = GCVector<JSObject*, 8>;
217 class StringifyContext {
218 public:
219 StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
220 HandleObject replacer, const RootedIdVector& propertyList,
221 bool maybeSafely)
222 : sb(sb),
223 gap(gap),
224 replacer(cx, replacer),
225 stack(cx, ObjectVector(cx)),
226 propertyList(propertyList),
227 depth(0),
228 maybeSafely(maybeSafely) {
229 MOZ_ASSERT_IF(maybeSafely, !replacer);
230 MOZ_ASSERT_IF(maybeSafely, gap.empty());
233 StringBuffer& sb;
234 const StringBuffer& gap;
235 RootedObject replacer;
236 Rooted<ObjectVector> stack;
237 const RootedIdVector& propertyList;
238 uint32_t depth;
239 bool maybeSafely;
242 } /* anonymous namespace */
244 static bool Str(JSContext* cx, const Value& v, StringifyContext* scx);
246 static bool WriteIndent(StringifyContext* scx, uint32_t limit) {
247 if (!scx->gap.empty()) {
248 if (!scx->sb.append('\n')) {
249 return false;
252 if (scx->gap.isUnderlyingBufferLatin1()) {
253 for (uint32_t i = 0; i < limit; i++) {
254 if (!scx->sb.append(scx->gap.rawLatin1Begin(),
255 scx->gap.rawLatin1End())) {
256 return false;
259 } else {
260 for (uint32_t i = 0; i < limit; i++) {
261 if (!scx->sb.append(scx->gap.rawTwoByteBegin(),
262 scx->gap.rawTwoByteEnd())) {
263 return false;
269 return true;
272 namespace {
274 template <typename KeyType>
275 class KeyStringifier {};
277 template <>
278 class KeyStringifier<uint32_t> {
279 public:
280 static JSString* toString(JSContext* cx, uint32_t index) {
281 return IndexToString(cx, index);
285 template <>
286 class KeyStringifier<HandleId> {
287 public:
288 static JSString* toString(JSContext* cx, HandleId id) {
289 return IdToString(cx, id);
293 } /* anonymous namespace */
296 * ES5 15.12.3 Str, steps 2-4, extracted to enable preprocessing of property
297 * values when stringifying objects in JO.
299 template <typename KeyType>
300 static bool PreprocessValue(JSContext* cx, HandleObject holder, KeyType key,
301 MutableHandleValue vp, StringifyContext* scx) {
302 // We don't want to do any preprocessing here if scx->maybeSafely,
303 // since the stuff we do here can have side-effects.
304 if (scx->maybeSafely) {
305 return true;
308 RootedString keyStr(cx);
310 // Step 2. Modified by BigInt spec 6.1 to check for a toJSON method on the
311 // BigInt prototype when the value is a BigInt, and to pass the BigInt
312 // primitive value as receiver.
313 if (vp.isObject() || vp.isBigInt()) {
314 RootedValue toJSON(cx);
315 RootedObject obj(cx, JS::ToObject(cx, vp));
316 if (!obj) {
317 return false;
320 if (!GetProperty(cx, obj, vp, cx->names().toJSON, &toJSON)) {
321 return false;
324 if (IsCallable(toJSON)) {
325 keyStr = KeyStringifier<KeyType>::toString(cx, key);
326 if (!keyStr) {
327 return false;
330 RootedValue arg0(cx, StringValue(keyStr));
331 if (!js::Call(cx, toJSON, vp, arg0, vp)) {
332 return false;
337 /* Step 3. */
338 if (scx->replacer && scx->replacer->isCallable()) {
339 MOZ_ASSERT(holder != nullptr,
340 "holder object must be present when replacer is callable");
342 if (!keyStr) {
343 keyStr = KeyStringifier<KeyType>::toString(cx, key);
344 if (!keyStr) {
345 return false;
349 RootedValue arg0(cx, StringValue(keyStr));
350 RootedValue replacerVal(cx, ObjectValue(*scx->replacer));
351 if (!js::Call(cx, replacerVal, holder, arg0, vp, vp)) {
352 return false;
356 /* Step 4. */
357 if (vp.get().isObject()) {
358 RootedObject obj(cx, &vp.get().toObject());
360 ESClass cls;
361 if (!JS::GetBuiltinClass(cx, obj, &cls)) {
362 return false;
365 if (cls == ESClass::Number) {
366 double d;
367 if (!ToNumber(cx, vp, &d)) {
368 return false;
370 vp.setNumber(d);
371 } else if (cls == ESClass::String) {
372 JSString* str = ToStringSlow<CanGC>(cx, vp);
373 if (!str) {
374 return false;
376 vp.setString(str);
377 } else if (cls == ESClass::Boolean || cls == ESClass::BigInt ||
378 IF_RECORD_TUPLE(
379 obj->is<RecordObject>() || obj->is<TupleObject>(), false)) {
380 if (!Unbox(cx, obj, vp)) {
381 return false;
386 return true;
390 * Determines whether a value which has passed by ES5 150.2.3 Str steps 1-4's
391 * gauntlet will result in Str returning |undefined|. This function is used to
392 * properly omit properties resulting in such values when stringifying objects,
393 * while properly stringifying such properties as null when they're encountered
394 * in arrays.
396 static inline bool IsFilteredValue(const Value& v) {
397 MOZ_ASSERT_IF(v.isMagic(), v.isMagic(JS_ELEMENTS_HOLE));
398 return v.isUndefined() || v.isSymbol() || v.isMagic() || IsCallable(v);
401 class CycleDetector {
402 public:
403 CycleDetector(StringifyContext* scx, HandleObject obj)
404 : stack_(&scx->stack), obj_(obj), appended_(false) {}
406 MOZ_ALWAYS_INLINE bool foundCycle(JSContext* cx) {
407 JSObject* obj = obj_;
408 for (JSObject* obj2 : stack_) {
409 if (MOZ_UNLIKELY(obj == obj2)) {
410 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
411 JSMSG_JSON_CYCLIC_VALUE);
412 return false;
415 appended_ = stack_.append(obj);
416 return appended_;
419 ~CycleDetector() {
420 if (MOZ_LIKELY(appended_)) {
421 MOZ_ASSERT(stack_.back() == obj_);
422 stack_.popBack();
426 private:
427 MutableHandle<ObjectVector> stack_;
428 HandleObject obj_;
429 bool appended_;
432 #ifdef ENABLE_RECORD_TUPLE
433 enum class JOType { Record, Object };
434 template <JOType type = JOType::Object>
435 #endif
436 /* ES5 15.12.3 JO. */
437 static bool JO(JSContext* cx, HandleObject obj, StringifyContext* scx) {
439 * This method implements the JO algorithm in ES5 15.12.3, but:
441 * * The algorithm is somewhat reformulated to allow the final string to
442 * be streamed into a single buffer, rather than be created and copied
443 * into place incrementally as the ES5 algorithm specifies it. This
444 * requires moving portions of the Str call in 8a into this algorithm
445 * (and in JA as well).
448 #ifdef ENABLE_RECORD_TUPLE
449 RecordType* rec;
451 if constexpr (type == JOType::Record) {
452 MOZ_ASSERT(obj->is<RecordType>());
453 rec = &obj->as<RecordType>();
454 } else {
455 MOZ_ASSERT(!IsExtendedPrimitive(*obj));
457 #endif
458 MOZ_ASSERT_IF(scx->maybeSafely, obj->is<PlainObject>());
460 /* Steps 1-2, 11. */
461 CycleDetector detect(scx, obj);
462 if (!detect.foundCycle(cx)) {
463 return false;
466 if (!scx->sb.append('{')) {
467 return false;
470 /* Steps 5-7. */
471 Maybe<RootedIdVector> ids;
472 const RootedIdVector* props;
473 if (scx->replacer && !scx->replacer->isCallable()) {
474 // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
475 // might have been a revocable proxy to an array. Such a proxy
476 // satisfies |IsArray|, but any side effect of JSON.stringify
477 // could revoke the proxy so that |!IsArray(scx->replacer)|. See
478 // bug 1196497.
479 props = &scx->propertyList;
480 } else {
481 MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0);
482 ids.emplace(cx);
483 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr())) {
484 return false;
486 props = ids.ptr();
489 /* My kingdom for not-quite-initialized-from-the-start references. */
490 const RootedIdVector& propertyList = *props;
492 /* Steps 8-10, 13. */
493 bool wroteMember = false;
494 RootedId id(cx);
495 for (size_t i = 0, len = propertyList.length(); i < len; i++) {
496 if (!CheckForInterrupt(cx)) {
497 return false;
501 * Steps 8a-8b. Note that the call to Str is broken up into 1) getting
502 * the property; 2) processing for toJSON, calling the replacer, and
503 * handling boxed Number/String/Boolean objects; 3) filtering out
504 * values which process to |undefined|, and 4) stringifying all values
505 * which pass the filter.
507 id = propertyList[i];
508 RootedValue outputValue(cx);
509 #ifdef DEBUG
510 if (scx->maybeSafely) {
511 PropertyResult prop;
512 if (!NativeLookupOwnPropertyNoResolve(cx, &obj->as<NativeObject>(), id,
513 &prop)) {
514 return false;
516 MOZ_ASSERT(prop.isNativeProperty() &&
517 prop.propertyInfo().isDataDescriptor());
519 #endif // DEBUG
521 #ifdef ENABLE_RECORD_TUPLE
522 if constexpr (type == JOType::Record) {
523 MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, id, &outputValue));
524 } else
525 #endif
527 RootedValue objValue(cx, ObjectValue(*obj));
528 if (!GetProperty(cx, obj, objValue, id, &outputValue)) {
529 return false;
532 if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx)) {
533 return false;
535 if (IsFilteredValue(outputValue)) {
536 continue;
539 /* Output a comma unless this is the first member to write. */
540 if (wroteMember && !scx->sb.append(',')) {
541 return false;
543 wroteMember = true;
545 if (!WriteIndent(scx, scx->depth)) {
546 return false;
549 JSString* s = IdToString(cx, id);
550 if (!s) {
551 return false;
554 if (!Quote(cx, scx->sb, s) || !scx->sb.append(':') ||
555 !(scx->gap.empty() || scx->sb.append(' ')) ||
556 !Str(cx, outputValue, scx)) {
557 return false;
561 if (wroteMember && !WriteIndent(scx, scx->depth - 1)) {
562 return false;
565 return scx->sb.append('}');
568 // For JSON.stringify and JSON.parse with a reviver function, we need to know
569 // the length of an object for which JS::IsArray returned true. This must be
570 // either an ArrayObject or a proxy wrapping one.
571 static MOZ_ALWAYS_INLINE bool GetLengthPropertyForArrayLike(JSContext* cx,
572 HandleObject obj,
573 uint32_t* lengthp) {
574 if (MOZ_LIKELY(obj->is<ArrayObject>())) {
575 *lengthp = obj->as<ArrayObject>().length();
576 return true;
578 #ifdef ENABLE_RECORD_TUPLE
579 if (obj->is<TupleType>()) {
580 *lengthp = obj->as<TupleType>().length();
581 return true;
583 #endif
585 MOZ_ASSERT(obj->is<ProxyObject>());
587 uint64_t len = 0;
588 if (!GetLengthProperty(cx, obj, &len)) {
589 return false;
592 // A scripted proxy wrapping an array can return a length value larger than
593 // UINT32_MAX. Stringification will likely report an OOM in this case. Match
594 // other JS engines and report an early error in this case, although
595 // technically this is observable, for example when stringifying with a
596 // replacer function.
597 if (len > UINT32_MAX) {
598 ReportAllocationOverflow(cx);
599 return false;
602 *lengthp = uint32_t(len);
603 return true;
606 /* ES5 15.12.3 JA. */
607 static bool JA(JSContext* cx, HandleObject obj, StringifyContext* scx) {
609 * This method implements the JA algorithm in ES5 15.12.3, but:
611 * * The algorithm is somewhat reformulated to allow the final string to
612 * be streamed into a single buffer, rather than be created and copied
613 * into place incrementally as the ES5 algorithm specifies it. This
614 * requires moving portions of the Str call in 8a into this algorithm
615 * (and in JO as well).
618 /* Steps 1-2, 11. */
619 CycleDetector detect(scx, obj);
620 if (!detect.foundCycle(cx)) {
621 return false;
624 if (!scx->sb.append('[')) {
625 return false;
628 /* Step 6. */
629 uint32_t length;
630 if (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
631 return false;
634 /* Steps 7-10. */
635 if (length != 0) {
636 /* Steps 4, 10b(i). */
637 if (!WriteIndent(scx, scx->depth)) {
638 return false;
641 /* Steps 7-10. */
642 RootedValue outputValue(cx);
643 for (uint32_t i = 0; i < length; i++) {
644 if (!CheckForInterrupt(cx)) {
645 return false;
649 * Steps 8a-8c. Again note how the call to the spec's Str method
650 * is broken up into getting the property, running it past toJSON
651 * and the replacer and maybe unboxing, and interpreting some
652 * values as |null| in separate steps.
654 #ifdef DEBUG
655 if (scx->maybeSafely) {
657 * Trying to do a JS_AlreadyHasOwnElement runs the risk of
658 * hitting OOM on jsid creation. Let's just assert sanity for
659 * small enough indices.
661 MOZ_ASSERT(obj->is<ArrayObject>());
662 MOZ_ASSERT(obj->is<NativeObject>());
663 Rooted<NativeObject*> nativeObj(cx, &obj->as<NativeObject>());
664 if (i <= PropertyKey::IntMax) {
665 MOZ_ASSERT(
666 nativeObj->containsDenseElement(i) != nativeObj->isIndexed(),
667 "the array must either be small enough to remain "
668 "fully dense (and otherwise un-indexed), *or* "
669 "all its initially-dense elements were sparsified "
670 "and the object is indexed");
671 } else {
672 MOZ_ASSERT(nativeObj->isIndexed());
675 #endif
676 if (!GetElement(cx, obj, i, &outputValue)) {
677 return false;
679 if (!PreprocessValue(cx, obj, i, &outputValue, scx)) {
680 return false;
682 if (IsFilteredValue(outputValue)) {
683 if (!scx->sb.append("null")) {
684 return false;
686 } else {
687 if (!Str(cx, outputValue, scx)) {
688 return false;
692 /* Steps 3, 4, 10b(i). */
693 if (i < length - 1) {
694 if (!scx->sb.append(',')) {
695 return false;
697 if (!WriteIndent(scx, scx->depth)) {
698 return false;
703 /* Step 10(b)(iii). */
704 if (!WriteIndent(scx, scx->depth - 1)) {
705 return false;
709 return scx->sb.append(']');
712 static bool Str(JSContext* cx, const Value& v, StringifyContext* scx) {
713 /* Step 11 must be handled by the caller. */
714 MOZ_ASSERT(!IsFilteredValue(v));
717 * This method implements the Str algorithm in ES5 15.12.3, but:
719 * * We move property retrieval (step 1) into callers to stream the
720 * stringification process and avoid constantly copying strings.
721 * * We move the preprocessing in steps 2-4 into a helper function to
722 * allow both JO and JA to use this method. While JA could use it
723 * without this move, JO must omit any |undefined|-valued property per
724 * so it can't stream out a value using the Str method exactly as
725 * defined by ES5.
726 * * We move step 11 into callers, again to ease streaming.
729 /* Step 8. */
730 if (v.isString()) {
731 return Quote(cx, scx->sb, v.toString());
734 /* Step 5. */
735 if (v.isNull()) {
736 return scx->sb.append("null");
739 /* Steps 6-7. */
740 if (v.isBoolean()) {
741 return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false");
744 /* Step 9. */
745 if (v.isNumber()) {
746 if (v.isDouble()) {
747 if (!std::isfinite(v.toDouble())) {
748 MOZ_ASSERT(!scx->maybeSafely,
749 "input JS::ToJSONMaybeSafely must not include "
750 "reachable non-finite numbers");
751 return scx->sb.append("null");
755 return NumberValueToStringBuffer(v, scx->sb);
758 /* Step 10 in the BigInt proposal. */
759 if (v.isBigInt()) {
760 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
761 JSMSG_BIGINT_NOT_SERIALIZABLE);
762 return false;
765 AutoCheckRecursionLimit recursion(cx);
766 if (!recursion.check(cx)) {
767 return false;
770 /* Step 10. */
771 MOZ_ASSERT(v.hasObjectPayload());
772 RootedObject obj(cx, &v.getObjectPayload());
774 MOZ_ASSERT(
775 !scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(),
776 "input to JS::ToJSONMaybeSafely must not include reachable "
777 "objects that are neither arrays nor plain objects");
779 scx->depth++;
780 auto dec = mozilla::MakeScopeExit([&] { scx->depth--; });
782 #ifdef ENABLE_RECORD_TUPLE
783 if (v.isExtendedPrimitive()) {
784 if (obj->is<RecordType>()) {
785 return JO<JOType::Record>(cx, obj, scx);
787 if (obj->is<TupleType>()) {
788 return JA(cx, obj, scx);
790 MOZ_CRASH("Unexpected extended primitive - boxes cannot be stringified.");
792 #endif
794 bool isArray;
795 if (!IsArray(cx, obj, &isArray)) {
796 return false;
799 return isArray ? JA(cx, obj, scx) : JO(cx, obj, scx);
802 static bool CanFastStringifyObject(NativeObject* obj) {
803 if (ClassCanHaveExtraEnumeratedProperties(obj->getClass())) {
804 return false;
807 if (obj->is<ArrayObject>()) {
808 // Arrays will look up all keys [0..length) so disallow anything that could
809 // find those keys anywhere but in the dense elements.
810 if (!IsPackedArray(obj) && ObjectMayHaveExtraIndexedProperties(obj)) {
811 return false;
813 } else {
814 // Non-Arrays will only look at own properties, but still disallow any
815 // indexed properties other than in the dense elements because they would
816 // require sorting.
817 if (ObjectMayHaveExtraIndexedOwnProperties(obj)) {
818 return false;
822 // Only used for internal environment objects that should never be passed to
823 // JSON.stringify.
824 MOZ_ASSERT(!obj->getOpsLookupProperty());
826 #ifdef ENABLE_RECORD_TUPLE
827 if (ObjectValue(*obj).isExtendedPrimitive()) {
828 return false;
830 #endif
832 return true;
835 #define FOR_EACH_STRINGIFY_BAIL_REASON(MACRO) \
836 MACRO(NO_REASON) \
837 MACRO(INELIGIBLE_OBJECT) \
838 MACRO(DEEP_RECURSION) \
839 MACRO(NON_DATA_PROPERTY) \
840 MACRO(TOO_MANY_PROPERTIES) \
841 MACRO(BIGINT) \
842 MACRO(API) \
843 MACRO(HAVE_REPLACER) \
844 MACRO(HAVE_SPACE) \
845 MACRO(PRIMITIVE) \
846 MACRO(HAVE_TOJSON) \
847 MACRO(IMPURE_LOOKUP) \
848 MACRO(INTERRUPT)
850 enum class BailReason : uint8_t {
851 #define DECLARE_ENUM(name) name,
852 FOR_EACH_STRINGIFY_BAIL_REASON(DECLARE_ENUM)
853 #undef DECLARE_ENUM
856 static const char* DescribeStringifyBailReason(BailReason whySlow) {
857 switch (whySlow) {
858 #define ENUM_NAME(name) \
859 case BailReason::name: \
860 return #name;
861 FOR_EACH_STRINGIFY_BAIL_REASON(ENUM_NAME)
862 #undef ENUM_NAME
863 default:
864 return "Unknown";
868 // Iterator over all the dense elements of an object. Used
869 // for both Arrays and non-Arrays.
870 class DenseElementsIteratorForJSON {
871 HeapSlotArray elements;
872 uint32_t element;
874 // Arrays can have a length less than getDenseInitializedLength(), in which
875 // case the remaining Array elements are treated as UndefinedValue.
876 uint32_t numElements;
877 uint32_t length;
879 public:
880 explicit DenseElementsIteratorForJSON(NativeObject* nobj)
881 : elements(nobj->getDenseElements()),
882 element(0),
883 numElements(nobj->getDenseInitializedLength()) {
884 length = nobj->is<ArrayObject>() ? nobj->as<ArrayObject>().length()
885 : numElements;
888 bool done() const { return element == length; }
890 Value next() {
891 // For Arrays, steps 6-8 of
892 // https://262.ecma-international.org/13.0/#sec-serializejsonarray. For
893 // non-Arrays, step 6a of
894 // https://262.ecma-international.org/13.0/#sec-serializejsonobject
895 // following the order from
896 // https://262.ecma-international.org/13.0/#sec-ordinaryownpropertykeys
898 MOZ_ASSERT(!done());
899 auto i = element++;
900 // Consider specializing the iterator for Arrays vs non-Arrays to avoid this
901 // branch.
902 return i < numElements ? elements.begin()[i] : UndefinedValue();
905 uint32_t getIndex() const { return element; }
908 // An iterator over the non-element properties of a Shape, returned in forward
909 // (creation) order. Note that it is fallible, so after iteration is complete
910 // isOverflowed() should be called to verify that the results are actually
911 // complete.
913 class ShapePropertyForwardIterNoGC {
914 // Pointer to the current PropMap with length and an index within it.
915 PropMap* map_;
916 uint32_t mapLength_;
917 uint32_t i_ = 0;
919 // Stack of PropMaps to iterate through, oldest properties on top. The current
920 // map (map_, above) is never on this stack.
921 mozilla::Vector<PropMap*> stack_;
923 const NativeShape* shape_;
925 MOZ_ALWAYS_INLINE void settle() {
926 while (true) {
927 if (MOZ_UNLIKELY(i_ == mapLength_)) {
928 i_ = 0;
929 if (stack_.empty()) {
930 mapLength_ = 0; // Done
931 return;
933 map_ = stack_.back();
934 stack_.popBack();
935 mapLength_ =
936 stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
937 } else if (MOZ_UNLIKELY(shape_->isDictionary() && !map_->hasKey(i_))) {
938 // Dictionary maps can have "holes" for removed properties, so keep
939 // going until we find a non-hole slot.
940 i_++;
941 } else {
942 return;
947 public:
948 explicit ShapePropertyForwardIterNoGC(NativeShape* shape) : shape_(shape) {
949 // Set map_ to the PropMap containing the first property (the deepest map in
950 // the previous() chain). Push pointers to all other PropMaps onto stack_.
951 map_ = shape->propMap();
952 if (!map_) {
953 // No properties.
954 i_ = mapLength_ = 0;
955 return;
957 while (map_->hasPrevious()) {
958 if (!stack_.append(map_)) {
959 // Overflowed.
960 i_ = mapLength_ = UINT32_MAX;
961 return;
963 map_ = map_->asLinked()->previous();
966 // Set mapLength_ to the number of properties in map_ (including dictionary
967 // holes, if any.)
968 mapLength_ = stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
970 settle();
973 bool done() const { return i_ == mapLength_; }
974 bool isOverflowed() const { return i_ == UINT32_MAX; }
976 void operator++(int) {
977 MOZ_ASSERT(!done());
978 i_++;
979 settle();
982 PropertyInfoWithKey get() const {
983 MOZ_ASSERT(!done());
984 return map_->getPropertyInfoWithKey(i_);
987 PropertyInfoWithKey operator*() const { return get(); }
989 // Fake pointer struct to make operator-> work.
990 // See https://stackoverflow.com/a/52856349.
991 struct FakePtr {
992 PropertyInfoWithKey val_;
993 const PropertyInfoWithKey* operator->() const { return &val_; }
995 FakePtr operator->() const { return {get()}; }
998 // Iterator over EnumerableOwnPropertyNames
999 // https://262.ecma-international.org/13.0/#sec-enumerableownpropertynames
1000 // that fails if it encounters any accessor properties, as they are not handled
1001 // by JSON FastStr, or if it sees too many properties on one object.
1002 class OwnNonIndexKeysIterForJSON {
1003 ShapePropertyForwardIterNoGC shapeIter;
1004 bool done_ = false;
1005 BailReason fastFailed_ = BailReason::NO_REASON;
1007 void settle() {
1008 // Skip over any non-enumerable or Symbol properties, and permanently fail
1009 // if any enumerable non-data properties are encountered.
1010 for (; !shapeIter.done(); shapeIter++) {
1011 if (!shapeIter->enumerable()) {
1012 continue;
1014 if (!shapeIter->isDataProperty()) {
1015 fastFailed_ = BailReason::NON_DATA_PROPERTY;
1016 done_ = true;
1017 return;
1019 PropertyKey id = shapeIter->key();
1020 if (!id.isSymbol()) {
1021 return;
1024 done_ = true;
1027 public:
1028 explicit OwnNonIndexKeysIterForJSON(const NativeObject* nobj)
1029 : shapeIter(nobj->shape()) {
1030 if (MOZ_UNLIKELY(shapeIter.isOverflowed())) {
1031 fastFailed_ = BailReason::TOO_MANY_PROPERTIES;
1032 done_ = true;
1033 return;
1035 if (!nobj->hasEnumerableProperty()) {
1036 // Non-Arrays with no enumerable properties can just be skipped.
1037 MOZ_ASSERT(!nobj->is<ArrayObject>());
1038 done_ = true;
1039 return;
1041 settle();
1044 bool done() const { return done_ || shapeIter.done(); }
1045 BailReason cannotFastStringify() const { return fastFailed_; }
1047 PropertyInfoWithKey next() {
1048 MOZ_ASSERT(!done());
1049 PropertyInfoWithKey prop = shapeIter.get();
1050 shapeIter++;
1051 settle();
1052 return prop;
1056 // Steps from https://262.ecma-international.org/13.0/#sec-serializejsonproperty
1057 static bool EmitSimpleValue(JSContext* cx, StringBuffer& sb, const Value& v) {
1058 /* Step 8. */
1059 if (v.isString()) {
1060 return Quote(cx, sb, v.toString());
1063 /* Step 5. */
1064 if (v.isNull()) {
1065 return sb.append("null");
1068 /* Steps 6-7. */
1069 if (v.isBoolean()) {
1070 return v.toBoolean() ? sb.append("true") : sb.append("false");
1073 /* Step 9. */
1074 if (v.isNumber()) {
1075 if (v.isDouble()) {
1076 if (!std::isfinite(v.toDouble())) {
1077 return sb.append("null");
1081 return NumberValueToStringBuffer(v, sb);
1084 // Unrepresentable values.
1085 if (v.isUndefined() || v.isMagic()) {
1086 MOZ_ASSERT_IF(v.isMagic(), v.isMagic(JS_ELEMENTS_HOLE));
1087 return sb.append("null");
1090 /* Step 10. */
1091 MOZ_CRASH("should have validated printable simple value already");
1094 // https://262.ecma-international.org/13.0/#sec-serializejsonproperty step 8b
1095 // where K is an integer index.
1096 static bool EmitQuotedIndexColon(StringBuffer& sb, uint32_t index) {
1097 Int32ToCStringBuf cbuf;
1098 size_t cstrlen;
1099 const char* cstr = ::Int32ToCString(&cbuf, index, &cstrlen);
1100 if (!sb.reserve(sb.length() + 1 + cstrlen + 1 + 1)) {
1101 return false;
1103 sb.infallibleAppend('"');
1104 sb.infallibleAppend(cstr, cstrlen);
1105 sb.infallibleAppend('"');
1106 sb.infallibleAppend(':');
1107 return true;
1110 // Similar to PreprocessValue: replace the value with a simpler one to
1111 // stringify, but also detect whether the value is compatible with the fast
1112 // path. If not, bail out by setting *whySlow and returning true.
1113 static bool PreprocessFastValue(JSContext* cx, Value* vp, StringifyContext* scx,
1114 BailReason* whySlow) {
1115 MOZ_ASSERT(!scx->maybeSafely);
1117 // Steps are from
1118 // https://262.ecma-international.org/13.0/#sec-serializejsonproperty
1120 // Disallow BigInts to avoid caring about BigInt.prototype.toJSON.
1121 if (vp->isBigInt()) {
1122 *whySlow = BailReason::BIGINT;
1123 return true;
1126 if (!vp->isObject()) {
1127 return true;
1130 if (!vp->toObject().is<NativeObject>()) {
1131 *whySlow = BailReason::INELIGIBLE_OBJECT;
1132 return true;
1135 // Step 2: lookup a .toJSON property (and bail if found).
1136 NativeObject* obj = &vp->toObject().as<NativeObject>();
1137 PropertyResult toJSON;
1138 NativeObject* holder;
1139 PropertyKey id = NameToId(cx->names().toJSON);
1140 if (!NativeLookupPropertyInline<NoGC, LookupResolveMode::CheckMayResolve>(
1141 cx, obj, id, &holder, &toJSON)) {
1142 // Looking up this property would require a side effect.
1143 *whySlow = BailReason::IMPURE_LOOKUP;
1144 return true;
1146 if (toJSON.isFound()) {
1147 *whySlow = BailReason::HAVE_TOJSON;
1148 return true;
1151 // Step 4: convert primitive wrapper objects to primitives. Disallowed for
1152 // fast path.
1153 if (obj->is<NumberObject>() || obj->is<StringObject>() ||
1154 obj->is<BooleanObject>() || obj->is<BigIntObject>() ||
1155 IF_RECORD_TUPLE(obj->is<RecordObject>() || obj->is<TupleObject>(),
1156 false)) {
1157 // Primitive wrapper objects can invoke arbitrary code when being coerced to
1158 // their primitive values (eg via @@toStringTag).
1159 *whySlow = BailReason::INELIGIBLE_OBJECT;
1160 return true;
1163 if (obj->isCallable()) {
1164 // Steps 11,12: Callable objects are treated as undefined.
1165 vp->setUndefined();
1166 return true;
1169 if (!CanFastStringifyObject(obj)) {
1170 *whySlow = BailReason::INELIGIBLE_OBJECT;
1171 return true;
1174 return true;
1177 // FastStr maintains an explicit stack to handle nested objects. For each
1178 // object, first the dense elements are iterated, then the named properties
1179 // (included sparse indexes, which will cause FastStr to bail out.)
1181 // The iterators for each of those parts are not merged into a single common
1182 // iterator because the interface is different for the two parts, and they are
1183 // handled separately in the FastStr code.
1184 struct FastStackEntry {
1185 NativeObject* nobj;
1186 Variant<DenseElementsIteratorForJSON, OwnNonIndexKeysIterForJSON> iter;
1187 bool isArray; // Cached nobj->is<ArrayObject>()
1189 // Given an object, a FastStackEntry starts with the dense elements. The
1190 // caller is expected to inspect the variant to use it differently based on
1191 // which iterator is active.
1192 explicit FastStackEntry(NativeObject* obj)
1193 : nobj(obj),
1194 iter(AsVariant(DenseElementsIteratorForJSON(obj))),
1195 isArray(obj->is<ArrayObject>()) {}
1197 // Called by Vector when moving data around.
1198 FastStackEntry(FastStackEntry&& other) noexcept
1199 : nobj(other.nobj), iter(std::move(other.iter)), isArray(other.isArray) {}
1201 // Move assignment, called when updating the `top` entry.
1202 void operator=(FastStackEntry&& other) noexcept {
1203 nobj = other.nobj;
1204 iter = std::move(other.iter);
1205 isArray = other.isArray;
1208 // Advance from dense elements to the named properties.
1209 void advanceToProperties() {
1210 iter = AsVariant(OwnNonIndexKeysIterForJSON(nobj));
1214 static bool FastStr(JSContext* cx, Handle<Value> v, StringifyContext* scx,
1215 BailReason* whySlow) {
1216 MOZ_ASSERT(*whySlow == BailReason::NO_REASON);
1217 MOZ_ASSERT(v.isObject());
1220 * FastStr is an optimistic fast path for the Str algorithm in ES5 15.12.3
1221 * that applies in limited situations. It falls back to Str() if:
1223 * * Any externally visible code attempts to run: getter, enumerate
1224 * hook, toJSON property.
1225 * * Sparse index found (this would require accumulating props and sorting.)
1226 * * Max stack depth is reached. (This will also detect self-referential
1227 * input.)
1229 * Algorithm:
1231 * stack = []
1232 * top = iter(obj)
1233 * wroteMember = false
1234 * OUTER: while true:
1235 * if !wroteMember:
1236 * emit("[" or "{")
1237 * while !top.done():
1238 * key, value = top.next()
1239 * if top is a non-Array and value is skippable:
1240 * continue
1241 * if wroteMember:
1242 * emit(",")
1243 * wroteMember = true
1244 * if value is object:
1245 * emit(key + ":") if top is iterating a non-Array
1246 * stack.push(top)
1247 * top <- value
1248 * wroteMember = false
1249 * continue OUTER
1250 * else:
1251 * emit(value) or emit(key + ":" + value)
1252 * emit("]" or "}")
1253 * if stack is empty: done!
1254 * top <- stack.pop()
1255 * wroteMember = true
1257 * except:
1259 * * The `while !top.done()` loop is split into the dense element portion
1260 * and the slot portion. Each is iterated to completion before advancing
1261 * or finishing.
1263 * * For Arrays, the named properties are not output, but they are still
1264 * scanned to bail if any numeric keys are found that could be indexes.
1267 // FastStr will bail if an interrupt is requested in the middle of an
1268 // operation, so handle any interrupts now before starting. Note: this can GC,
1269 // but after this point nothing should be able to GC unless something fails,
1270 // so rooting is unnecessary.
1271 if (!CheckForInterrupt(cx)) {
1272 return false;
1275 constexpr size_t MAX_STACK_DEPTH = 20;
1276 Vector<FastStackEntry> stack(cx);
1277 if (!stack.reserve(MAX_STACK_DEPTH - 1)) {
1278 return false;
1280 // Construct an iterator for the object,
1281 // https://262.ecma-international.org/13.0/#sec-serializejsonobject step 6:
1282 // EnumerableOwnPropertyNames or
1283 // https://262.ecma-international.org/13.0/#sec-serializejsonarray step 7-8.
1284 FastStackEntry top(&v.toObject().as<NativeObject>());
1285 bool wroteMember = false;
1287 if (!CanFastStringifyObject(top.nobj)) {
1288 *whySlow = BailReason::INELIGIBLE_OBJECT;
1289 return true;
1292 while (true) {
1293 if (!wroteMember) {
1294 if (!scx->sb.append(top.isArray ? '[' : '{')) {
1295 return false;
1299 if (top.iter.is<DenseElementsIteratorForJSON>()) {
1300 auto& iter = top.iter.as<DenseElementsIteratorForJSON>();
1301 bool nestedObject = false;
1302 while (!iter.done()) {
1303 // Interrupts can GC and we are working with unrooted pointers.
1304 if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
1305 cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
1306 *whySlow = BailReason::INTERRUPT;
1307 return true;
1310 uint32_t index = iter.getIndex();
1311 Value val = iter.next();
1313 if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
1314 return false;
1316 if (*whySlow != BailReason::NO_REASON) {
1317 return true;
1319 if (IsFilteredValue(val)) {
1320 if (top.isArray) {
1321 // Arrays convert unrepresentable values to "null".
1322 val = UndefinedValue();
1323 } else {
1324 // Objects skip unrepresentable values.
1325 continue;
1329 if (wroteMember && !scx->sb.append(',')) {
1330 return false;
1332 wroteMember = true;
1334 if (!top.isArray) {
1335 if (!EmitQuotedIndexColon(scx->sb, index)) {
1336 return false;
1340 if (val.isObject()) {
1341 if (stack.length() >= MAX_STACK_DEPTH - 1) {
1342 *whySlow = BailReason::DEEP_RECURSION;
1343 return true;
1345 // Save the current iterator position on the stack and
1346 // switch to processing the nested value.
1347 stack.infallibleAppend(std::move(top));
1348 top = FastStackEntry(&val.toObject().as<NativeObject>());
1349 wroteMember = false;
1350 nestedObject = true; // Break out to the outer loop.
1351 break;
1353 if (!EmitSimpleValue(cx, scx->sb, val)) {
1354 return false;
1358 if (nestedObject) {
1359 continue; // Break out to outer loop.
1362 MOZ_ASSERT(iter.done());
1363 if (top.isArray) {
1364 MOZ_ASSERT(!top.nobj->isIndexed() || IsPackedArray(top.nobj));
1365 } else {
1366 top.advanceToProperties();
1370 if (top.iter.is<OwnNonIndexKeysIterForJSON>()) {
1371 auto& iter = top.iter.as<OwnNonIndexKeysIterForJSON>();
1372 bool nesting = false;
1373 while (!iter.done()) {
1374 // Interrupts can GC and we are working with unrooted pointers.
1375 if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
1376 cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
1377 *whySlow = BailReason::INTERRUPT;
1378 return true;
1381 PropertyInfoWithKey prop = iter.next();
1383 // A non-Array with indexed elements would need to sort the indexes
1384 // numerically, which this code does not support. These objects are
1385 // skipped when obj->isIndexed(), so no index properties should be found
1386 // here.
1387 mozilla::DebugOnly<uint32_t> index = -1;
1388 MOZ_ASSERT(!IdIsIndex(prop.key(), &index));
1390 Value val = top.nobj->getSlot(prop.slot());
1391 if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
1392 return false;
1394 if (*whySlow != BailReason::NO_REASON) {
1395 return true;
1397 if (IsFilteredValue(val)) {
1398 // Undefined check in
1399 // https://262.ecma-international.org/13.0/#sec-serializejsonobject
1400 // step 8b, covering undefined, symbol
1401 continue;
1404 if (wroteMember && !scx->sb.append(",")) {
1405 return false;
1407 wroteMember = true;
1409 MOZ_ASSERT(prop.key().isString());
1410 if (!Quote(cx, scx->sb, prop.key().toString())) {
1411 return false;
1414 if (!scx->sb.append(':')) {
1415 return false;
1417 if (val.isObject()) {
1418 if (stack.length() >= MAX_STACK_DEPTH - 1) {
1419 *whySlow = BailReason::DEEP_RECURSION;
1420 return true;
1422 // Save the current iterator position on the stack and
1423 // switch to processing the nested value.
1424 stack.infallibleAppend(std::move(top));
1425 top = FastStackEntry(&val.toObject().as<NativeObject>());
1426 wroteMember = false;
1427 nesting = true; // Break out to the outer loop.
1428 break;
1430 if (!EmitSimpleValue(cx, scx->sb, val)) {
1431 return false;
1434 *whySlow = iter.cannotFastStringify();
1435 if (*whySlow != BailReason::NO_REASON) {
1436 return true;
1438 if (nesting) {
1439 continue; // Break out to outer loop.
1441 MOZ_ASSERT(iter.done());
1444 if (!scx->sb.append(top.isArray ? ']' : '}')) {
1445 return false;
1447 if (stack.empty()) {
1448 return true; // Success!
1450 top = std::move(stack.back());
1452 stack.popBack();
1453 wroteMember = true;
1457 /* ES6 24.3.2. */
1458 bool js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_,
1459 const Value& space_, StringBuffer& sb,
1460 StringifyBehavior stringifyBehavior) {
1461 RootedObject replacer(cx, replacer_);
1462 RootedValue space(cx, space_);
1464 MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
1465 space.isNull());
1466 MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
1467 vp.isObject());
1469 * This uses MOZ_ASSERT, since it's actually asserting something jsapi
1470 * consumers could get wrong, so needs a better error message.
1472 MOZ_ASSERT(stringifyBehavior != StringifyBehavior::RestrictedSafe ||
1473 vp.toObject().is<PlainObject>() ||
1474 vp.toObject().is<ArrayObject>(),
1475 "input to JS::ToJSONMaybeSafely must be a plain object or array");
1477 /* Step 4. */
1478 RootedIdVector propertyList(cx);
1479 BailReason whySlow = BailReason::NO_REASON;
1480 if (stringifyBehavior == StringifyBehavior::SlowOnly ||
1481 stringifyBehavior == StringifyBehavior::RestrictedSafe) {
1482 whySlow = BailReason::API;
1484 if (replacer) {
1485 whySlow = BailReason::HAVE_REPLACER;
1486 bool isArray;
1487 if (replacer->isCallable()) {
1488 /* Step 4a(i): use replacer to transform values. */
1489 } else if (!IsArray(cx, replacer, &isArray)) {
1490 return false;
1491 } else if (isArray) {
1492 /* Step 4b(iii). */
1494 /* Step 4b(iii)(2-3). */
1495 uint32_t len;
1496 if (!GetLengthPropertyForArrayLike(cx, replacer, &len)) {
1497 return false;
1500 // Cap the initial size to a moderately small value. This avoids
1501 // ridiculous over-allocation if an array with bogusly-huge length
1502 // is passed in. If we end up having to add elements past this
1503 // size, the set will naturally resize to accommodate them.
1504 const uint32_t MaxInitialSize = 32;
1505 Rooted<GCHashSet<jsid>> idSet(
1506 cx, GCHashSet<jsid>(cx, std::min(len, MaxInitialSize)));
1508 /* Step 4b(iii)(4). */
1509 uint32_t k = 0;
1511 /* Step 4b(iii)(5). */
1512 RootedValue item(cx);
1513 for (; k < len; k++) {
1514 if (!CheckForInterrupt(cx)) {
1515 return false;
1518 /* Step 4b(iii)(5)(a-b). */
1519 if (!GetElement(cx, replacer, k, &item)) {
1520 return false;
1523 /* Step 4b(iii)(5)(c-g). */
1524 RootedId id(cx);
1525 if (item.isNumber() || item.isString()) {
1526 if (!PrimitiveValueToId<CanGC>(cx, item, &id)) {
1527 return false;
1529 } else {
1530 ESClass cls;
1531 if (!GetClassOfValue(cx, item, &cls)) {
1532 return false;
1535 if (cls != ESClass::String && cls != ESClass::Number) {
1536 continue;
1539 JSAtom* atom = ToAtom<CanGC>(cx, item);
1540 if (!atom) {
1541 return false;
1544 id.set(AtomToId(atom));
1547 /* Step 4b(iii)(5)(g). */
1548 auto p = idSet.lookupForAdd(id);
1549 if (!p) {
1550 /* Step 4b(iii)(5)(g)(i). */
1551 if (!idSet.add(p, id) || !propertyList.append(id)) {
1552 return false;
1556 } else {
1557 replacer = nullptr;
1561 /* Step 5. */
1562 if (space.isObject()) {
1563 RootedObject spaceObj(cx, &space.toObject());
1565 ESClass cls;
1566 if (!JS::GetBuiltinClass(cx, spaceObj, &cls)) {
1567 return false;
1570 if (cls == ESClass::Number) {
1571 double d;
1572 if (!ToNumber(cx, space, &d)) {
1573 return false;
1575 space = NumberValue(d);
1576 } else if (cls == ESClass::String) {
1577 JSString* str = ToStringSlow<CanGC>(cx, space);
1578 if (!str) {
1579 return false;
1581 space = StringValue(str);
1585 StringBuffer gap(cx);
1587 if (space.isNumber()) {
1588 /* Step 6. */
1589 double d;
1590 MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
1591 d = std::min(10.0, d);
1592 if (d >= 1 && !gap.appendN(' ', uint32_t(d))) {
1593 return false;
1595 } else if (space.isString()) {
1596 /* Step 7. */
1597 JSLinearString* str = space.toString()->ensureLinear(cx);
1598 if (!str) {
1599 return false;
1601 size_t len = std::min(size_t(10), str->length());
1602 if (!gap.appendSubstring(str, 0, len)) {
1603 return false;
1605 } else {
1606 /* Step 8. */
1607 MOZ_ASSERT(gap.empty());
1609 if (!gap.empty()) {
1610 whySlow = BailReason::HAVE_SPACE;
1613 Rooted<PlainObject*> wrapper(cx);
1614 RootedId emptyId(cx, NameToId(cx->names().empty_));
1615 if (replacer && replacer->isCallable()) {
1616 // We can skip creating the initial wrapper object if no replacer
1617 // function is present.
1619 /* Step 9. */
1620 wrapper = NewPlainObject(cx);
1621 if (!wrapper) {
1622 return false;
1625 /* Steps 10-11. */
1626 if (!NativeDefineDataProperty(cx, wrapper, emptyId, vp, JSPROP_ENUMERATE)) {
1627 return false;
1631 /* Step 12. */
1632 Rooted<JSAtom*> fastJSON(cx);
1633 if (whySlow == BailReason::NO_REASON) {
1634 MOZ_ASSERT(propertyList.empty());
1635 MOZ_ASSERT(stringifyBehavior != StringifyBehavior::RestrictedSafe);
1636 StringifyContext scx(cx, sb, gap, nullptr, propertyList, false);
1637 if (!PreprocessFastValue(cx, vp.address(), &scx, &whySlow)) {
1638 return false;
1640 if (!vp.isObject()) {
1641 // "Fast" stringify of primitives would create a wrapper object and thus
1642 // be slower than regular stringify.
1643 whySlow = BailReason::PRIMITIVE;
1645 if (whySlow == BailReason::NO_REASON) {
1646 if (!FastStr(cx, vp, &scx, &whySlow)) {
1647 return false;
1649 if (whySlow == BailReason::NO_REASON) {
1650 // Fast stringify succeeded!
1651 if (stringifyBehavior != StringifyBehavior::Compare) {
1652 return true;
1654 fastJSON = scx.sb.finishAtom();
1655 if (!fastJSON) {
1656 return false;
1659 scx.sb.clear(); // Preserves allocated space.
1663 if (MOZ_UNLIKELY((stringifyBehavior == StringifyBehavior::FastOnly) &&
1664 (whySlow != BailReason::NO_REASON))) {
1665 JS_ReportErrorASCII(cx, "JSON stringify failed mandatory fast path: %s",
1666 DescribeStringifyBailReason(whySlow));
1667 return false;
1670 // Slow, general path.
1672 StringifyContext scx(cx, sb, gap, replacer, propertyList,
1673 stringifyBehavior == StringifyBehavior::RestrictedSafe);
1674 if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx)) {
1675 return false;
1677 if (IsFilteredValue(vp)) {
1678 return true;
1681 if (!Str(cx, vp, &scx)) {
1682 return false;
1685 // For StringBehavior::Compare, when the fast path succeeded.
1686 if (MOZ_UNLIKELY(fastJSON)) {
1687 JSAtom* slowJSON = scx.sb.finishAtom();
1688 if (!slowJSON) {
1689 return false;
1691 if (fastJSON != slowJSON) {
1692 MOZ_CRASH("JSON.stringify mismatch between fast and slow paths");
1694 // Put the JSON back into the StringBuffer for returning.
1695 if (!sb.append(slowJSON)) {
1696 return false;
1700 return true;
1703 /* ES5 15.12.2 Walk. */
1704 static bool Walk(JSContext* cx, HandleObject holder, HandleId name,
1705 HandleValue reviver, MutableHandleValue vp) {
1706 AutoCheckRecursionLimit recursion(cx);
1707 if (!recursion.check(cx)) {
1708 return false;
1711 /* Step 1. */
1712 RootedValue val(cx);
1713 if (!GetProperty(cx, holder, holder, name, &val)) {
1714 return false;
1717 /* Step 2. */
1718 if (val.isObject()) {
1719 RootedObject obj(cx, &val.toObject());
1721 bool isArray;
1722 if (!IsArray(cx, obj, &isArray)) {
1723 return false;
1726 if (isArray) {
1727 /* Step 2a(ii). */
1728 uint32_t length;
1729 if (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
1730 return false;
1733 /* Step 2a(i), 2a(iii-iv). */
1734 RootedId id(cx);
1735 RootedValue newElement(cx);
1736 for (uint32_t i = 0; i < length; i++) {
1737 if (!CheckForInterrupt(cx)) {
1738 return false;
1741 if (!IndexToId(cx, i, &id)) {
1742 return false;
1745 /* Step 2a(iii)(1). */
1746 if (!Walk(cx, obj, id, reviver, &newElement)) {
1747 return false;
1750 ObjectOpResult ignored;
1751 if (newElement.isUndefined()) {
1752 /* Step 2a(iii)(2). The spec deliberately ignores strict failure. */
1753 if (!DeleteProperty(cx, obj, id, ignored)) {
1754 return false;
1756 } else {
1757 /* Step 2a(iii)(3). The spec deliberately ignores strict failure. */
1758 Rooted<PropertyDescriptor> desc(
1759 cx, PropertyDescriptor::Data(newElement,
1760 {JS::PropertyAttribute::Configurable,
1761 JS::PropertyAttribute::Enumerable,
1762 JS::PropertyAttribute::Writable}));
1763 if (!DefineProperty(cx, obj, id, desc, ignored)) {
1764 return false;
1768 } else {
1769 /* Step 2b(i). */
1770 RootedIdVector keys(cx);
1771 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) {
1772 return false;
1775 /* Step 2b(ii). */
1776 RootedId id(cx);
1777 RootedValue newElement(cx);
1778 for (size_t i = 0, len = keys.length(); i < len; i++) {
1779 if (!CheckForInterrupt(cx)) {
1780 return false;
1783 /* Step 2b(ii)(1). */
1784 id = keys[i];
1785 if (!Walk(cx, obj, id, reviver, &newElement)) {
1786 return false;
1789 ObjectOpResult ignored;
1790 if (newElement.isUndefined()) {
1791 /* Step 2b(ii)(2). The spec deliberately ignores strict failure. */
1792 if (!DeleteProperty(cx, obj, id, ignored)) {
1793 return false;
1795 } else {
1796 /* Step 2b(ii)(3). The spec deliberately ignores strict failure. */
1797 Rooted<PropertyDescriptor> desc(
1798 cx, PropertyDescriptor::Data(newElement,
1799 {JS::PropertyAttribute::Configurable,
1800 JS::PropertyAttribute::Enumerable,
1801 JS::PropertyAttribute::Writable}));
1802 if (!DefineProperty(cx, obj, id, desc, ignored)) {
1803 return false;
1810 /* Step 3. */
1811 RootedString key(cx, IdToString(cx, name));
1812 if (!key) {
1813 return false;
1816 RootedValue keyVal(cx, StringValue(key));
1817 return js::Call(cx, reviver, holder, keyVal, val, vp);
1820 static bool Revive(JSContext* cx, HandleValue reviver, MutableHandleValue vp) {
1821 Rooted<PlainObject*> obj(cx, NewPlainObject(cx));
1822 if (!obj) {
1823 return false;
1826 if (!DefineDataProperty(cx, obj, cx->names().empty_, vp)) {
1827 return false;
1830 Rooted<jsid> id(cx, NameToId(cx->names().empty_));
1831 return Walk(cx, obj, id, reviver, vp);
1834 template <typename CharT>
1835 bool ParseJSON(JSContext* cx, const mozilla::Range<const CharT> chars,
1836 MutableHandleValue vp) {
1837 Rooted<JSONParser<CharT>> parser(cx, cx, chars,
1838 JSONParser<CharT>::ParseType::JSONParse);
1839 return parser.parse(vp);
1842 template <typename CharT>
1843 bool js::ParseJSONWithReviver(JSContext* cx,
1844 const mozilla::Range<const CharT> chars,
1845 HandleValue reviver, MutableHandleValue vp) {
1846 /* 15.12.2 steps 2-3. */
1847 if (!ParseJSON(cx, chars, vp)) {
1848 return false;
1851 /* 15.12.2 steps 4-5. */
1852 if (IsCallable(reviver)) {
1853 return Revive(cx, reviver, vp);
1855 return true;
1858 template bool js::ParseJSONWithReviver(
1859 JSContext* cx, const mozilla::Range<const Latin1Char> chars,
1860 HandleValue reviver, MutableHandleValue vp);
1862 template bool js::ParseJSONWithReviver(
1863 JSContext* cx, const mozilla::Range<const char16_t> chars,
1864 HandleValue reviver, MutableHandleValue vp);
1866 static bool json_toSource(JSContext* cx, unsigned argc, Value* vp) {
1867 CallArgs args = CallArgsFromVp(argc, vp);
1868 args.rval().setString(cx->names().JSON);
1869 return true;
1872 /* ES5 15.12.2. */
1873 static bool json_parse(JSContext* cx, unsigned argc, Value* vp) {
1874 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parse");
1875 CallArgs args = CallArgsFromVp(argc, vp);
1877 /* Step 1. */
1878 JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0])
1879 : cx->names().undefined;
1880 if (!str) {
1881 return false;
1884 JSLinearString* linear = str->ensureLinear(cx);
1885 if (!linear) {
1886 return false;
1889 AutoStableStringChars linearChars(cx);
1890 if (!linearChars.init(cx, linear)) {
1891 return false;
1894 HandleValue reviver = args.get(1);
1896 /* Steps 2-5. */
1897 return linearChars.isLatin1()
1898 ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver,
1899 args.rval())
1900 : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver,
1901 args.rval());
1904 #ifdef ENABLE_RECORD_TUPLE
1905 bool BuildImmutableProperty(JSContext* cx, HandleValue value, HandleId name,
1906 HandleValue reviver,
1907 MutableHandleValue immutableRes) {
1908 MOZ_ASSERT(!name.isSymbol());
1910 // Step 1
1911 if (value.isObject()) {
1912 RootedValue childValue(cx), newElement(cx);
1913 RootedId childName(cx);
1915 // Step 1.a-1.b
1916 if (value.toObject().is<ArrayObject>()) {
1917 Rooted<ArrayObject*> arr(cx, &value.toObject().as<ArrayObject>());
1919 // Step 1.b.iii
1920 uint32_t len = arr->length();
1922 TupleType* tup = TupleType::createUninitialized(cx, len);
1923 if (!tup) {
1924 return false;
1926 immutableRes.setExtendedPrimitive(*tup);
1928 // Step 1.b.iv
1929 for (uint32_t i = 0; i < len; i++) {
1930 // Step 1.b.iv.1
1931 childName.set(PropertyKey::Int(i));
1933 // Step 1.b.iv.2
1934 if (!GetProperty(cx, arr, value, childName, &childValue)) {
1935 return false;
1938 // Step 1.b.iv.3
1939 if (!BuildImmutableProperty(cx, childValue, childName, reviver,
1940 &newElement)) {
1941 return false;
1943 MOZ_ASSERT(newElement.isPrimitive());
1945 // Step 1.b.iv.5
1946 if (!tup->initializeNextElement(cx, newElement)) {
1947 return false;
1951 // Step 1.b.v
1952 tup->finishInitialization(cx);
1953 } else {
1954 RootedObject obj(cx, &value.toObject());
1956 // Step 1.c.i - We only get the property keys rather than the
1957 // entries, but the difference is not observable from user code
1958 // because `obj` is a plan object not exposed externally
1959 RootedIdVector props(cx);
1960 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &props)) {
1961 return false;
1964 RecordType* rec = RecordType::createUninitialized(cx, props.length());
1965 if (!rec) {
1966 return false;
1968 immutableRes.setExtendedPrimitive(*rec);
1970 for (uint32_t i = 0; i < props.length(); i++) {
1971 // Step 1.c.iii.1
1972 childName.set(props[i]);
1974 // Step 1.c.iii.2
1975 if (!GetProperty(cx, obj, value, childName, &childValue)) {
1976 return false;
1979 // Step 1.c.iii.3
1980 if (!BuildImmutableProperty(cx, childValue, childName, reviver,
1981 &newElement)) {
1982 return false;
1984 MOZ_ASSERT(newElement.isPrimitive());
1986 // Step 1.c.iii.5
1987 if (!newElement.isUndefined()) {
1988 // Step 1.c.iii.5.a-b
1989 rec->initializeNextProperty(cx, childName, newElement);
1993 // Step 1.c.iv
1994 rec->finishInitialization(cx);
1996 } else {
1997 // Step 2.a
1998 immutableRes.set(value);
2001 // Step 3
2002 if (IsCallable(reviver)) {
2003 RootedValue keyVal(cx, StringValue(IdToString(cx, name)));
2005 // Step 3.a
2006 if (!Call(cx, reviver, UndefinedHandleValue, keyVal, immutableRes,
2007 immutableRes)) {
2008 return false;
2011 // Step 3.b
2012 if (!immutableRes.isPrimitive()) {
2013 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
2014 JSMSG_RECORD_TUPLE_NO_OBJECT);
2015 return false;
2019 return true;
2022 static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) {
2023 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parseImmutable");
2024 CallArgs args = CallArgsFromVp(argc, vp);
2026 /* Step 1. */
2027 JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0])
2028 : cx->names().undefined;
2029 if (!str) {
2030 return false;
2033 JSLinearString* linear = str->ensureLinear(cx);
2034 if (!linear) {
2035 return false;
2038 AutoStableStringChars linearChars(cx);
2039 if (!linearChars.init(cx, linear)) {
2040 return false;
2043 HandleValue reviver = args.get(1);
2044 RootedValue unfiltered(cx);
2046 if (linearChars.isLatin1()) {
2047 if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered)) {
2048 return false;
2050 } else {
2051 if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered)) {
2052 return false;
2056 RootedId id(cx, NameToId(cx->names().empty_));
2057 return BuildImmutableProperty(cx, unfiltered, id, reviver, args.rval());
2059 #endif
2061 /* ES6 24.3.2. */
2062 bool json_stringify(JSContext* cx, unsigned argc, Value* vp) {
2063 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "stringify");
2064 CallArgs args = CallArgsFromVp(argc, vp);
2066 RootedObject replacer(cx,
2067 args.get(1).isObject() ? &args[1].toObject() : nullptr);
2068 RootedValue value(cx, args.get(0));
2069 RootedValue space(cx, args.get(2));
2071 #ifdef DEBUG
2072 StringifyBehavior behavior = StringifyBehavior::Compare;
2073 #else
2074 StringifyBehavior behavior = StringifyBehavior::Normal;
2075 #endif
2077 JSStringBuilder sb(cx);
2078 if (!Stringify(cx, &value, replacer, space, sb, behavior)) {
2079 return false;
2082 // XXX This can never happen to nsJSON.cpp, but the JSON object
2083 // needs to support returning undefined. So this is a little awkward
2084 // for the API, because we want to support streaming writers.
2085 if (!sb.empty()) {
2086 JSString* str = sb.finishString();
2087 if (!str) {
2088 return false;
2090 args.rval().setString(str);
2091 } else {
2092 args.rval().setUndefined();
2095 return true;
2098 static const JSFunctionSpec json_static_methods[] = {
2099 JS_FN("toSource", json_toSource, 0, 0), JS_FN("parse", json_parse, 2, 0),
2100 JS_FN("stringify", json_stringify, 3, 0),
2101 #ifdef ENABLE_RECORD_TUPLE
2102 JS_FN("parseImmutable", json_parseImmutable, 2, 0),
2103 #endif
2104 JS_FS_END};
2106 static const JSPropertySpec json_static_properties[] = {
2107 JS_STRING_SYM_PS(toStringTag, "JSON", JSPROP_READONLY), JS_PS_END};
2109 static JSObject* CreateJSONObject(JSContext* cx, JSProtoKey key) {
2110 RootedObject proto(cx, &cx->global()->getObjectPrototype());
2111 return NewTenuredObjectWithGivenProto(cx, &JSONClass, proto);
2114 static const ClassSpec JSONClassSpec = {
2115 CreateJSONObject, nullptr, json_static_methods, json_static_properties};
2117 const JSClass js::JSONClass = {"JSON", JSCLASS_HAS_CACHED_PROTO(JSProto_JSON),
2118 JS_NULL_CLASS_OPS, &JSONClassSpec};