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"
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"
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"
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"
54 using mozilla::AsVariant
;
55 using mozilla::CheckedInt
;
57 using mozilla::RangedPtr
;
58 using mozilla::Variant
;
60 using JS::AutoStableStringChars
;
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] = {
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
91 auto ToLowerHex
= [](uint8_t u
) {
93 return "0123456789abcdef"[u
];
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.
110 // Escape the rest, elaborating Unicode escapes when needed.
113 if (escaped
== 'u') {
121 *dstPtr
++ = ToLowerHex(c
& 0xF);
127 // Non-ASCII non-surrogates are directly copied.
128 if (!unicode::IsSurrogate(c
)) {
133 // So too for complete surrogate pairs.
134 if (MOZ_LIKELY(unicode::IsLeadSurrogate(c
) && srcBegin
< srcEnd
&&
135 unicode::IsTrailSurrogate(*srcBegin
))) {
137 *dstPtr
++ = *srcBegin
++;
141 // But lone surrogates are Unicode-escaped.
142 char32_t as32
= char32_t(c
);
145 *dstPtr
++ = ToLowerHex(as32
>> 12);
146 *dstPtr
++ = ToLowerHex((as32
>> 8) & 0xF);
147 *dstPtr
++ = ToLowerHex((as32
>> 4) & 0xF);
148 *dstPtr
++ = ToLowerHex(as32
& 0xF);
156 template <typename SrcCharT
, typename DstCharT
>
157 static size_t QuoteHelper(const JSLinearString
& linear
, StringBuffer
& sb
,
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
>(),
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
);
177 if (linear
->hasTwoByteChars() && !sb
.ensureTwoByteChars()) {
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
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
);
194 if (!sb
.growByUninitialized(reservedLen
.value())) {
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
);
205 newSize
= QuoteHelper
<Latin1Char
, char16_t
>(*linear
, sb
, sbInitialLen
);
208 sb
.shrinkTo(newSize
);
215 using ObjectVector
= GCVector
<JSObject
*, 8>;
217 class StringifyContext
{
219 StringifyContext(JSContext
* cx
, StringBuffer
& sb
, const StringBuffer
& gap
,
220 HandleObject replacer
, const RootedIdVector
& propertyList
,
224 replacer(cx
, replacer
),
225 stack(cx
, ObjectVector(cx
)),
226 propertyList(propertyList
),
228 maybeSafely(maybeSafely
) {
229 MOZ_ASSERT_IF(maybeSafely
, !replacer
);
230 MOZ_ASSERT_IF(maybeSafely
, gap
.empty());
234 const StringBuffer
& gap
;
235 RootedObject replacer
;
236 Rooted
<ObjectVector
> stack
;
237 const RootedIdVector
& propertyList
;
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')) {
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())) {
260 for (uint32_t i
= 0; i
< limit
; i
++) {
261 if (!scx
->sb
.append(scx
->gap
.rawTwoByteBegin(),
262 scx
->gap
.rawTwoByteEnd())) {
274 template <typename KeyType
>
275 class KeyStringifier
{};
278 class KeyStringifier
<uint32_t> {
280 static JSString
* toString(JSContext
* cx
, uint32_t index
) {
281 return IndexToString(cx
, index
);
286 class KeyStringifier
<HandleId
> {
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
) {
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
));
320 if (!GetProperty(cx
, obj
, vp
, cx
->names().toJSON
, &toJSON
)) {
324 if (IsCallable(toJSON
)) {
325 keyStr
= KeyStringifier
<KeyType
>::toString(cx
, key
);
330 RootedValue
arg0(cx
, StringValue(keyStr
));
331 if (!js::Call(cx
, toJSON
, vp
, arg0
, vp
)) {
338 if (scx
->replacer
&& scx
->replacer
->isCallable()) {
339 MOZ_ASSERT(holder
!= nullptr,
340 "holder object must be present when replacer is callable");
343 keyStr
= KeyStringifier
<KeyType
>::toString(cx
, key
);
349 RootedValue
arg0(cx
, StringValue(keyStr
));
350 RootedValue
replacerVal(cx
, ObjectValue(*scx
->replacer
));
351 if (!js::Call(cx
, replacerVal
, holder
, arg0
, vp
, vp
)) {
357 if (vp
.get().isObject()) {
358 RootedObject
obj(cx
, &vp
.get().toObject());
361 if (!JS::GetBuiltinClass(cx
, obj
, &cls
)) {
365 if (cls
== ESClass::Number
) {
367 if (!ToNumber(cx
, vp
, &d
)) {
371 } else if (cls
== ESClass::String
) {
372 JSString
* str
= ToStringSlow
<CanGC
>(cx
, vp
);
377 } else if (cls
== ESClass::Boolean
|| cls
== ESClass::BigInt
||
379 obj
->is
<RecordObject
>() || obj
->is
<TupleObject
>(), false)) {
380 if (!Unbox(cx
, obj
, vp
)) {
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
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
{
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
);
415 appended_
= stack_
.append(obj
);
420 if (MOZ_LIKELY(appended_
)) {
421 MOZ_ASSERT(stack_
.back() == obj_
);
427 MutableHandle
<ObjectVector
> stack_
;
432 #ifdef ENABLE_RECORD_TUPLE
433 enum class JOType
{ Record
, Object
};
434 template <JOType type
= JOType::Object
>
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
451 if constexpr (type
== JOType::Record
) {
452 MOZ_ASSERT(obj
->is
<RecordType
>());
453 rec
= &obj
->as
<RecordType
>();
455 MOZ_ASSERT(!IsExtendedPrimitive(*obj
));
458 MOZ_ASSERT_IF(scx
->maybeSafely
, obj
->is
<PlainObject
>());
461 CycleDetector
detect(scx
, obj
);
462 if (!detect
.foundCycle(cx
)) {
466 if (!scx
->sb
.append('{')) {
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
479 props
= &scx
->propertyList
;
481 MOZ_ASSERT_IF(scx
->replacer
, scx
->propertyList
.length() == 0);
483 if (!GetPropertyKeys(cx
, obj
, JSITER_OWNONLY
, 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;
495 for (size_t i
= 0, len
= propertyList
.length(); i
< len
; i
++) {
496 if (!CheckForInterrupt(cx
)) {
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
);
510 if (scx
->maybeSafely
) {
512 if (!NativeLookupOwnPropertyNoResolve(cx
, &obj
->as
<NativeObject
>(), id
,
516 MOZ_ASSERT(prop
.isNativeProperty() &&
517 prop
.propertyInfo().isDataDescriptor());
521 #ifdef ENABLE_RECORD_TUPLE
522 if constexpr (type
== JOType::Record
) {
523 MOZ_ALWAYS_TRUE(rec
->getOwnProperty(cx
, id
, &outputValue
));
527 RootedValue
objValue(cx
, ObjectValue(*obj
));
528 if (!GetProperty(cx
, obj
, objValue
, id
, &outputValue
)) {
532 if (!PreprocessValue(cx
, obj
, HandleId(id
), &outputValue
, scx
)) {
535 if (IsFilteredValue(outputValue
)) {
539 /* Output a comma unless this is the first member to write. */
540 if (wroteMember
&& !scx
->sb
.append(',')) {
545 if (!WriteIndent(scx
, scx
->depth
)) {
549 JSString
* s
= IdToString(cx
, id
);
554 if (!Quote(cx
, scx
->sb
, s
) || !scx
->sb
.append(':') ||
555 !(scx
->gap
.empty() || scx
->sb
.append(' ')) ||
556 !Str(cx
, outputValue
, scx
)) {
561 if (wroteMember
&& !WriteIndent(scx
, scx
->depth
- 1)) {
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
,
574 if (MOZ_LIKELY(obj
->is
<ArrayObject
>())) {
575 *lengthp
= obj
->as
<ArrayObject
>().length();
578 #ifdef ENABLE_RECORD_TUPLE
579 if (obj
->is
<TupleType
>()) {
580 *lengthp
= obj
->as
<TupleType
>().length();
585 MOZ_ASSERT(obj
->is
<ProxyObject
>());
588 if (!GetLengthProperty(cx
, obj
, &len
)) {
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
);
602 *lengthp
= uint32_t(len
);
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).
619 CycleDetector
detect(scx
, obj
);
620 if (!detect
.foundCycle(cx
)) {
624 if (!scx
->sb
.append('[')) {
630 if (!GetLengthPropertyForArrayLike(cx
, obj
, &length
)) {
636 /* Steps 4, 10b(i). */
637 if (!WriteIndent(scx
, scx
->depth
)) {
642 RootedValue
outputValue(cx
);
643 for (uint32_t i
= 0; i
< length
; i
++) {
644 if (!CheckForInterrupt(cx
)) {
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.
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
) {
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");
672 MOZ_ASSERT(nativeObj
->isIndexed());
676 if (!GetElement(cx
, obj
, i
, &outputValue
)) {
679 if (!PreprocessValue(cx
, obj
, i
, &outputValue
, scx
)) {
682 if (IsFilteredValue(outputValue
)) {
683 if (!scx
->sb
.append("null")) {
687 if (!Str(cx
, outputValue
, scx
)) {
692 /* Steps 3, 4, 10b(i). */
693 if (i
< length
- 1) {
694 if (!scx
->sb
.append(',')) {
697 if (!WriteIndent(scx
, scx
->depth
)) {
703 /* Step 10(b)(iii). */
704 if (!WriteIndent(scx
, scx
->depth
- 1)) {
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
726 * * We move step 11 into callers, again to ease streaming.
731 return Quote(cx
, scx
->sb
, v
.toString());
736 return scx
->sb
.append("null");
741 return v
.toBoolean() ? scx
->sb
.append("true") : scx
->sb
.append("false");
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. */
760 JS_ReportErrorNumberASCII(cx
, GetErrorMessage
, nullptr,
761 JSMSG_BIGINT_NOT_SERIALIZABLE
);
765 AutoCheckRecursionLimit
recursion(cx
);
766 if (!recursion
.check(cx
)) {
771 MOZ_ASSERT(v
.hasObjectPayload());
772 RootedObject
obj(cx
, &v
.getObjectPayload());
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");
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.");
795 if (!IsArray(cx
, obj
, &isArray
)) {
799 return isArray
? JA(cx
, obj
, scx
) : JO(cx
, obj
, scx
);
802 static bool CanFastStringifyObject(NativeObject
* obj
) {
803 if (ClassCanHaveExtraEnumeratedProperties(obj
->getClass())) {
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
)) {
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
817 if (ObjectMayHaveExtraIndexedOwnProperties(obj
)) {
822 // Only used for internal environment objects that should never be passed to
824 MOZ_ASSERT(!obj
->getOpsLookupProperty());
826 #ifdef ENABLE_RECORD_TUPLE
827 if (ObjectValue(*obj
).isExtendedPrimitive()) {
835 #define FOR_EACH_STRINGIFY_BAIL_REASON(MACRO) \
837 MACRO(INELIGIBLE_OBJECT) \
838 MACRO(DEEP_RECURSION) \
839 MACRO(NON_DATA_PROPERTY) \
840 MACRO(TOO_MANY_PROPERTIES) \
843 MACRO(HAVE_REPLACER) \
847 MACRO(IMPURE_LOOKUP) \
850 enum class BailReason
: uint8_t {
851 #define DECLARE_ENUM(name) name,
852 FOR_EACH_STRINGIFY_BAIL_REASON(DECLARE_ENUM
)
856 static const char* DescribeStringifyBailReason(BailReason whySlow
) {
858 #define ENUM_NAME(name) \
859 case BailReason::name: \
861 FOR_EACH_STRINGIFY_BAIL_REASON(ENUM_NAME
)
868 // Iterator over all the dense elements of an object. Used
869 // for both Arrays and non-Arrays.
870 class DenseElementsIteratorForJSON
{
871 HeapSlotArray elements
;
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
;
880 explicit DenseElementsIteratorForJSON(NativeObject
* nobj
)
881 : elements(nobj
->getDenseElements()),
883 numElements(nobj
->getDenseInitializedLength()) {
884 length
= nobj
->is
<ArrayObject
>() ? nobj
->as
<ArrayObject
>().length()
888 bool done() const { return element
== length
; }
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
900 // Consider specializing the iterator for Arrays vs non-Arrays to avoid this
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
913 class ShapePropertyForwardIterNoGC
{
914 // Pointer to the current PropMap with length and an index within it.
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() {
927 if (MOZ_UNLIKELY(i_
== mapLength_
)) {
929 if (stack_
.empty()) {
930 mapLength_
= 0; // Done
933 map_
= stack_
.back();
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.
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();
957 while (map_
->hasPrevious()) {
958 if (!stack_
.append(map_
)) {
960 i_
= mapLength_
= UINT32_MAX
;
963 map_
= map_
->asLinked()->previous();
966 // Set mapLength_ to the number of properties in map_ (including dictionary
968 mapLength_
= stack_
.empty() ? shape_
->propMapLength() : PropMap::Capacity
;
973 bool done() const { return i_
== mapLength_
; }
974 bool isOverflowed() const { return i_
== UINT32_MAX
; }
976 void operator++(int) {
982 PropertyInfoWithKey
get() const {
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.
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
;
1005 BailReason fastFailed_
= BailReason::NO_REASON
;
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()) {
1014 if (!shapeIter
->isDataProperty()) {
1015 fastFailed_
= BailReason::NON_DATA_PROPERTY
;
1019 PropertyKey id
= shapeIter
->key();
1020 if (!id
.isSymbol()) {
1028 explicit OwnNonIndexKeysIterForJSON(const NativeObject
* nobj
)
1029 : shapeIter(nobj
->shape()) {
1030 if (MOZ_UNLIKELY(shapeIter
.isOverflowed())) {
1031 fastFailed_
= BailReason::TOO_MANY_PROPERTIES
;
1035 if (!nobj
->hasEnumerableProperty()) {
1036 // Non-Arrays with no enumerable properties can just be skipped.
1037 MOZ_ASSERT(!nobj
->is
<ArrayObject
>());
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();
1056 // Steps from https://262.ecma-international.org/13.0/#sec-serializejsonproperty
1057 static bool EmitSimpleValue(JSContext
* cx
, StringBuffer
& sb
, const Value
& v
) {
1060 return Quote(cx
, sb
, v
.toString());
1065 return sb
.append("null");
1069 if (v
.isBoolean()) {
1070 return v
.toBoolean() ? sb
.append("true") : sb
.append("false");
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");
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
;
1099 const char* cstr
= ::Int32ToCString(&cbuf
, index
, &cstrlen
);
1100 if (!sb
.reserve(sb
.length() + 1 + cstrlen
+ 1 + 1)) {
1103 sb
.infallibleAppend('"');
1104 sb
.infallibleAppend(cstr
, cstrlen
);
1105 sb
.infallibleAppend('"');
1106 sb
.infallibleAppend(':');
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
);
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
;
1126 if (!vp
->isObject()) {
1130 if (!vp
->toObject().is
<NativeObject
>()) {
1131 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
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
;
1146 if (toJSON
.isFound()) {
1147 *whySlow
= BailReason::HAVE_TOJSON
;
1151 // Step 4: convert primitive wrapper objects to primitives. Disallowed for
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
>(),
1157 // Primitive wrapper objects can invoke arbitrary code when being coerced to
1158 // their primitive values (eg via @@toStringTag).
1159 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
1163 if (obj
->isCallable()) {
1164 // Steps 11,12: Callable objects are treated as undefined.
1169 if (!CanFastStringifyObject(obj
)) {
1170 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
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
{
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
)
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
{
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
1233 * wroteMember = false
1234 * OUTER: while true:
1237 * while !top.done():
1238 * key, value = top.next()
1239 * if top is a non-Array and value is skippable:
1243 * wroteMember = true
1244 * if value is object:
1245 * emit(key + ":") if top is iterating a non-Array
1248 * wroteMember = false
1251 * emit(value) or emit(key + ":" + value)
1253 * if stack is empty: done!
1254 * top <- stack.pop()
1255 * wroteMember = true
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
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
)) {
1275 constexpr size_t MAX_STACK_DEPTH
= 20;
1276 Vector
<FastStackEntry
> stack(cx
);
1277 if (!stack
.reserve(MAX_STACK_DEPTH
- 1)) {
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
;
1294 if (!scx
->sb
.append(top
.isArray
? '[' : '{')) {
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
;
1310 uint32_t index
= iter
.getIndex();
1311 Value val
= iter
.next();
1313 if (!PreprocessFastValue(cx
, &val
, scx
, whySlow
)) {
1316 if (*whySlow
!= BailReason::NO_REASON
) {
1319 if (IsFilteredValue(val
)) {
1321 // Arrays convert unrepresentable values to "null".
1322 val
= UndefinedValue();
1324 // Objects skip unrepresentable values.
1329 if (wroteMember
&& !scx
->sb
.append(',')) {
1335 if (!EmitQuotedIndexColon(scx
->sb
, index
)) {
1340 if (val
.isObject()) {
1341 if (stack
.length() >= MAX_STACK_DEPTH
- 1) {
1342 *whySlow
= BailReason::DEEP_RECURSION
;
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.
1353 if (!EmitSimpleValue(cx
, scx
->sb
, val
)) {
1359 continue; // Break out to outer loop.
1362 MOZ_ASSERT(iter
.done());
1364 MOZ_ASSERT(!top
.nobj
->isIndexed() || IsPackedArray(top
.nobj
));
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
;
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
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
)) {
1394 if (*whySlow
!= BailReason::NO_REASON
) {
1397 if (IsFilteredValue(val
)) {
1398 // Undefined check in
1399 // https://262.ecma-international.org/13.0/#sec-serializejsonobject
1400 // step 8b, covering undefined, symbol
1404 if (wroteMember
&& !scx
->sb
.append(",")) {
1409 MOZ_ASSERT(prop
.key().isString());
1410 if (!Quote(cx
, scx
->sb
, prop
.key().toString())) {
1414 if (!scx
->sb
.append(':')) {
1417 if (val
.isObject()) {
1418 if (stack
.length() >= MAX_STACK_DEPTH
- 1) {
1419 *whySlow
= BailReason::DEEP_RECURSION
;
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.
1430 if (!EmitSimpleValue(cx
, scx
->sb
, val
)) {
1434 *whySlow
= iter
.cannotFastStringify();
1435 if (*whySlow
!= BailReason::NO_REASON
) {
1439 continue; // Break out to outer loop.
1441 MOZ_ASSERT(iter
.done());
1444 if (!scx
->sb
.append(top
.isArray
? ']' : '}')) {
1447 if (stack
.empty()) {
1448 return true; // Success!
1450 top
= std::move(stack
.back());
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
,
1466 MOZ_ASSERT_IF(stringifyBehavior
== StringifyBehavior::RestrictedSafe
,
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");
1478 RootedIdVector
propertyList(cx
);
1479 BailReason whySlow
= BailReason::NO_REASON
;
1480 if (stringifyBehavior
== StringifyBehavior::SlowOnly
||
1481 stringifyBehavior
== StringifyBehavior::RestrictedSafe
) {
1482 whySlow
= BailReason::API
;
1485 whySlow
= BailReason::HAVE_REPLACER
;
1487 if (replacer
->isCallable()) {
1488 /* Step 4a(i): use replacer to transform values. */
1489 } else if (!IsArray(cx
, replacer
, &isArray
)) {
1491 } else if (isArray
) {
1494 /* Step 4b(iii)(2-3). */
1496 if (!GetLengthPropertyForArrayLike(cx
, replacer
, &len
)) {
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). */
1511 /* Step 4b(iii)(5). */
1512 RootedValue
item(cx
);
1513 for (; k
< len
; k
++) {
1514 if (!CheckForInterrupt(cx
)) {
1518 /* Step 4b(iii)(5)(a-b). */
1519 if (!GetElement(cx
, replacer
, k
, &item
)) {
1523 /* Step 4b(iii)(5)(c-g). */
1525 if (item
.isNumber() || item
.isString()) {
1526 if (!PrimitiveValueToId
<CanGC
>(cx
, item
, &id
)) {
1531 if (!GetClassOfValue(cx
, item
, &cls
)) {
1535 if (cls
!= ESClass::String
&& cls
!= ESClass::Number
) {
1539 JSAtom
* atom
= ToAtom
<CanGC
>(cx
, item
);
1544 id
.set(AtomToId(atom
));
1547 /* Step 4b(iii)(5)(g). */
1548 auto p
= idSet
.lookupForAdd(id
);
1550 /* Step 4b(iii)(5)(g)(i). */
1551 if (!idSet
.add(p
, id
) || !propertyList
.append(id
)) {
1562 if (space
.isObject()) {
1563 RootedObject
spaceObj(cx
, &space
.toObject());
1566 if (!JS::GetBuiltinClass(cx
, spaceObj
, &cls
)) {
1570 if (cls
== ESClass::Number
) {
1572 if (!ToNumber(cx
, space
, &d
)) {
1575 space
= NumberValue(d
);
1576 } else if (cls
== ESClass::String
) {
1577 JSString
* str
= ToStringSlow
<CanGC
>(cx
, space
);
1581 space
= StringValue(str
);
1585 StringBuffer
gap(cx
);
1587 if (space
.isNumber()) {
1590 MOZ_ALWAYS_TRUE(ToInteger(cx
, space
, &d
));
1591 d
= std::min(10.0, d
);
1592 if (d
>= 1 && !gap
.appendN(' ', uint32_t(d
))) {
1595 } else if (space
.isString()) {
1597 JSLinearString
* str
= space
.toString()->ensureLinear(cx
);
1601 size_t len
= std::min(size_t(10), str
->length());
1602 if (!gap
.appendSubstring(str
, 0, len
)) {
1607 MOZ_ASSERT(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.
1620 wrapper
= NewPlainObject(cx
);
1626 if (!NativeDefineDataProperty(cx
, wrapper
, emptyId
, vp
, JSPROP_ENUMERATE
)) {
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
)) {
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
)) {
1649 if (whySlow
== BailReason::NO_REASON
) {
1650 // Fast stringify succeeded!
1651 if (stringifyBehavior
!= StringifyBehavior::Compare
) {
1654 fastJSON
= scx
.sb
.finishAtom();
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
));
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
)) {
1677 if (IsFilteredValue(vp
)) {
1681 if (!Str(cx
, vp
, &scx
)) {
1685 // For StringBehavior::Compare, when the fast path succeeded.
1686 if (MOZ_UNLIKELY(fastJSON
)) {
1687 JSAtom
* slowJSON
= scx
.sb
.finishAtom();
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
)) {
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
)) {
1712 RootedValue
val(cx
);
1713 if (!GetProperty(cx
, holder
, holder
, name
, &val
)) {
1718 if (val
.isObject()) {
1719 RootedObject
obj(cx
, &val
.toObject());
1722 if (!IsArray(cx
, obj
, &isArray
)) {
1729 if (!GetLengthPropertyForArrayLike(cx
, obj
, &length
)) {
1733 /* Step 2a(i), 2a(iii-iv). */
1735 RootedValue
newElement(cx
);
1736 for (uint32_t i
= 0; i
< length
; i
++) {
1737 if (!CheckForInterrupt(cx
)) {
1741 if (!IndexToId(cx
, i
, &id
)) {
1745 /* Step 2a(iii)(1). */
1746 if (!Walk(cx
, obj
, id
, reviver
, &newElement
)) {
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
)) {
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
)) {
1770 RootedIdVector
keys(cx
);
1771 if (!GetPropertyKeys(cx
, obj
, JSITER_OWNONLY
, &keys
)) {
1777 RootedValue
newElement(cx
);
1778 for (size_t i
= 0, len
= keys
.length(); i
< len
; i
++) {
1779 if (!CheckForInterrupt(cx
)) {
1783 /* Step 2b(ii)(1). */
1785 if (!Walk(cx
, obj
, id
, reviver
, &newElement
)) {
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
)) {
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
)) {
1811 RootedString
key(cx
, IdToString(cx
, name
));
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
));
1826 if (!DefineDataProperty(cx
, obj
, cx
->names().empty_
, vp
)) {
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
)) {
1851 /* 15.12.2 steps 4-5. */
1852 if (IsCallable(reviver
)) {
1853 return Revive(cx
, reviver
, vp
);
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
);
1873 static bool json_parse(JSContext
* cx
, unsigned argc
, Value
* vp
) {
1874 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "parse");
1875 CallArgs args
= CallArgsFromVp(argc
, vp
);
1878 JSString
* str
= (args
.length() >= 1) ? ToString
<CanGC
>(cx
, args
[0])
1879 : cx
->names().undefined
;
1884 JSLinearString
* linear
= str
->ensureLinear(cx
);
1889 AutoStableStringChars
linearChars(cx
);
1890 if (!linearChars
.init(cx
, linear
)) {
1894 HandleValue reviver
= args
.get(1);
1897 return linearChars
.isLatin1()
1898 ? ParseJSONWithReviver(cx
, linearChars
.latin1Range(), reviver
,
1900 : ParseJSONWithReviver(cx
, linearChars
.twoByteRange(), reviver
,
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());
1911 if (value
.isObject()) {
1912 RootedValue
childValue(cx
), newElement(cx
);
1913 RootedId
childName(cx
);
1916 if (value
.toObject().is
<ArrayObject
>()) {
1917 Rooted
<ArrayObject
*> arr(cx
, &value
.toObject().as
<ArrayObject
>());
1920 uint32_t len
= arr
->length();
1922 TupleType
* tup
= TupleType::createUninitialized(cx
, len
);
1926 immutableRes
.setExtendedPrimitive(*tup
);
1929 for (uint32_t i
= 0; i
< len
; i
++) {
1931 childName
.set(PropertyKey::Int(i
));
1934 if (!GetProperty(cx
, arr
, value
, childName
, &childValue
)) {
1939 if (!BuildImmutableProperty(cx
, childValue
, childName
, reviver
,
1943 MOZ_ASSERT(newElement
.isPrimitive());
1946 if (!tup
->initializeNextElement(cx
, newElement
)) {
1952 tup
->finishInitialization(cx
);
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
)) {
1964 RecordType
* rec
= RecordType::createUninitialized(cx
, props
.length());
1968 immutableRes
.setExtendedPrimitive(*rec
);
1970 for (uint32_t i
= 0; i
< props
.length(); i
++) {
1972 childName
.set(props
[i
]);
1975 if (!GetProperty(cx
, obj
, value
, childName
, &childValue
)) {
1980 if (!BuildImmutableProperty(cx
, childValue
, childName
, reviver
,
1984 MOZ_ASSERT(newElement
.isPrimitive());
1987 if (!newElement
.isUndefined()) {
1988 // Step 1.c.iii.5.a-b
1989 rec
->initializeNextProperty(cx
, childName
, newElement
);
1994 rec
->finishInitialization(cx
);
1998 immutableRes
.set(value
);
2002 if (IsCallable(reviver
)) {
2003 RootedValue
keyVal(cx
, StringValue(IdToString(cx
, name
)));
2006 if (!Call(cx
, reviver
, UndefinedHandleValue
, keyVal
, immutableRes
,
2012 if (!immutableRes
.isPrimitive()) {
2013 JS_ReportErrorNumberASCII(cx
, GetErrorMessage
, nullptr,
2014 JSMSG_RECORD_TUPLE_NO_OBJECT
);
2022 static bool json_parseImmutable(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2023 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "parseImmutable");
2024 CallArgs args
= CallArgsFromVp(argc
, vp
);
2027 JSString
* str
= (args
.length() >= 1) ? ToString
<CanGC
>(cx
, args
[0])
2028 : cx
->names().undefined
;
2033 JSLinearString
* linear
= str
->ensureLinear(cx
);
2038 AutoStableStringChars
linearChars(cx
);
2039 if (!linearChars
.init(cx
, linear
)) {
2043 HandleValue reviver
= args
.get(1);
2044 RootedValue
unfiltered(cx
);
2046 if (linearChars
.isLatin1()) {
2047 if (!ParseJSON(cx
, linearChars
.latin1Range(), &unfiltered
)) {
2051 if (!ParseJSON(cx
, linearChars
.twoByteRange(), &unfiltered
)) {
2056 RootedId
id(cx
, NameToId(cx
->names().empty_
));
2057 return BuildImmutableProperty(cx
, unfiltered
, id
, reviver
, args
.rval());
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));
2072 StringifyBehavior behavior
= StringifyBehavior::Compare
;
2074 StringifyBehavior behavior
= StringifyBehavior::Normal
;
2077 JSStringBuilder
sb(cx
);
2078 if (!Stringify(cx
, &value
, replacer
, space
, sb
, behavior
)) {
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.
2086 JSString
* str
= sb
.finishString();
2090 args
.rval().setString(str
);
2092 args
.rval().setUndefined();
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),
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
};