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 "builtin/ParseRecordObject.h"
23 #include "builtin/RawJSONObject.h"
24 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
25 #include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit
26 #include "js/Object.h" // JS::GetBuiltinClass
27 #include "js/Prefs.h" // JS::Prefs
28 #include "js/PropertySpec.h"
29 #include "js/StableStringChars.h"
30 #include "js/TypeDecls.h"
32 #include "util/StringBuffer.h"
33 #include "vm/BooleanObject.h" // js::BooleanObject
34 #include "vm/EqualityOperations.h" // js::SameValue
35 #include "vm/Interpreter.h"
36 #include "vm/Iteration.h"
37 #include "vm/JSAtomUtils.h" // ToAtom
38 #include "vm/JSContext.h"
39 #include "vm/JSObject.h"
40 #include "vm/JSONParser.h"
41 #include "vm/NativeObject.h"
42 #include "vm/NumberObject.h" // js::NumberObject
43 #include "vm/PlainObject.h" // js::PlainObject
44 #include "vm/StringObject.h" // js::StringObject
45 #ifdef ENABLE_RECORD_TUPLE
46 # include "builtin/RecordObject.h"
47 # include "builtin/TupleObject.h"
48 # include "vm/RecordType.h"
51 #include "builtin/Array-inl.h"
52 #include "vm/GeckoProfiler-inl.h"
53 #include "vm/JSAtomUtils-inl.h" // AtomToId, PrimitiveValueToId, IndexToId, IdToString,
54 #include "vm/NativeObject-inl.h"
58 using mozilla::AsVariant
;
59 using mozilla::CheckedInt
;
61 using mozilla::RangedPtr
;
62 using mozilla::Variant
;
64 using JS::AutoStableStringChars
;
66 /* https://262.ecma-international.org/14.0/#sec-quotejsonstring
67 * Requires that the destination has enough space allocated for src after
68 * escaping (that is, `2 + 6 * (srcEnd - srcBegin)` characters).
70 template <typename SrcCharT
, typename DstCharT
>
71 static MOZ_ALWAYS_INLINE RangedPtr
<DstCharT
> InfallibleQuoteJSONString(
72 RangedPtr
<const SrcCharT
> srcBegin
, RangedPtr
<const SrcCharT
> srcEnd
,
73 RangedPtr
<DstCharT
> dstPtr
) {
74 // Maps characters < 256 to the value that must follow the '\\' in the quoted
75 // string. Entries with 'u' are handled as \\u00xy, and entries with 0 are not
76 // escaped in any way. Characters >= 256 are all assumed to be unescaped.
77 static const Latin1Char escapeLookup
[256] = {
79 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'b', 't',
80 'n', 'u', 'f', 'r', 'u', 'u', 'u', 'u', 'u', 'u',
81 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u',
82 'u', 'u', 0, 0, '\"', 0, 0, 0, 0, 0,
83 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
84 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
85 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
86 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
87 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
88 0, 0, '\\', // rest are all zeros
95 auto ToLowerHex
= [](uint8_t u
) {
97 return "0123456789abcdef"[u
];
101 while (srcBegin
!= srcEnd
) {
102 const SrcCharT c
= *srcBegin
++;
104 // Handle the Latin-1 cases.
105 if (MOZ_LIKELY(c
< sizeof(escapeLookup
))) {
106 Latin1Char escaped
= escapeLookup
[c
];
108 // Directly copy non-escaped code points.
114 // Escape the rest, elaborating Unicode escapes when needed.
117 if (escaped
== 'u') {
125 *dstPtr
++ = ToLowerHex(c
& 0xF);
131 // Non-ASCII non-surrogates are directly copied.
132 if (!unicode::IsSurrogate(c
)) {
137 // So too for complete surrogate pairs.
138 if (MOZ_LIKELY(unicode::IsLeadSurrogate(c
) && srcBegin
< srcEnd
&&
139 unicode::IsTrailSurrogate(*srcBegin
))) {
141 *dstPtr
++ = *srcBegin
++;
145 // But lone surrogates are Unicode-escaped.
146 char32_t as32
= char32_t(c
);
149 *dstPtr
++ = ToLowerHex(as32
>> 12);
150 *dstPtr
++ = ToLowerHex((as32
>> 8) & 0xF);
151 *dstPtr
++ = ToLowerHex((as32
>> 4) & 0xF);
152 *dstPtr
++ = ToLowerHex(as32
& 0xF);
160 template <typename SrcCharT
, typename DstCharT
>
161 static size_t QuoteJSONStringHelper(const JSLinearString
& linear
,
162 StringBuffer
& sb
, size_t sbOffset
) {
163 size_t len
= linear
.length();
165 JS::AutoCheckCannotGC nogc
;
166 RangedPtr
<const SrcCharT
> srcBegin
{linear
.chars
<SrcCharT
>(nogc
), len
};
167 RangedPtr
<DstCharT
> dstBegin
{sb
.begin
<DstCharT
>(), sb
.begin
<DstCharT
>(),
169 RangedPtr
<DstCharT
> dstEnd
=
170 InfallibleQuoteJSONString(srcBegin
, srcBegin
+ len
, dstBegin
+ sbOffset
);
172 return dstEnd
- dstBegin
;
175 static bool QuoteJSONString(JSContext
* cx
, StringBuffer
& sb
, JSString
* str
) {
176 JSLinearString
* linear
= str
->ensureLinear(cx
);
181 if (linear
->hasTwoByteChars() && !sb
.ensureTwoByteChars()) {
185 // We resize the backing buffer to the maximum size we could possibly need,
186 // write the escaped string into it, and shrink it back to the size we ended
189 size_t len
= linear
->length();
190 size_t sbInitialLen
= sb
.length();
192 CheckedInt
<size_t> reservedLen
= CheckedInt
<size_t>(len
) * 6 + 2;
193 if (MOZ_UNLIKELY(!reservedLen
.isValid())) {
194 ReportAllocationOverflow(cx
);
198 if (!sb
.growByUninitialized(reservedLen
.value())) {
204 if (linear
->hasTwoByteChars()) {
206 QuoteJSONStringHelper
<char16_t
, char16_t
>(*linear
, sb
, sbInitialLen
);
207 } else if (sb
.isUnderlyingBufferLatin1()) {
208 newSize
= QuoteJSONStringHelper
<Latin1Char
, Latin1Char
>(*linear
, sb
,
212 QuoteJSONStringHelper
<Latin1Char
, char16_t
>(*linear
, sb
, sbInitialLen
);
215 sb
.shrinkTo(newSize
);
222 using ObjectVector
= GCVector
<JSObject
*, 8>;
224 class StringifyContext
{
226 StringifyContext(JSContext
* cx
, StringBuffer
& sb
, const StringBuffer
& gap
,
227 HandleObject replacer
, const RootedIdVector
& propertyList
,
231 replacer(cx
, replacer
),
232 stack(cx
, ObjectVector(cx
)),
233 propertyList(propertyList
),
235 maybeSafely(maybeSafely
) {
236 MOZ_ASSERT_IF(maybeSafely
, !replacer
);
237 MOZ_ASSERT_IF(maybeSafely
, gap
.empty());
241 const StringBuffer
& gap
;
242 RootedObject replacer
;
243 Rooted
<ObjectVector
> stack
;
244 const RootedIdVector
& propertyList
;
249 } /* anonymous namespace */
251 static bool SerializeJSONProperty(JSContext
* cx
, const Value
& v
,
252 StringifyContext
* scx
);
254 static bool WriteIndent(StringifyContext
* scx
, uint32_t limit
) {
255 if (!scx
->gap
.empty()) {
256 if (!scx
->sb
.append('\n')) {
260 if (scx
->gap
.isUnderlyingBufferLatin1()) {
261 for (uint32_t i
= 0; i
< limit
; i
++) {
262 if (!scx
->sb
.append(scx
->gap
.rawLatin1Begin(),
263 scx
->gap
.rawLatin1End())) {
268 for (uint32_t i
= 0; i
< limit
; i
++) {
269 if (!scx
->sb
.append(scx
->gap
.rawTwoByteBegin(),
270 scx
->gap
.rawTwoByteEnd())) {
282 template <typename KeyType
>
283 class KeyStringifier
{};
286 class KeyStringifier
<uint32_t> {
288 static JSString
* toString(JSContext
* cx
, uint32_t index
) {
289 return IndexToString(cx
, index
);
294 class KeyStringifier
<HandleId
> {
296 static JSString
* toString(JSContext
* cx
, HandleId id
) {
297 return IdToString(cx
, id
);
301 } /* anonymous namespace */
304 * https://262.ecma-international.org/14.0/#sec-serializejsonproperty, steps
305 * 2-4, extracted to enable preprocessing of property values when stringifying
306 * objects in SerializeJSONObject.
308 template <typename KeyType
>
309 static bool PreprocessValue(JSContext
* cx
, HandleObject holder
, KeyType key
,
310 MutableHandleValue vp
, StringifyContext
* scx
) {
311 // We don't want to do any preprocessing here if scx->maybeSafely,
312 // since the stuff we do here can have side-effects.
313 if (scx
->maybeSafely
) {
317 RootedString
keyStr(cx
);
319 // Step 2. Modified by BigInt spec 6.1 to check for a toJSON method on the
320 // BigInt prototype when the value is a BigInt, and to pass the BigInt
321 // primitive value as receiver.
322 if (vp
.isObject() || vp
.isBigInt()) {
323 RootedValue
toJSON(cx
);
324 RootedObject
obj(cx
, JS::ToObject(cx
, vp
));
329 if (!GetProperty(cx
, obj
, vp
, cx
->names().toJSON
, &toJSON
)) {
333 if (IsCallable(toJSON
)) {
334 keyStr
= KeyStringifier
<KeyType
>::toString(cx
, key
);
339 RootedValue
arg0(cx
, StringValue(keyStr
));
340 if (!js::Call(cx
, toJSON
, vp
, arg0
, vp
)) {
347 if (scx
->replacer
&& scx
->replacer
->isCallable()) {
348 MOZ_ASSERT(holder
!= nullptr,
349 "holder object must be present when replacer is callable");
352 keyStr
= KeyStringifier
<KeyType
>::toString(cx
, key
);
358 RootedValue
arg0(cx
, StringValue(keyStr
));
359 RootedValue
replacerVal(cx
, ObjectValue(*scx
->replacer
));
360 if (!js::Call(cx
, replacerVal
, holder
, arg0
, vp
, vp
)) {
366 if (vp
.get().isObject()) {
367 RootedObject
obj(cx
, &vp
.get().toObject());
370 if (!JS::GetBuiltinClass(cx
, obj
, &cls
)) {
374 if (cls
== ESClass::Number
) {
376 if (!ToNumber(cx
, vp
, &d
)) {
380 } else if (cls
== ESClass::String
) {
381 JSString
* str
= ToStringSlow
<CanGC
>(cx
, vp
);
386 } else if (cls
== ESClass::Boolean
|| cls
== ESClass::BigInt
||
388 obj
->is
<RecordObject
>() || obj
->is
<TupleObject
>(), false)) {
389 if (!Unbox(cx
, obj
, vp
)) {
399 * Determines whether a value which has passed by
400 * https://262.ecma-international.org/14.0/#sec-serializejsonproperty steps
401 * 1-4's gauntlet will result in SerializeJSONProperty returning |undefined|.
402 * This function is used to properly omit properties resulting in such values
403 * when stringifying objects, while properly stringifying such properties as
404 * null when they're encountered in arrays.
406 static inline bool IsFilteredValue(const Value
& v
) {
407 MOZ_ASSERT_IF(v
.isMagic(), v
.isMagic(JS_ELEMENTS_HOLE
));
408 return v
.isUndefined() || v
.isSymbol() || v
.isMagic() || IsCallable(v
);
411 class CycleDetector
{
413 CycleDetector(StringifyContext
* scx
, HandleObject obj
)
414 : stack_(&scx
->stack
), obj_(obj
), appended_(false) {}
416 MOZ_ALWAYS_INLINE
bool foundCycle(JSContext
* cx
) {
417 JSObject
* obj
= obj_
;
418 for (JSObject
* obj2
: stack_
) {
419 if (MOZ_UNLIKELY(obj
== obj2
)) {
420 JS_ReportErrorNumberASCII(cx
, GetErrorMessage
, nullptr,
421 JSMSG_JSON_CYCLIC_VALUE
);
425 appended_
= stack_
.append(obj
);
430 if (MOZ_LIKELY(appended_
)) {
431 MOZ_ASSERT(stack_
.back() == obj_
);
437 MutableHandle
<ObjectVector
> stack_
;
442 static inline JSString
* MaybeGetRawJSON(JSContext
* cx
, JSObject
* obj
) {
443 if (!obj
->is
<RawJSONObject
>()) {
447 JSString
* rawJSON
= obj
->as
<js::RawJSONObject
>().rawJSON(cx
);
452 #ifdef ENABLE_RECORD_TUPLE
453 enum class JOType
{ Record
, Object
};
454 template <JOType type
= JOType::Object
>
456 /* https://262.ecma-international.org/14.0/#sec-serializejsonobject */
457 static bool SerializeJSONObject(JSContext
* cx
, HandleObject obj
,
458 StringifyContext
* scx
) {
460 * This method implements the SerializeJSONObject algorithm, but:
462 * * The algorithm is somewhat reformulated to allow the final string to
463 * be streamed into a single buffer, rather than be created and copied
464 * into place incrementally as the algorithm specifies it. This
465 * requires moving portions of the SerializeJSONProperty call in 8a into
466 * this algorithm (and in SerializeJSONArray as well).
469 #ifdef ENABLE_RECORD_TUPLE
472 if constexpr (type
== JOType::Record
) {
473 MOZ_ASSERT(obj
->is
<RecordType
>());
474 rec
= &obj
->as
<RecordType
>();
476 MOZ_ASSERT(!IsExtendedPrimitive(*obj
));
479 MOZ_ASSERT_IF(scx
->maybeSafely
, obj
->is
<PlainObject
>());
482 CycleDetector
detect(scx
, obj
);
483 if (!detect
.foundCycle(cx
)) {
487 if (!scx
->sb
.append('{')) {
492 Maybe
<RootedIdVector
> ids
;
493 const RootedIdVector
* props
;
494 if (scx
->replacer
&& !scx
->replacer
->isCallable()) {
495 // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
496 // might have been a revocable proxy to an array. Such a proxy
497 // satisfies |IsArray|, but any side effect of JSON.stringify
498 // could revoke the proxy so that |!IsArray(scx->replacer)|. See
500 props
= &scx
->propertyList
;
502 MOZ_ASSERT_IF(scx
->replacer
, scx
->propertyList
.length() == 0);
504 if (!GetPropertyKeys(cx
, obj
, JSITER_OWNONLY
, ids
.ptr())) {
510 /* My kingdom for not-quite-initialized-from-the-start references. */
511 const RootedIdVector
& propertyList
= *props
;
513 /* Steps 8-10, 13. */
514 bool wroteMember
= false;
516 for (size_t i
= 0, len
= propertyList
.length(); i
< len
; i
++) {
517 if (!CheckForInterrupt(cx
)) {
522 * Steps 8a-8b. Note that the call to SerializeJSONProperty is broken up
523 * into 1) getting the property; 2) processing for toJSON, calling the
524 * replacer, and handling boxed Number/String/Boolean objects; 3) filtering
525 * out values which process to |undefined|, and 4) stringifying all values
526 * which pass the filter.
528 id
= propertyList
[i
];
529 RootedValue
outputValue(cx
);
531 if (scx
->maybeSafely
) {
533 if (!NativeLookupOwnPropertyNoResolve(cx
, &obj
->as
<NativeObject
>(), id
,
537 MOZ_ASSERT(prop
.isNativeProperty() &&
538 prop
.propertyInfo().isDataDescriptor());
542 #ifdef ENABLE_RECORD_TUPLE
543 if constexpr (type
== JOType::Record
) {
544 MOZ_ALWAYS_TRUE(rec
->getOwnProperty(cx
, id
, &outputValue
));
548 RootedValue
objValue(cx
, ObjectValue(*obj
));
549 if (!GetProperty(cx
, obj
, objValue
, id
, &outputValue
)) {
553 if (!PreprocessValue(cx
, obj
, HandleId(id
), &outputValue
, scx
)) {
556 if (IsFilteredValue(outputValue
)) {
560 /* Output a comma unless this is the first member to write. */
561 if (wroteMember
&& !scx
->sb
.append(',')) {
566 if (!WriteIndent(scx
, scx
->depth
)) {
570 JSString
* s
= IdToString(cx
, id
);
575 if (!QuoteJSONString(cx
, scx
->sb
, s
) || !scx
->sb
.append(':') ||
576 !(scx
->gap
.empty() || scx
->sb
.append(' ')) ||
577 !SerializeJSONProperty(cx
, outputValue
, scx
)) {
582 if (wroteMember
&& !WriteIndent(scx
, scx
->depth
- 1)) {
586 return scx
->sb
.append('}');
589 // For JSON.stringify and JSON.parse with a reviver function, we need to know
590 // the length of an object for which JS::IsArray returned true. This must be
591 // either an ArrayObject or a proxy wrapping one.
592 static MOZ_ALWAYS_INLINE
bool GetLengthPropertyForArrayLike(JSContext
* cx
,
595 if (MOZ_LIKELY(obj
->is
<ArrayObject
>())) {
596 *lengthp
= obj
->as
<ArrayObject
>().length();
599 #ifdef ENABLE_RECORD_TUPLE
600 if (obj
->is
<TupleType
>()) {
601 *lengthp
= obj
->as
<TupleType
>().length();
606 MOZ_ASSERT(obj
->is
<ProxyObject
>());
609 if (!GetLengthProperty(cx
, obj
, &len
)) {
613 // A scripted proxy wrapping an array can return a length value larger than
614 // UINT32_MAX. Stringification will likely report an OOM in this case. Match
615 // other JS engines and report an early error in this case, although
616 // technically this is observable, for example when stringifying with a
617 // replacer function.
618 if (len
> UINT32_MAX
) {
619 ReportAllocationOverflow(cx
);
623 *lengthp
= uint32_t(len
);
627 /* https://262.ecma-international.org/14.0/#sec-serializejsonarray */
628 static bool SerializeJSONArray(JSContext
* cx
, HandleObject obj
,
629 StringifyContext
* scx
) {
631 * This method implements the SerializeJSONArray algorithm, but:
633 * * The algorithm is somewhat reformulated to allow the final string to
634 * be streamed into a single buffer, rather than be created and copied
635 * into place incrementally as the algorithm specifies it. This
636 * requires moving portions of the SerializeJSONProperty call in 8a into
637 * this algorithm (and in SerializeJSONObject as well).
641 CycleDetector
detect(scx
, obj
);
642 if (!detect
.foundCycle(cx
)) {
646 if (!scx
->sb
.append('[')) {
652 if (!GetLengthPropertyForArrayLike(cx
, obj
, &length
)) {
658 /* Steps 4, 10b(i). */
659 if (!WriteIndent(scx
, scx
->depth
)) {
664 RootedValue
outputValue(cx
);
665 for (uint32_t i
= 0; i
< length
; i
++) {
666 if (!CheckForInterrupt(cx
)) {
671 * Steps 8a-8c. Again note how the call to the spec's
672 * SerializeJSONProperty method is broken up into getting the property,
673 * running it past toJSON and the replacer and maybe unboxing, and
674 * interpreting some values as |null| in separate steps.
677 if (scx
->maybeSafely
) {
679 * Trying to do a JS_AlreadyHasOwnElement runs the risk of
680 * hitting OOM on jsid creation. Let's just assert sanity for
681 * small enough indices.
683 MOZ_ASSERT(obj
->is
<ArrayObject
>());
684 MOZ_ASSERT(obj
->is
<NativeObject
>());
685 Rooted
<NativeObject
*> nativeObj(cx
, &obj
->as
<NativeObject
>());
686 if (i
<= PropertyKey::IntMax
) {
688 nativeObj
->containsDenseElement(i
) != nativeObj
->isIndexed(),
689 "the array must either be small enough to remain "
690 "fully dense (and otherwise un-indexed), *or* "
691 "all its initially-dense elements were sparsified "
692 "and the object is indexed");
694 MOZ_ASSERT(nativeObj
->isIndexed());
698 if (!GetElement(cx
, obj
, i
, &outputValue
)) {
701 if (!PreprocessValue(cx
, obj
, i
, &outputValue
, scx
)) {
704 if (IsFilteredValue(outputValue
)) {
705 if (!scx
->sb
.append("null")) {
709 if (!SerializeJSONProperty(cx
, outputValue
, scx
)) {
714 /* Steps 3, 4, 10b(i). */
715 if (i
< length
- 1) {
716 if (!scx
->sb
.append(',')) {
719 if (!WriteIndent(scx
, scx
->depth
)) {
725 /* Step 10(b)(iii). */
726 if (!WriteIndent(scx
, scx
->depth
- 1)) {
731 return scx
->sb
.append(']');
734 /* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
735 static bool SerializeJSONProperty(JSContext
* cx
, const Value
& v
,
736 StringifyContext
* scx
) {
737 /* Step 12 must be handled by the caller. */
738 MOZ_ASSERT(!IsFilteredValue(v
));
741 * This method implements the SerializeJSONProperty algorithm, but:
743 * * We move property retrieval (step 1) into callers to stream the
744 * stringification process and avoid constantly copying strings.
745 * * We move the preprocessing in steps 2-4 into a helper function to
746 * allow both SerializeJSONObject and SerializeJSONArray to use this
747 * method. While SerializeJSONArray could use it without this move,
748 * SerializeJSONObject must omit any |undefined|-valued property per so it
749 * can't stream out a value using the SerializeJSONProperty method exactly as
750 * defined by the spec.
751 * * We move step 12 into callers, again to ease streaming.
756 return QuoteJSONString(cx
, scx
->sb
, v
.toString());
761 return scx
->sb
.append("null");
766 return v
.toBoolean() ? scx
->sb
.append("true") : scx
->sb
.append("false");
772 if (!std::isfinite(v
.toDouble())) {
773 MOZ_ASSERT(!scx
->maybeSafely
,
774 "input JS::ToJSONMaybeSafely must not include "
775 "reachable non-finite numbers");
776 return scx
->sb
.append("null");
780 return NumberValueToStringBuffer(v
, scx
->sb
);
785 JS_ReportErrorNumberASCII(cx
, GetErrorMessage
, nullptr,
786 JSMSG_BIGINT_NOT_SERIALIZABLE
);
790 AutoCheckRecursionLimit
recursion(cx
);
791 if (!recursion
.check(cx
)) {
796 MOZ_ASSERT(v
.hasObjectPayload());
797 RootedObject
obj(cx
, &v
.getObjectPayload());
799 /* https://tc39.es/proposal-json-parse-with-source/#sec-serializejsonproperty
801 if (JSString
* rawJSON
= MaybeGetRawJSON(cx
, obj
)) {
802 return scx
->sb
.append(rawJSON
);
806 !scx
->maybeSafely
|| obj
->is
<PlainObject
>() || obj
->is
<ArrayObject
>(),
807 "input to JS::ToJSONMaybeSafely must not include reachable "
808 "objects that are neither arrays nor plain objects");
811 auto dec
= mozilla::MakeScopeExit([&] { scx
->depth
--; });
813 #ifdef ENABLE_RECORD_TUPLE
814 if (v
.isExtendedPrimitive()) {
815 if (obj
->is
<RecordType
>()) {
816 return SerializeJSONObject
<JOType::Record
>(cx
, obj
, scx
);
818 if (obj
->is
<TupleType
>()) {
819 return SerializeJSONArray(cx
, obj
, scx
);
821 MOZ_CRASH("Unexpected extended primitive - boxes cannot be stringified.");
826 if (!IsArray(cx
, obj
, &isArray
)) {
830 return isArray
? SerializeJSONArray(cx
, obj
, scx
)
831 : SerializeJSONObject(cx
, obj
, scx
);
834 static bool CanFastStringifyObject(NativeObject
* obj
) {
835 if (ClassCanHaveExtraEnumeratedProperties(obj
->getClass())) {
839 if (obj
->is
<ArrayObject
>()) {
840 // Arrays will look up all keys [0..length) so disallow anything that could
841 // find those keys anywhere but in the dense elements.
842 if (!IsPackedArray(obj
) && ObjectMayHaveExtraIndexedProperties(obj
)) {
846 // Non-Arrays will only look at own properties, but still disallow any
847 // indexed properties other than in the dense elements because they would
849 if (ObjectMayHaveExtraIndexedOwnProperties(obj
)) {
854 // Only used for internal environment objects that should never be passed to
856 MOZ_ASSERT(!obj
->getOpsLookupProperty());
858 #ifdef ENABLE_RECORD_TUPLE
859 if (ObjectValue(*obj
).isExtendedPrimitive()) {
867 #define FOR_EACH_STRINGIFY_BAIL_REASON(MACRO) \
869 MACRO(INELIGIBLE_OBJECT) \
870 MACRO(DEEP_RECURSION) \
871 MACRO(NON_DATA_PROPERTY) \
872 MACRO(TOO_MANY_PROPERTIES) \
875 MACRO(HAVE_REPLACER) \
879 MACRO(IMPURE_LOOKUP) \
882 enum class BailReason
: uint8_t {
883 #define DECLARE_ENUM(name) name,
884 FOR_EACH_STRINGIFY_BAIL_REASON(DECLARE_ENUM
)
888 static const char* DescribeStringifyBailReason(BailReason whySlow
) {
890 #define ENUM_NAME(name) \
891 case BailReason::name: \
893 FOR_EACH_STRINGIFY_BAIL_REASON(ENUM_NAME
)
900 // Iterator over all the dense elements of an object. Used
901 // for both Arrays and non-Arrays.
902 class DenseElementsIteratorForJSON
{
903 HeapSlotArray elements
;
906 // Arrays can have a length less than getDenseInitializedLength(), in which
907 // case the remaining Array elements are treated as UndefinedValue.
908 uint32_t numElements
;
912 explicit DenseElementsIteratorForJSON(NativeObject
* nobj
)
913 : elements(nobj
->getDenseElements()),
915 numElements(nobj
->getDenseInitializedLength()) {
916 length
= nobj
->is
<ArrayObject
>() ? nobj
->as
<ArrayObject
>().length()
920 bool done() const { return element
== length
; }
923 // For Arrays, steps 6-8 of
924 // https://262.ecma-international.org/14.0/#sec-serializejsonarray. For
925 // non-Arrays, step 6a of
926 // https://262.ecma-international.org/14.0/#sec-serializejsonobject
927 // following the order from
928 // https://262.ecma-international.org/14.0/#sec-ordinaryownpropertykeys
932 // Consider specializing the iterator for Arrays vs non-Arrays to avoid this
934 return i
< numElements
? elements
.begin()[i
] : UndefinedValue();
937 uint32_t getIndex() const { return element
; }
940 // An iterator over the non-element properties of a Shape, returned in forward
941 // (creation) order. Note that it is fallible, so after iteration is complete
942 // isOverflowed() should be called to verify that the results are actually
945 class ShapePropertyForwardIterNoGC
{
946 // Pointer to the current PropMap with length and an index within it.
951 // Stack of PropMaps to iterate through, oldest properties on top. The current
952 // map (map_, above) is never on this stack.
953 mozilla::Vector
<PropMap
*> stack_
;
955 const NativeShape
* shape_
;
957 MOZ_ALWAYS_INLINE
void settle() {
959 if (MOZ_UNLIKELY(i_
== mapLength_
)) {
961 if (stack_
.empty()) {
962 mapLength_
= 0; // Done
965 map_
= stack_
.back();
968 stack_
.empty() ? shape_
->propMapLength() : PropMap::Capacity
;
969 } else if (MOZ_UNLIKELY(shape_
->isDictionary() && !map_
->hasKey(i_
))) {
970 // Dictionary maps can have "holes" for removed properties, so keep
971 // going until we find a non-hole slot.
980 explicit ShapePropertyForwardIterNoGC(NativeShape
* shape
) : shape_(shape
) {
981 // Set map_ to the PropMap containing the first property (the deepest map in
982 // the previous() chain). Push pointers to all other PropMaps onto stack_.
983 map_
= shape
->propMap();
989 while (map_
->hasPrevious()) {
990 if (!stack_
.append(map_
)) {
992 i_
= mapLength_
= UINT32_MAX
;
995 map_
= map_
->asLinked()->previous();
998 // Set mapLength_ to the number of properties in map_ (including dictionary
1000 mapLength_
= stack_
.empty() ? shape_
->propMapLength() : PropMap::Capacity
;
1005 bool done() const { return i_
== mapLength_
; }
1006 bool isOverflowed() const { return i_
== UINT32_MAX
; }
1008 void operator++(int) {
1009 MOZ_ASSERT(!done());
1014 PropertyInfoWithKey
get() const {
1015 MOZ_ASSERT(!done());
1016 return map_
->getPropertyInfoWithKey(i_
);
1019 PropertyInfoWithKey
operator*() const { return get(); }
1021 // Fake pointer struct to make operator-> work.
1022 // See https://stackoverflow.com/a/52856349.
1024 PropertyInfoWithKey val_
;
1025 const PropertyInfoWithKey
* operator->() const { return &val_
; }
1027 FakePtr
operator->() const { return {get()}; }
1030 // Iterator over EnumerableOwnProperties
1031 // https://262.ecma-international.org/14.0/#sec-enumerableownproperties
1032 // that fails if it encounters any accessor properties, as they are not handled
1033 // by JSON FastSerializeJSONProperty, or if it sees too many properties on one
1035 class OwnNonIndexKeysIterForJSON
{
1036 ShapePropertyForwardIterNoGC shapeIter
;
1038 BailReason fastFailed_
= BailReason::NO_REASON
;
1041 // Skip over any non-enumerable or Symbol properties, and permanently fail
1042 // if any enumerable non-data properties are encountered.
1043 for (; !shapeIter
.done(); shapeIter
++) {
1044 if (!shapeIter
->enumerable()) {
1047 if (!shapeIter
->isDataProperty()) {
1048 fastFailed_
= BailReason::NON_DATA_PROPERTY
;
1052 PropertyKey id
= shapeIter
->key();
1053 if (!id
.isSymbol()) {
1061 explicit OwnNonIndexKeysIterForJSON(const NativeObject
* nobj
)
1062 : shapeIter(nobj
->shape()) {
1063 if (MOZ_UNLIKELY(shapeIter
.isOverflowed())) {
1064 fastFailed_
= BailReason::TOO_MANY_PROPERTIES
;
1068 if (!nobj
->hasEnumerableProperty()) {
1069 // Non-Arrays with no enumerable properties can just be skipped.
1070 MOZ_ASSERT(!nobj
->is
<ArrayObject
>());
1077 bool done() const { return done_
|| shapeIter
.done(); }
1078 BailReason
cannotFastStringify() const { return fastFailed_
; }
1080 PropertyInfoWithKey
next() {
1081 MOZ_ASSERT(!done());
1082 PropertyInfoWithKey prop
= shapeIter
.get();
1089 // Steps from https://262.ecma-international.org/14.0/#sec-serializejsonproperty
1090 static bool EmitSimpleValue(JSContext
* cx
, StringBuffer
& sb
, const Value
& v
) {
1093 return QuoteJSONString(cx
, sb
, v
.toString());
1098 return sb
.append("null");
1102 if (v
.isBoolean()) {
1103 return v
.toBoolean() ? sb
.append("true") : sb
.append("false");
1109 if (!std::isfinite(v
.toDouble())) {
1110 return sb
.append("null");
1114 return NumberValueToStringBuffer(v
, sb
);
1117 // Unrepresentable values.
1118 if (v
.isUndefined() || v
.isMagic()) {
1119 MOZ_ASSERT_IF(v
.isMagic(), v
.isMagic(JS_ELEMENTS_HOLE
));
1120 return sb
.append("null");
1124 MOZ_CRASH("should have validated printable simple value already");
1127 // https://262.ecma-international.org/14.0/#sec-serializejsonproperty step 8b
1128 // where K is an integer index.
1129 static bool EmitQuotedIndexColon(StringBuffer
& sb
, uint32_t index
) {
1130 Int32ToCStringBuf cbuf
;
1132 const char* cstr
= ::Int32ToCString(&cbuf
, index
, &cstrlen
);
1133 if (!sb
.reserve(sb
.length() + 1 + cstrlen
+ 1 + 1)) {
1136 sb
.infallibleAppend('"');
1137 sb
.infallibleAppend(cstr
, cstrlen
);
1138 sb
.infallibleAppend('"');
1139 sb
.infallibleAppend(':');
1143 // Similar to PreprocessValue: replace the value with a simpler one to
1144 // stringify, but also detect whether the value is compatible with the fast
1145 // path. If not, bail out by setting *whySlow and returning true.
1146 static bool PreprocessFastValue(JSContext
* cx
, Value
* vp
, StringifyContext
* scx
,
1147 BailReason
* whySlow
) {
1148 MOZ_ASSERT(!scx
->maybeSafely
);
1151 // https://262.ecma-international.org/14.0/#sec-serializejsonproperty
1153 // Disallow BigInts to avoid caring about BigInt.prototype.toJSON.
1154 if (vp
->isBigInt()) {
1155 *whySlow
= BailReason::BIGINT
;
1159 if (!vp
->isObject()) {
1163 if (!vp
->toObject().is
<NativeObject
>()) {
1164 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
1168 // Step 2: lookup a .toJSON property (and bail if found).
1169 NativeObject
* obj
= &vp
->toObject().as
<NativeObject
>();
1170 PropertyResult toJSON
;
1171 NativeObject
* holder
;
1172 PropertyKey id
= NameToId(cx
->names().toJSON
);
1173 if (!NativeLookupPropertyInline
<NoGC
, LookupResolveMode::CheckMayResolve
>(
1174 cx
, obj
, id
, &holder
, &toJSON
)) {
1175 // Looking up this property would require a side effect.
1176 *whySlow
= BailReason::IMPURE_LOOKUP
;
1179 if (toJSON
.isFound()) {
1180 *whySlow
= BailReason::HAVE_TOJSON
;
1184 // Step 4: convert primitive wrapper objects to primitives. Disallowed for
1186 if (obj
->is
<NumberObject
>() || obj
->is
<StringObject
>() ||
1187 obj
->is
<BooleanObject
>() || obj
->is
<BigIntObject
>() ||
1188 IF_RECORD_TUPLE(obj
->is
<RecordObject
>() || obj
->is
<TupleObject
>(),
1190 // Primitive wrapper objects can invoke arbitrary code when being coerced to
1191 // their primitive values (eg via @@toStringTag).
1192 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
1196 if (obj
->isCallable()) {
1197 // Steps 11,12: Callable objects are treated as undefined.
1202 if (!CanFastStringifyObject(obj
)) {
1203 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
1210 // FastSerializeJSONProperty maintains an explicit stack to handle nested
1211 // objects. For each object, first the dense elements are iterated, then the
1212 // named properties (included sparse indexes, which will cause
1213 // FastSerializeJSONProperty to bail out.)
1215 // The iterators for each of those parts are not merged into a single common
1216 // iterator because the interface is different for the two parts, and they are
1217 // handled separately in the FastSerializeJSONProperty code.
1218 struct FastStackEntry
{
1220 Variant
<DenseElementsIteratorForJSON
, OwnNonIndexKeysIterForJSON
> iter
;
1221 bool isArray
; // Cached nobj->is<ArrayObject>()
1223 // Given an object, a FastStackEntry starts with the dense elements. The
1224 // caller is expected to inspect the variant to use it differently based on
1225 // which iterator is active.
1226 explicit FastStackEntry(NativeObject
* obj
)
1228 iter(AsVariant(DenseElementsIteratorForJSON(obj
))),
1229 isArray(obj
->is
<ArrayObject
>()) {}
1231 // Called by Vector when moving data around.
1232 FastStackEntry(FastStackEntry
&& other
) noexcept
1233 : nobj(other
.nobj
), iter(std::move(other
.iter
)), isArray(other
.isArray
) {}
1235 // Move assignment, called when updating the `top` entry.
1236 void operator=(FastStackEntry
&& other
) noexcept
{
1238 iter
= std::move(other
.iter
);
1239 isArray
= other
.isArray
;
1242 // Advance from dense elements to the named properties.
1243 void advanceToProperties() {
1244 iter
= AsVariant(OwnNonIndexKeysIterForJSON(nobj
));
1248 /* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
1249 static bool FastSerializeJSONProperty(JSContext
* cx
, Handle
<Value
> v
,
1250 StringifyContext
* scx
,
1251 BailReason
* whySlow
) {
1252 MOZ_ASSERT(*whySlow
== BailReason::NO_REASON
);
1253 MOZ_ASSERT(v
.isObject());
1255 if (JSString
* rawJSON
= MaybeGetRawJSON(cx
, &v
.toObject())) {
1256 return scx
->sb
.append(rawJSON
);
1260 * FastSerializeJSONProperty is an optimistic fast path for the
1261 * SerializeJSONProperty algorithm that applies in limited situations. It
1262 * falls back to SerializeJSONProperty() if:
1264 * * Any externally visible code attempts to run: getter, enumerate
1265 * hook, toJSON property.
1266 * * Sparse index found (this would require accumulating props and sorting.)
1267 * * Max stack depth is reached. (This will also detect self-referential
1274 * wroteMember = false
1275 * OUTER: while true:
1278 * while !top.done():
1279 * key, value = top.next()
1280 * if top is a non-Array and value is skippable:
1284 * wroteMember = true
1285 * if value is object:
1286 * emit(key + ":") if top is iterating a non-Array
1289 * wroteMember = false
1292 * emit(value) or emit(key + ":" + value)
1294 * if stack is empty: done!
1295 * top <- stack.pop()
1296 * wroteMember = true
1300 * * The `while !top.done()` loop is split into the dense element portion
1301 * and the slot portion. Each is iterated to completion before advancing
1304 * * For Arrays, the named properties are not output, but they are still
1305 * scanned to bail if any numeric keys are found that could be indexes.
1308 // FastSerializeJSONProperty will bail if an interrupt is requested in the
1309 // middle of an operation, so handle any interrupts now before starting. Note:
1310 // this can GC, but after this point nothing should be able to GC unless
1311 // something fails, so rooting is unnecessary.
1312 if (!CheckForInterrupt(cx
)) {
1316 constexpr size_t MAX_STACK_DEPTH
= 20;
1317 Vector
<FastStackEntry
> stack(cx
);
1318 if (!stack
.reserve(MAX_STACK_DEPTH
- 1)) {
1321 // Construct an iterator for the object,
1322 // https://262.ecma-international.org/14.0/#sec-serializejsonobject step 6:
1323 // EnumerableOwnPropertyNames or
1324 // https://262.ecma-international.org/14.0/#sec-serializejsonarray step 7-8.
1325 FastStackEntry
top(&v
.toObject().as
<NativeObject
>());
1326 bool wroteMember
= false;
1328 if (!CanFastStringifyObject(top
.nobj
)) {
1329 *whySlow
= BailReason::INELIGIBLE_OBJECT
;
1335 if (!scx
->sb
.append(top
.isArray
? '[' : '{')) {
1340 if (top
.iter
.is
<DenseElementsIteratorForJSON
>()) {
1341 auto& iter
= top
.iter
.as
<DenseElementsIteratorForJSON
>();
1342 bool nestedObject
= false;
1343 while (!iter
.done()) {
1344 // Interrupts can GC and we are working with unrooted pointers.
1345 if (cx
->hasPendingInterrupt(InterruptReason::CallbackUrgent
) ||
1346 cx
->hasPendingInterrupt(InterruptReason::CallbackCanWait
)) {
1347 *whySlow
= BailReason::INTERRUPT
;
1351 uint32_t index
= iter
.getIndex();
1352 Value val
= iter
.next();
1354 if (!PreprocessFastValue(cx
, &val
, scx
, whySlow
)) {
1357 if (*whySlow
!= BailReason::NO_REASON
) {
1360 if (IsFilteredValue(val
)) {
1362 // Arrays convert unrepresentable values to "null".
1363 val
= UndefinedValue();
1365 // Objects skip unrepresentable values.
1370 if (wroteMember
&& !scx
->sb
.append(',')) {
1376 if (!EmitQuotedIndexColon(scx
->sb
, index
)) {
1381 if (val
.isObject()) {
1382 if (JSString
* rawJSON
= MaybeGetRawJSON(cx
, &val
.toObject())) {
1383 if (!scx
->sb
.append(rawJSON
)) {
1387 if (stack
.length() >= MAX_STACK_DEPTH
- 1) {
1388 *whySlow
= BailReason::DEEP_RECURSION
;
1391 // Save the current iterator position on the stack and
1392 // switch to processing the nested value.
1393 stack
.infallibleAppend(std::move(top
));
1394 top
= FastStackEntry(&val
.toObject().as
<NativeObject
>());
1395 wroteMember
= false;
1396 nestedObject
= true; // Break out to the outer loop.
1399 } else if (!EmitSimpleValue(cx
, scx
->sb
, val
)) {
1405 continue; // Break out to outer loop.
1408 MOZ_ASSERT(iter
.done());
1410 MOZ_ASSERT(!top
.nobj
->isIndexed() || IsPackedArray(top
.nobj
));
1412 top
.advanceToProperties();
1416 if (top
.iter
.is
<OwnNonIndexKeysIterForJSON
>()) {
1417 auto& iter
= top
.iter
.as
<OwnNonIndexKeysIterForJSON
>();
1418 bool nesting
= false;
1419 while (!iter
.done()) {
1420 // Interrupts can GC and we are working with unrooted pointers.
1421 if (cx
->hasPendingInterrupt(InterruptReason::CallbackUrgent
) ||
1422 cx
->hasPendingInterrupt(InterruptReason::CallbackCanWait
)) {
1423 *whySlow
= BailReason::INTERRUPT
;
1427 PropertyInfoWithKey prop
= iter
.next();
1429 // A non-Array with indexed elements would need to sort the indexes
1430 // numerically, which this code does not support. These objects are
1431 // skipped when obj->isIndexed(), so no index properties should be found
1433 mozilla::DebugOnly
<uint32_t> index
= -1;
1434 MOZ_ASSERT(!IdIsIndex(prop
.key(), &index
));
1436 Value val
= top
.nobj
->getSlot(prop
.slot());
1437 if (!PreprocessFastValue(cx
, &val
, scx
, whySlow
)) {
1440 if (*whySlow
!= BailReason::NO_REASON
) {
1443 if (IsFilteredValue(val
)) {
1444 // Undefined check in
1445 // https://262.ecma-international.org/14.0/#sec-serializejsonobject
1446 // step 8b, covering undefined, symbol
1450 if (wroteMember
&& !scx
->sb
.append(",")) {
1455 MOZ_ASSERT(prop
.key().isString());
1456 if (!QuoteJSONString(cx
, scx
->sb
, prop
.key().toString())) {
1460 if (!scx
->sb
.append(':')) {
1463 if (val
.isObject()) {
1464 if (JSString
* rawJSON
= MaybeGetRawJSON(cx
, &val
.toObject())) {
1465 if (!scx
->sb
.append(rawJSON
)) {
1469 if (stack
.length() >= MAX_STACK_DEPTH
- 1) {
1470 *whySlow
= BailReason::DEEP_RECURSION
;
1473 // Save the current iterator position on the stack and
1474 // switch to processing the nested value.
1475 stack
.infallibleAppend(std::move(top
));
1476 top
= FastStackEntry(&val
.toObject().as
<NativeObject
>());
1477 wroteMember
= false;
1478 nesting
= true; // Break out to the outer loop.
1481 } else if (!EmitSimpleValue(cx
, scx
->sb
, val
)) {
1485 *whySlow
= iter
.cannotFastStringify();
1486 if (*whySlow
!= BailReason::NO_REASON
) {
1490 continue; // Break out to outer loop.
1492 MOZ_ASSERT(iter
.done());
1495 if (!scx
->sb
.append(top
.isArray
? ']' : '}')) {
1498 if (stack
.empty()) {
1499 return true; // Success!
1501 top
= std::move(stack
.back());
1508 /* https://262.ecma-international.org/14.0/#sec-json.stringify */
1509 bool js::Stringify(JSContext
* cx
, MutableHandleValue vp
, JSObject
* replacer_
,
1510 const Value
& space_
, StringBuffer
& sb
,
1511 StringifyBehavior stringifyBehavior
) {
1512 RootedObject
replacer(cx
, replacer_
);
1513 RootedValue
space(cx
, space_
);
1515 MOZ_ASSERT_IF(stringifyBehavior
== StringifyBehavior::RestrictedSafe
,
1517 MOZ_ASSERT_IF(stringifyBehavior
== StringifyBehavior::RestrictedSafe
,
1520 * This uses MOZ_ASSERT, since it's actually asserting something jsapi
1521 * consumers could get wrong, so needs a better error message.
1523 MOZ_ASSERT(stringifyBehavior
!= StringifyBehavior::RestrictedSafe
||
1524 vp
.toObject().is
<PlainObject
>() ||
1525 vp
.toObject().is
<ArrayObject
>(),
1526 "input to JS::ToJSONMaybeSafely must be a plain object or array");
1529 RootedIdVector
propertyList(cx
);
1530 BailReason whySlow
= BailReason::NO_REASON
;
1531 if (stringifyBehavior
== StringifyBehavior::SlowOnly
||
1532 stringifyBehavior
== StringifyBehavior::RestrictedSafe
) {
1533 whySlow
= BailReason::API
;
1536 whySlow
= BailReason::HAVE_REPLACER
;
1538 if (replacer
->isCallable()) {
1539 /* Step 5a(i): use replacer to transform values. */
1540 } else if (!IsArray(cx
, replacer
, &isArray
)) {
1542 } else if (isArray
) {
1545 /* Step 5b(ii)(2). */
1547 if (!GetLengthPropertyForArrayLike(cx
, replacer
, &len
)) {
1551 // Cap the initial size to a moderately small value. This avoids
1552 // ridiculous over-allocation if an array with bogusly-huge length
1553 // is passed in. If we end up having to add elements past this
1554 // size, the set will naturally resize to accommodate them.
1555 const uint32_t MaxInitialSize
= 32;
1556 Rooted
<GCHashSet
<jsid
>> idSet(
1557 cx
, GCHashSet
<jsid
>(cx
, std::min(len
, MaxInitialSize
)));
1559 /* Step 5b(ii)(3). */
1562 /* Step 5b(ii)(4). */
1563 RootedValue
item(cx
);
1564 for (; k
< len
; k
++) {
1565 if (!CheckForInterrupt(cx
)) {
1569 /* Step 5b(ii)(4)(a-b). */
1570 if (!GetElement(cx
, replacer
, k
, &item
)) {
1574 /* Step 5b(ii)(4)(c-g). */
1576 if (item
.isNumber() || item
.isString()) {
1577 if (!PrimitiveValueToId
<CanGC
>(cx
, item
, &id
)) {
1582 if (!GetClassOfValue(cx
, item
, &cls
)) {
1586 if (cls
!= ESClass::String
&& cls
!= ESClass::Number
) {
1590 JSAtom
* atom
= ToAtom
<CanGC
>(cx
, item
);
1595 id
.set(AtomToId(atom
));
1598 /* Step 5b(ii)(4)(g). */
1599 auto p
= idSet
.lookupForAdd(id
);
1601 /* Step 5b(ii)(4)(g)(i). */
1602 if (!idSet
.add(p
, id
) || !propertyList
.append(id
)) {
1613 if (space
.isObject()) {
1614 RootedObject
spaceObj(cx
, &space
.toObject());
1617 if (!JS::GetBuiltinClass(cx
, spaceObj
, &cls
)) {
1621 if (cls
== ESClass::Number
) {
1623 if (!ToNumber(cx
, space
, &d
)) {
1626 space
= NumberValue(d
);
1627 } else if (cls
== ESClass::String
) {
1628 JSString
* str
= ToStringSlow
<CanGC
>(cx
, space
);
1632 space
= StringValue(str
);
1636 StringBuffer
gap(cx
);
1638 if (space
.isNumber()) {
1641 MOZ_ALWAYS_TRUE(ToInteger(cx
, space
, &d
));
1642 d
= std::min(10.0, d
);
1643 if (d
>= 1 && !gap
.appendN(' ', uint32_t(d
))) {
1646 } else if (space
.isString()) {
1648 JSLinearString
* str
= space
.toString()->ensureLinear(cx
);
1652 size_t len
= std::min(size_t(10), str
->length());
1653 if (!gap
.appendSubstring(str
, 0, len
)) {
1658 MOZ_ASSERT(gap
.empty());
1661 whySlow
= BailReason::HAVE_SPACE
;
1664 Rooted
<PlainObject
*> wrapper(cx
);
1665 RootedId
emptyId(cx
, NameToId(cx
->names().empty_
));
1666 if (replacer
&& replacer
->isCallable()) {
1667 // We can skip creating the initial wrapper object if no replacer
1668 // function is present.
1671 wrapper
= NewPlainObject(cx
);
1677 if (!NativeDefineDataProperty(cx
, wrapper
, emptyId
, vp
, JSPROP_ENUMERATE
)) {
1683 Rooted
<JSAtom
*> fastJSON(cx
);
1684 if (whySlow
== BailReason::NO_REASON
) {
1685 MOZ_ASSERT(propertyList
.empty());
1686 MOZ_ASSERT(stringifyBehavior
!= StringifyBehavior::RestrictedSafe
);
1687 StringifyContext
scx(cx
, sb
, gap
, nullptr, propertyList
, false);
1688 if (!PreprocessFastValue(cx
, vp
.address(), &scx
, &whySlow
)) {
1691 if (!vp
.isObject()) {
1692 // "Fast" stringify of primitives would create a wrapper object and thus
1693 // be slower than regular stringify.
1694 whySlow
= BailReason::PRIMITIVE
;
1696 if (whySlow
== BailReason::NO_REASON
) {
1697 if (!FastSerializeJSONProperty(cx
, vp
, &scx
, &whySlow
)) {
1700 if (whySlow
== BailReason::NO_REASON
) {
1701 // Fast stringify succeeded!
1702 if (stringifyBehavior
!= StringifyBehavior::Compare
) {
1705 fastJSON
= scx
.sb
.finishAtom();
1710 scx
.sb
.clear(); // Preserves allocated space.
1714 if (MOZ_UNLIKELY((stringifyBehavior
== StringifyBehavior::FastOnly
) &&
1715 (whySlow
!= BailReason::NO_REASON
))) {
1716 JS_ReportErrorASCII(cx
, "JSON stringify failed mandatory fast path: %s",
1717 DescribeStringifyBailReason(whySlow
));
1721 // Slow, general path.
1723 StringifyContext
scx(cx
, sb
, gap
, replacer
, propertyList
,
1724 stringifyBehavior
== StringifyBehavior::RestrictedSafe
);
1725 if (!PreprocessValue(cx
, wrapper
, HandleId(emptyId
), vp
, &scx
)) {
1728 if (IsFilteredValue(vp
)) {
1732 if (!SerializeJSONProperty(cx
, vp
, &scx
)) {
1736 // For StringBehavior::Compare, when the fast path succeeded.
1737 if (MOZ_UNLIKELY(fastJSON
)) {
1738 JSAtom
* slowJSON
= scx
.sb
.finishAtom();
1742 if (fastJSON
!= slowJSON
) {
1743 MOZ_CRASH("JSON.stringify mismatch between fast and slow paths");
1745 // Put the JSON back into the StringBuffer for returning.
1746 if (!sb
.append(slowJSON
)) {
1754 /* https://262.ecma-international.org/14.0/#sec-internalizejsonproperty */
1755 static bool InternalizeJSONProperty(
1756 JSContext
* cx
, HandleObject holder
, HandleId name
, HandleValue reviver
,
1757 MutableHandle
<ParseRecordObject
> parseRecord
, MutableHandleValue vp
) {
1758 AutoCheckRecursionLimit
recursion(cx
);
1759 if (!recursion
.check(cx
)) {
1764 RootedValue
val(cx
);
1765 if (!GetProperty(cx
, holder
, holder
, name
, &val
)) {
1769 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1770 RootedObject
context(cx
);
1771 Rooted
<UniquePtr
<ParseRecordObject::EntryMap
>> entries(cx
);
1772 if (JS::Prefs::experimental_json_parse_with_source()) {
1773 // https://tc39.es/proposal-json-parse-with-source/#sec-internalizejsonproperty
1774 bool sameVal
= false;
1775 Rooted
<Value
> parsedValue(cx
, parseRecord
.get().value
);
1776 if (!SameValue(cx
, parsedValue
, val
, &sameVal
)) {
1779 if (!parseRecord
.get().isEmpty() && sameVal
) {
1780 if (parseRecord
.get().parseNode
) {
1781 MOZ_ASSERT(!val
.isObject());
1782 Rooted
<IdValueVector
> props(cx
, cx
);
1783 if (!props
.emplaceBack(
1784 IdValuePair(NameToId(cx
->names().source
),
1785 StringValue(parseRecord
.get().parseNode
)))) {
1788 context
= NewPlainObjectWithUniqueNames(cx
, props
);
1793 entries
= std::move(parseRecord
.get().entries
);
1796 context
= NewPlainObject(cx
);
1805 if (val
.isObject()) {
1806 RootedObject
obj(cx
, &val
.toObject());
1809 if (!IsArray(cx
, obj
, &isArray
)) {
1816 if (!GetLengthPropertyForArrayLike(cx
, obj
, &length
)) {
1820 /* Steps 2b(ii-iii). */
1822 RootedValue
newElement(cx
);
1823 for (uint32_t i
= 0; i
< length
; i
++) {
1824 if (!CheckForInterrupt(cx
)) {
1828 if (!IndexToId(cx
, i
, &id
)) {
1832 /* Step 2a(iii)(1). */
1833 Rooted
<ParseRecordObject
> elementRecord(cx
);
1834 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1836 if (auto entry
= entries
->lookup(id
)) {
1837 elementRecord
= std::move(entry
->value());
1841 if (!InternalizeJSONProperty(cx
, obj
, id
, reviver
, &elementRecord
,
1846 ObjectOpResult ignored
;
1847 if (newElement
.isUndefined()) {
1848 /* Step 2b(iii)(3). The spec deliberately ignores strict failure. */
1849 if (!DeleteProperty(cx
, obj
, id
, ignored
)) {
1853 /* Step 2b(iii)(4). The spec deliberately ignores strict failure. */
1854 Rooted
<PropertyDescriptor
> desc(
1855 cx
, PropertyDescriptor::Data(newElement
,
1856 {JS::PropertyAttribute::Configurable
,
1857 JS::PropertyAttribute::Enumerable
,
1858 JS::PropertyAttribute::Writable
}));
1859 if (!DefineProperty(cx
, obj
, id
, desc
, ignored
)) {
1866 RootedIdVector
keys(cx
);
1867 if (!GetPropertyKeys(cx
, obj
, JSITER_OWNONLY
, &keys
)) {
1873 RootedValue
newElement(cx
);
1874 for (size_t i
= 0, len
= keys
.length(); i
< len
; i
++) {
1875 if (!CheckForInterrupt(cx
)) {
1879 /* Step 2c(ii)(1). */
1881 Rooted
<ParseRecordObject
> entryRecord(cx
);
1882 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1884 if (auto entry
= entries
->lookup(id
)) {
1885 entryRecord
= std::move(entry
->value());
1889 if (!InternalizeJSONProperty(cx
, obj
, id
, reviver
, &entryRecord
,
1894 ObjectOpResult ignored
;
1895 if (newElement
.isUndefined()) {
1896 /* Step 2c(ii)(2). The spec deliberately ignores strict failure. */
1897 if (!DeleteProperty(cx
, obj
, id
, ignored
)) {
1901 /* Step 2c(ii)(3). The spec deliberately ignores strict failure. */
1902 Rooted
<PropertyDescriptor
> desc(
1903 cx
, PropertyDescriptor::Data(newElement
,
1904 {JS::PropertyAttribute::Configurable
,
1905 JS::PropertyAttribute::Enumerable
,
1906 JS::PropertyAttribute::Writable
}));
1907 if (!DefineProperty(cx
, obj
, id
, desc
, ignored
)) {
1916 RootedString
key(cx
, IdToString(cx
, name
));
1921 RootedValue
keyVal(cx
, StringValue(key
));
1922 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1923 if (JS::Prefs::experimental_json_parse_with_source()) {
1924 RootedValue
contextVal(cx
, ObjectValue(*context
));
1925 return js::Call(cx
, reviver
, holder
, keyVal
, val
, contextVal
, vp
);
1928 return js::Call(cx
, reviver
, holder
, keyVal
, val
, vp
);
1931 static bool Revive(JSContext
* cx
, HandleValue reviver
,
1932 MutableHandle
<ParseRecordObject
> pro
,
1933 MutableHandleValue vp
) {
1934 Rooted
<PlainObject
*> obj(cx
, NewPlainObject(cx
));
1939 if (!DefineDataProperty(cx
, obj
, cx
->names().empty_
, vp
)) {
1943 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1944 MOZ_ASSERT_IF(JS::Prefs::experimental_json_parse_with_source(),
1945 pro
.get().value
== vp
.get());
1947 Rooted
<jsid
> id(cx
, NameToId(cx
->names().empty_
));
1948 return InternalizeJSONProperty(cx
, obj
, id
, reviver
, pro
, vp
);
1951 template <typename CharT
>
1952 bool ParseJSON(JSContext
* cx
, const mozilla::Range
<const CharT
> chars
,
1953 MutableHandleValue vp
) {
1954 Rooted
<JSONParser
<CharT
>> parser(cx
, cx
, chars
,
1955 JSONParser
<CharT
>::ParseType::JSONParse
);
1956 return parser
.parse(vp
);
1959 template <typename CharT
>
1960 bool js::ParseJSONWithReviver(JSContext
* cx
,
1961 const mozilla::Range
<const CharT
> chars
,
1962 HandleValue reviver
, MutableHandleValue vp
) {
1963 /* https://262.ecma-international.org/14.0/#sec-json.parse steps 2-10. */
1964 Rooted
<ParseRecordObject
> pro(cx
);
1965 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1966 if (JS::Prefs::experimental_json_parse_with_source() && IsCallable(reviver
)) {
1967 Rooted
<JSONReviveParser
<CharT
>> parser(cx
, cx
, chars
);
1968 if (!parser
.get().parse(vp
, &pro
)) {
1973 if (!ParseJSON(cx
, chars
, vp
)) {
1978 if (IsCallable(reviver
)) {
1979 return Revive(cx
, reviver
, &pro
, vp
);
1984 template bool js::ParseJSONWithReviver(
1985 JSContext
* cx
, const mozilla::Range
<const Latin1Char
> chars
,
1986 HandleValue reviver
, MutableHandleValue vp
);
1988 template bool js::ParseJSONWithReviver(
1989 JSContext
* cx
, const mozilla::Range
<const char16_t
> chars
,
1990 HandleValue reviver
, MutableHandleValue vp
);
1992 static bool json_toSource(JSContext
* cx
, unsigned argc
, Value
* vp
) {
1993 CallArgs args
= CallArgsFromVp(argc
, vp
);
1994 args
.rval().setString(cx
->names().JSON
);
1998 /* https://262.ecma-international.org/14.0/#sec-json.parse */
1999 static bool json_parse(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2000 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "parse");
2001 CallArgs args
= CallArgsFromVp(argc
, vp
);
2004 JSString
* str
= (args
.length() >= 1) ? ToString
<CanGC
>(cx
, args
[0])
2005 : cx
->names().undefined
;
2010 JSLinearString
* linear
= str
->ensureLinear(cx
);
2015 AutoStableStringChars
linearChars(cx
);
2016 if (!linearChars
.init(cx
, linear
)) {
2020 HandleValue reviver
= args
.get(1);
2023 return linearChars
.isLatin1()
2024 ? ParseJSONWithReviver(cx
, linearChars
.latin1Range(), reviver
,
2026 : ParseJSONWithReviver(cx
, linearChars
.twoByteRange(), reviver
,
2030 #ifdef ENABLE_RECORD_TUPLE
2031 bool BuildImmutableProperty(JSContext
* cx
, HandleValue value
, HandleId name
,
2032 HandleValue reviver
,
2033 MutableHandleValue immutableRes
) {
2034 MOZ_ASSERT(!name
.isSymbol());
2037 if (value
.isObject()) {
2038 RootedValue
childValue(cx
), newElement(cx
);
2039 RootedId
childName(cx
);
2042 if (value
.toObject().is
<ArrayObject
>()) {
2043 Rooted
<ArrayObject
*> arr(cx
, &value
.toObject().as
<ArrayObject
>());
2046 uint32_t len
= arr
->length();
2048 TupleType
* tup
= TupleType::createUninitialized(cx
, len
);
2052 immutableRes
.setExtendedPrimitive(*tup
);
2055 for (uint32_t i
= 0; i
< len
; i
++) {
2057 childName
.set(PropertyKey::Int(i
));
2060 if (!GetProperty(cx
, arr
, value
, childName
, &childValue
)) {
2065 if (!BuildImmutableProperty(cx
, childValue
, childName
, reviver
,
2069 MOZ_ASSERT(newElement
.isPrimitive());
2072 if (!tup
->initializeNextElement(cx
, newElement
)) {
2078 tup
->finishInitialization(cx
);
2080 RootedObject
obj(cx
, &value
.toObject());
2082 // Step 1.c.i - We only get the property keys rather than the
2083 // entries, but the difference is not observable from user code
2084 // because `obj` is a plan object not exposed externally
2085 RootedIdVector
props(cx
);
2086 if (!GetPropertyKeys(cx
, obj
, JSITER_OWNONLY
, &props
)) {
2090 RecordType
* rec
= RecordType::createUninitialized(cx
, props
.length());
2094 immutableRes
.setExtendedPrimitive(*rec
);
2096 for (uint32_t i
= 0; i
< props
.length(); i
++) {
2098 childName
.set(props
[i
]);
2101 if (!GetProperty(cx
, obj
, value
, childName
, &childValue
)) {
2106 if (!BuildImmutableProperty(cx
, childValue
, childName
, reviver
,
2110 MOZ_ASSERT(newElement
.isPrimitive());
2113 if (!newElement
.isUndefined()) {
2114 // Step 1.c.iii.5.a-b
2115 rec
->initializeNextProperty(cx
, childName
, newElement
);
2120 rec
->finishInitialization(cx
);
2124 immutableRes
.set(value
);
2128 if (IsCallable(reviver
)) {
2129 RootedValue
keyVal(cx
, StringValue(IdToString(cx
, name
)));
2132 if (!Call(cx
, reviver
, UndefinedHandleValue
, keyVal
, immutableRes
,
2138 if (!immutableRes
.isPrimitive()) {
2139 JS_ReportErrorNumberASCII(cx
, GetErrorMessage
, nullptr,
2140 JSMSG_RECORD_TUPLE_NO_OBJECT
);
2148 static bool json_parseImmutable(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2149 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "parseImmutable");
2150 CallArgs args
= CallArgsFromVp(argc
, vp
);
2153 JSString
* str
= (args
.length() >= 1) ? ToString
<CanGC
>(cx
, args
[0])
2154 : cx
->names().undefined
;
2159 JSLinearString
* linear
= str
->ensureLinear(cx
);
2164 AutoStableStringChars
linearChars(cx
);
2165 if (!linearChars
.init(cx
, linear
)) {
2169 HandleValue reviver
= args
.get(1);
2170 RootedValue
unfiltered(cx
);
2172 if (linearChars
.isLatin1()) {
2173 if (!ParseJSON(cx
, linearChars
.latin1Range(), &unfiltered
)) {
2177 if (!ParseJSON(cx
, linearChars
.twoByteRange(), &unfiltered
)) {
2182 RootedId
id(cx
, NameToId(cx
->names().empty_
));
2183 return BuildImmutableProperty(cx
, unfiltered
, id
, reviver
, args
.rval());
2187 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
2188 /* https://tc39.es/proposal-json-parse-with-source/#sec-json.israwjson */
2189 static bool json_isRawJSON(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2190 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "isRawJSON");
2191 CallArgs args
= CallArgsFromVp(argc
, vp
);
2194 if (args
.get(0).isObject()) {
2195 Rooted
<JSObject
*> obj(cx
, &args
[0].toObject());
2197 if (obj
->is
<RawJSONObject
>()) {
2198 bool objIsFrozen
= false;
2199 MOZ_ASSERT(js::TestIntegrityLevel(cx
, obj
, IntegrityLevel::Frozen
,
2201 MOZ_ASSERT(objIsFrozen
);
2204 args
.rval().setBoolean(obj
->is
<RawJSONObject
>());
2209 args
.rval().setBoolean(false);
2213 static inline bool IsJSONWhitespace(char16_t ch
) {
2214 return ch
== '\t' || ch
== '\n' || ch
== '\r' || ch
== ' ';
2217 /* https://tc39.es/proposal-json-parse-with-source/#sec-json.rawjson */
2218 static bool json_rawJSON(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2219 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "rawJSON");
2220 CallArgs args
= CallArgsFromVp(argc
, vp
);
2223 JSString
* jsonString
= ToString
<CanGC
>(cx
, args
.get(0));
2228 Rooted
<JSLinearString
*> linear(cx
, jsonString
->ensureLinear(cx
));
2233 AutoStableStringChars
linearChars(cx
);
2234 if (!linearChars
.init(cx
, linear
)) {
2239 if (linear
->empty()) {
2240 JS_ReportErrorNumberASCII(cx
, js::GetErrorMessage
, nullptr,
2241 JSMSG_JSON_RAW_EMPTY
);
2244 if (IsJSONWhitespace(linear
->latin1OrTwoByteChar(0)) ||
2245 IsJSONWhitespace(linear
->latin1OrTwoByteChar(linear
->length() - 1))) {
2246 JS_ReportErrorNumberASCII(cx
, js::GetErrorMessage
, nullptr,
2247 JSMSG_JSON_RAW_WHITESPACE
);
2252 RootedValue
parsedValue(cx
);
2253 if (linearChars
.isLatin1()) {
2254 if (!ParseJSON(cx
, linearChars
.latin1Range(), &parsedValue
)) {
2258 if (!ParseJSON(cx
, linearChars
.twoByteRange(), &parsedValue
)) {
2263 if (parsedValue
.isObject()) {
2264 JS_ReportErrorNumberASCII(cx
, js::GetErrorMessage
, nullptr,
2265 JSMSG_JSON_RAW_ARRAY_OR_OBJECT
);
2270 Rooted
<RawJSONObject
*> obj(cx
, RawJSONObject::create(cx
, linear
));
2276 if (!js::FreezeObject(cx
, obj
)) {
2280 args
.rval().setObject(*obj
);
2283 #endif // ENABLE_JSON_PARSE_WITH_SOURCE
2285 /* https://262.ecma-international.org/14.0/#sec-json.stringify */
2286 bool json_stringify(JSContext
* cx
, unsigned argc
, Value
* vp
) {
2287 AutoJSMethodProfilerEntry
pseudoFrame(cx
, "JSON", "stringify");
2288 CallArgs args
= CallArgsFromVp(argc
, vp
);
2290 RootedObject
replacer(cx
,
2291 args
.get(1).isObject() ? &args
[1].toObject() : nullptr);
2292 RootedValue
value(cx
, args
.get(0));
2293 RootedValue
space(cx
, args
.get(2));
2296 StringifyBehavior behavior
= StringifyBehavior::Compare
;
2298 StringifyBehavior behavior
= StringifyBehavior::Normal
;
2301 JSStringBuilder
sb(cx
);
2302 if (!Stringify(cx
, &value
, replacer
, space
, sb
, behavior
)) {
2306 // XXX This can never happen to nsJSON.cpp, but the JSON object
2307 // needs to support returning undefined. So this is a little awkward
2308 // for the API, because we want to support streaming writers.
2310 JSString
* str
= sb
.finishString();
2314 args
.rval().setString(str
);
2316 args
.rval().setUndefined();
2322 static const JSFunctionSpec json_static_methods
[] = {
2323 JS_FN("toSource", json_toSource
, 0, 0),
2324 JS_FN("parse", json_parse
, 2, 0),
2325 JS_FN("stringify", json_stringify
, 3, 0),
2326 #ifdef ENABLE_RECORD_TUPLE
2327 JS_FN("parseImmutable", json_parseImmutable
, 2, 0),
2329 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
2330 JS_FN("isRawJSON", json_isRawJSON
, 1, 0),
2331 JS_FN("rawJSON", json_rawJSON
, 1, 0),
2335 static const JSPropertySpec json_static_properties
[] = {
2336 JS_STRING_SYM_PS(toStringTag
, "JSON", JSPROP_READONLY
), JS_PS_END
};
2338 static JSObject
* CreateJSONObject(JSContext
* cx
, JSProtoKey key
) {
2339 RootedObject
proto(cx
, &cx
->global()->getObjectPrototype());
2340 return NewTenuredObjectWithGivenProto(cx
, &JSONClass
, proto
);
2343 static const ClassSpec JSONClassSpec
= {
2344 CreateJSONObject
, nullptr, json_static_methods
, json_static_properties
};
2346 const JSClass
js::JSONClass
= {"JSON", JSCLASS_HAS_CACHED_PROTO(JSProto_JSON
),
2347 JS_NULL_CLASS_OPS
, &JSONClassSpec
};