Backed out changeset 48baafc34055 (bug 1789166) for causing mochitests failures....
[gecko.git] / js / src / builtin / JSON.cpp
blob185e979ba82eb669ceae802756860de3b406fa57
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "builtin/JSON.h"
9 #include "mozilla/CheckedInt.h"
10 #include "mozilla/FloatingPoint.h"
11 #include "mozilla/Range.h"
12 #include "mozilla/ScopeExit.h"
13 #include "mozilla/Variant.h"
15 #include <algorithm>
17 #include "jsnum.h"
18 #include "jstypes.h"
20 #include "builtin/Array.h"
21 #include "builtin/BigInt.h"
22 #include "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"
31 #include "js/Value.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"
49 #endif
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"
56 using namespace js;
58 using mozilla::AsVariant;
59 using mozilla::CheckedInt;
60 using mozilla::Maybe;
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] = {
78 // clang-format off
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
89 // clang-format on
92 /* Step 1. */
93 *dstPtr++ = '"';
95 auto ToLowerHex = [](uint8_t u) {
96 MOZ_ASSERT(u <= 0xF);
97 return "0123456789abcdef"[u];
100 /* Step 2. */
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.
109 if (escaped == 0) {
110 *dstPtr++ = c;
111 continue;
114 // Escape the rest, elaborating Unicode escapes when needed.
115 *dstPtr++ = '\\';
116 *dstPtr++ = escaped;
117 if (escaped == 'u') {
118 *dstPtr++ = '0';
119 *dstPtr++ = '0';
121 uint8_t x = c >> 4;
122 MOZ_ASSERT(x < 10);
123 *dstPtr++ = '0' + x;
125 *dstPtr++ = ToLowerHex(c & 0xF);
128 continue;
131 // Non-ASCII non-surrogates are directly copied.
132 if (!unicode::IsSurrogate(c)) {
133 *dstPtr++ = c;
134 continue;
137 // So too for complete surrogate pairs.
138 if (MOZ_LIKELY(unicode::IsLeadSurrogate(c) && srcBegin < srcEnd &&
139 unicode::IsTrailSurrogate(*srcBegin))) {
140 *dstPtr++ = c;
141 *dstPtr++ = *srcBegin++;
142 continue;
145 // But lone surrogates are Unicode-escaped.
146 char32_t as32 = char32_t(c);
147 *dstPtr++ = '\\';
148 *dstPtr++ = 'u';
149 *dstPtr++ = ToLowerHex(as32 >> 12);
150 *dstPtr++ = ToLowerHex((as32 >> 8) & 0xF);
151 *dstPtr++ = ToLowerHex((as32 >> 4) & 0xF);
152 *dstPtr++ = ToLowerHex(as32 & 0xF);
155 /* Steps 3-4. */
156 *dstPtr++ = '"';
157 return dstPtr;
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>(),
168 sb.end<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);
177 if (!linear) {
178 return false;
181 if (linear->hasTwoByteChars() && !sb.ensureTwoByteChars()) {
182 return false;
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
187 // up needing.
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);
195 return false;
198 if (!sb.growByUninitialized(reservedLen.value())) {
199 return false;
202 size_t newSize;
204 if (linear->hasTwoByteChars()) {
205 newSize =
206 QuoteJSONStringHelper<char16_t, char16_t>(*linear, sb, sbInitialLen);
207 } else if (sb.isUnderlyingBufferLatin1()) {
208 newSize = QuoteJSONStringHelper<Latin1Char, Latin1Char>(*linear, sb,
209 sbInitialLen);
210 } else {
211 newSize =
212 QuoteJSONStringHelper<Latin1Char, char16_t>(*linear, sb, sbInitialLen);
215 sb.shrinkTo(newSize);
217 return true;
220 namespace {
222 using ObjectVector = GCVector<JSObject*, 8>;
224 class StringifyContext {
225 public:
226 StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
227 HandleObject replacer, const RootedIdVector& propertyList,
228 bool maybeSafely)
229 : sb(sb),
230 gap(gap),
231 replacer(cx, replacer),
232 stack(cx, ObjectVector(cx)),
233 propertyList(propertyList),
234 depth(0),
235 maybeSafely(maybeSafely) {
236 MOZ_ASSERT_IF(maybeSafely, !replacer);
237 MOZ_ASSERT_IF(maybeSafely, gap.empty());
240 StringBuffer& sb;
241 const StringBuffer& gap;
242 RootedObject replacer;
243 Rooted<ObjectVector> stack;
244 const RootedIdVector& propertyList;
245 uint32_t depth;
246 bool maybeSafely;
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')) {
257 return false;
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())) {
264 return false;
267 } else {
268 for (uint32_t i = 0; i < limit; i++) {
269 if (!scx->sb.append(scx->gap.rawTwoByteBegin(),
270 scx->gap.rawTwoByteEnd())) {
271 return false;
277 return true;
280 namespace {
282 template <typename KeyType>
283 class KeyStringifier {};
285 template <>
286 class KeyStringifier<uint32_t> {
287 public:
288 static JSString* toString(JSContext* cx, uint32_t index) {
289 return IndexToString(cx, index);
293 template <>
294 class KeyStringifier<HandleId> {
295 public:
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) {
314 return true;
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));
325 if (!obj) {
326 return false;
329 if (!GetProperty(cx, obj, vp, cx->names().toJSON, &toJSON)) {
330 return false;
333 if (IsCallable(toJSON)) {
334 keyStr = KeyStringifier<KeyType>::toString(cx, key);
335 if (!keyStr) {
336 return false;
339 RootedValue arg0(cx, StringValue(keyStr));
340 if (!js::Call(cx, toJSON, vp, arg0, vp)) {
341 return false;
346 /* Step 3. */
347 if (scx->replacer && scx->replacer->isCallable()) {
348 MOZ_ASSERT(holder != nullptr,
349 "holder object must be present when replacer is callable");
351 if (!keyStr) {
352 keyStr = KeyStringifier<KeyType>::toString(cx, key);
353 if (!keyStr) {
354 return false;
358 RootedValue arg0(cx, StringValue(keyStr));
359 RootedValue replacerVal(cx, ObjectValue(*scx->replacer));
360 if (!js::Call(cx, replacerVal, holder, arg0, vp, vp)) {
361 return false;
365 /* Step 4. */
366 if (vp.get().isObject()) {
367 RootedObject obj(cx, &vp.get().toObject());
369 ESClass cls;
370 if (!JS::GetBuiltinClass(cx, obj, &cls)) {
371 return false;
374 if (cls == ESClass::Number) {
375 double d;
376 if (!ToNumber(cx, vp, &d)) {
377 return false;
379 vp.setNumber(d);
380 } else if (cls == ESClass::String) {
381 JSString* str = ToStringSlow<CanGC>(cx, vp);
382 if (!str) {
383 return false;
385 vp.setString(str);
386 } else if (cls == ESClass::Boolean || cls == ESClass::BigInt ||
387 IF_RECORD_TUPLE(
388 obj->is<RecordObject>() || obj->is<TupleObject>(), false)) {
389 if (!Unbox(cx, obj, vp)) {
390 return false;
395 return true;
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 {
412 public:
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);
422 return false;
425 appended_ = stack_.append(obj);
426 return appended_;
429 ~CycleDetector() {
430 if (MOZ_LIKELY(appended_)) {
431 MOZ_ASSERT(stack_.back() == obj_);
432 stack_.popBack();
436 private:
437 MutableHandle<ObjectVector> stack_;
438 HandleObject obj_;
439 bool appended_;
442 static inline JSString* MaybeGetRawJSON(JSContext* cx, JSObject* obj) {
443 auto* unwrappedObj = obj->maybeUnwrapIf<js::RawJSONObject>();
444 if (!unwrappedObj) {
445 return nullptr;
447 JSAutoRealm ar(cx, unwrappedObj);
449 JSString* rawJSON = unwrappedObj->rawJSON(cx);
450 MOZ_ASSERT(rawJSON);
451 return rawJSON;
454 #ifdef ENABLE_RECORD_TUPLE
455 enum class JOType { Record, Object };
456 template <JOType type = JOType::Object>
457 #endif
458 /* https://262.ecma-international.org/14.0/#sec-serializejsonobject */
459 static bool SerializeJSONObject(JSContext* cx, HandleObject obj,
460 StringifyContext* scx) {
462 * This method implements the SerializeJSONObject algorithm, but:
464 * * The algorithm is somewhat reformulated to allow the final string to
465 * be streamed into a single buffer, rather than be created and copied
466 * into place incrementally as the algorithm specifies it. This
467 * requires moving portions of the SerializeJSONProperty call in 8a into
468 * this algorithm (and in SerializeJSONArray as well).
471 #ifdef ENABLE_RECORD_TUPLE
472 RecordType* rec;
474 if constexpr (type == JOType::Record) {
475 MOZ_ASSERT(obj->is<RecordType>());
476 rec = &obj->as<RecordType>();
477 } else {
478 MOZ_ASSERT(!IsExtendedPrimitive(*obj));
480 #endif
481 MOZ_ASSERT_IF(scx->maybeSafely, obj->is<PlainObject>());
483 /* Steps 1-2, 11. */
484 CycleDetector detect(scx, obj);
485 if (!detect.foundCycle(cx)) {
486 return false;
489 if (!scx->sb.append('{')) {
490 return false;
493 /* Steps 5-7. */
494 Maybe<RootedIdVector> ids;
495 const RootedIdVector* props;
496 if (scx->replacer && !scx->replacer->isCallable()) {
497 // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
498 // might have been a revocable proxy to an array. Such a proxy
499 // satisfies |IsArray|, but any side effect of JSON.stringify
500 // could revoke the proxy so that |!IsArray(scx->replacer)|. See
501 // bug 1196497.
502 props = &scx->propertyList;
503 } else {
504 MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0);
505 ids.emplace(cx);
506 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr())) {
507 return false;
509 props = ids.ptr();
512 /* My kingdom for not-quite-initialized-from-the-start references. */
513 const RootedIdVector& propertyList = *props;
515 /* Steps 8-10, 13. */
516 bool wroteMember = false;
517 RootedId id(cx);
518 for (size_t i = 0, len = propertyList.length(); i < len; i++) {
519 if (!CheckForInterrupt(cx)) {
520 return false;
524 * Steps 8a-8b. Note that the call to SerializeJSONProperty is broken up
525 * into 1) getting the property; 2) processing for toJSON, calling the
526 * replacer, and handling boxed Number/String/Boolean objects; 3) filtering
527 * out values which process to |undefined|, and 4) stringifying all values
528 * which pass the filter.
530 id = propertyList[i];
531 RootedValue outputValue(cx);
532 #ifdef DEBUG
533 if (scx->maybeSafely) {
534 PropertyResult prop;
535 if (!NativeLookupOwnPropertyNoResolve(cx, &obj->as<NativeObject>(), id,
536 &prop)) {
537 return false;
539 MOZ_ASSERT(prop.isNativeProperty() &&
540 prop.propertyInfo().isDataDescriptor());
542 #endif // DEBUG
544 #ifdef ENABLE_RECORD_TUPLE
545 if constexpr (type == JOType::Record) {
546 MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, id, &outputValue));
547 } else
548 #endif
550 RootedValue objValue(cx, ObjectValue(*obj));
551 if (!GetProperty(cx, obj, objValue, id, &outputValue)) {
552 return false;
555 if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx)) {
556 return false;
558 if (IsFilteredValue(outputValue)) {
559 continue;
562 /* Output a comma unless this is the first member to write. */
563 if (wroteMember && !scx->sb.append(',')) {
564 return false;
566 wroteMember = true;
568 if (!WriteIndent(scx, scx->depth)) {
569 return false;
572 JSString* s = IdToString(cx, id);
573 if (!s) {
574 return false;
577 if (!QuoteJSONString(cx, scx->sb, s) || !scx->sb.append(':') ||
578 !(scx->gap.empty() || scx->sb.append(' ')) ||
579 !SerializeJSONProperty(cx, outputValue, scx)) {
580 return false;
584 if (wroteMember && !WriteIndent(scx, scx->depth - 1)) {
585 return false;
588 return scx->sb.append('}');
591 // For JSON.stringify and JSON.parse with a reviver function, we need to know
592 // the length of an object for which JS::IsArray returned true. This must be
593 // either an ArrayObject or a proxy wrapping one.
594 static MOZ_ALWAYS_INLINE bool GetLengthPropertyForArrayLike(JSContext* cx,
595 HandleObject obj,
596 uint32_t* lengthp) {
597 if (MOZ_LIKELY(obj->is<ArrayObject>())) {
598 *lengthp = obj->as<ArrayObject>().length();
599 return true;
601 #ifdef ENABLE_RECORD_TUPLE
602 if (obj->is<TupleType>()) {
603 *lengthp = obj->as<TupleType>().length();
604 return true;
606 #endif
608 MOZ_ASSERT(obj->is<ProxyObject>());
610 uint64_t len = 0;
611 if (!GetLengthProperty(cx, obj, &len)) {
612 return false;
615 // A scripted proxy wrapping an array can return a length value larger than
616 // UINT32_MAX. Stringification will likely report an OOM in this case. Match
617 // other JS engines and report an early error in this case, although
618 // technically this is observable, for example when stringifying with a
619 // replacer function.
620 if (len > UINT32_MAX) {
621 ReportAllocationOverflow(cx);
622 return false;
625 *lengthp = uint32_t(len);
626 return true;
629 /* https://262.ecma-international.org/14.0/#sec-serializejsonarray */
630 static bool SerializeJSONArray(JSContext* cx, HandleObject obj,
631 StringifyContext* scx) {
633 * This method implements the SerializeJSONArray algorithm, but:
635 * * The algorithm is somewhat reformulated to allow the final string to
636 * be streamed into a single buffer, rather than be created and copied
637 * into place incrementally as the algorithm specifies it. This
638 * requires moving portions of the SerializeJSONProperty call in 8a into
639 * this algorithm (and in SerializeJSONObject as well).
642 /* Steps 1-2, 11. */
643 CycleDetector detect(scx, obj);
644 if (!detect.foundCycle(cx)) {
645 return false;
648 if (!scx->sb.append('[')) {
649 return false;
652 /* Step 6. */
653 uint32_t length;
654 if (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
655 return false;
658 /* Steps 7-10. */
659 if (length != 0) {
660 /* Steps 4, 10b(i). */
661 if (!WriteIndent(scx, scx->depth)) {
662 return false;
665 /* Steps 7-10. */
666 RootedValue outputValue(cx);
667 for (uint32_t i = 0; i < length; i++) {
668 if (!CheckForInterrupt(cx)) {
669 return false;
673 * Steps 8a-8c. Again note how the call to the spec's
674 * SerializeJSONProperty method is broken up into getting the property,
675 * running it past toJSON and the replacer and maybe unboxing, and
676 * interpreting some values as |null| in separate steps.
678 #ifdef DEBUG
679 if (scx->maybeSafely) {
681 * Trying to do a JS_AlreadyHasOwnElement runs the risk of
682 * hitting OOM on jsid creation. Let's just assert sanity for
683 * small enough indices.
685 MOZ_ASSERT(obj->is<ArrayObject>());
686 MOZ_ASSERT(obj->is<NativeObject>());
687 Rooted<NativeObject*> nativeObj(cx, &obj->as<NativeObject>());
688 if (i <= PropertyKey::IntMax) {
689 MOZ_ASSERT(
690 nativeObj->containsDenseElement(i) != nativeObj->isIndexed(),
691 "the array must either be small enough to remain "
692 "fully dense (and otherwise un-indexed), *or* "
693 "all its initially-dense elements were sparsified "
694 "and the object is indexed");
695 } else {
696 MOZ_ASSERT(nativeObj->isIndexed());
699 #endif
700 if (!GetElement(cx, obj, i, &outputValue)) {
701 return false;
703 if (!PreprocessValue(cx, obj, i, &outputValue, scx)) {
704 return false;
706 if (IsFilteredValue(outputValue)) {
707 if (!scx->sb.append("null")) {
708 return false;
710 } else {
711 if (!SerializeJSONProperty(cx, outputValue, scx)) {
712 return false;
716 /* Steps 3, 4, 10b(i). */
717 if (i < length - 1) {
718 if (!scx->sb.append(',')) {
719 return false;
721 if (!WriteIndent(scx, scx->depth)) {
722 return false;
727 /* Step 10(b)(iii). */
728 if (!WriteIndent(scx, scx->depth - 1)) {
729 return false;
733 return scx->sb.append(']');
736 /* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
737 static bool SerializeJSONProperty(JSContext* cx, const Value& v,
738 StringifyContext* scx) {
739 /* Step 12 must be handled by the caller. */
740 MOZ_ASSERT(!IsFilteredValue(v));
743 * This method implements the SerializeJSONProperty algorithm, but:
745 * * We move property retrieval (step 1) into callers to stream the
746 * stringification process and avoid constantly copying strings.
747 * * We move the preprocessing in steps 2-4 into a helper function to
748 * allow both SerializeJSONObject and SerializeJSONArray to use this
749 * method. While SerializeJSONArray could use it without this move,
750 * SerializeJSONObject must omit any |undefined|-valued property per so it
751 * can't stream out a value using the SerializeJSONProperty method exactly as
752 * defined by the spec.
753 * * We move step 12 into callers, again to ease streaming.
756 /* Step 8. */
757 if (v.isString()) {
758 return QuoteJSONString(cx, scx->sb, v.toString());
761 /* Step 5. */
762 if (v.isNull()) {
763 return scx->sb.append("null");
766 /* Steps 6-7. */
767 if (v.isBoolean()) {
768 return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false");
771 /* Step 9. */
772 if (v.isNumber()) {
773 if (v.isDouble()) {
774 if (!std::isfinite(v.toDouble())) {
775 MOZ_ASSERT(!scx->maybeSafely,
776 "input JS::ToJSONMaybeSafely must not include "
777 "reachable non-finite numbers");
778 return scx->sb.append("null");
782 return NumberValueToStringBuffer(v, scx->sb);
785 /* Step 10. */
786 if (v.isBigInt()) {
787 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
788 JSMSG_BIGINT_NOT_SERIALIZABLE);
789 return false;
792 AutoCheckRecursionLimit recursion(cx);
793 if (!recursion.check(cx)) {
794 return false;
797 /* Step 11. */
798 MOZ_ASSERT(v.hasObjectPayload());
799 RootedObject obj(cx, &v.getObjectPayload());
801 /* https://tc39.es/proposal-json-parse-with-source/#sec-serializejsonproperty
802 * Step 4a.*/
803 if (JSString* rawJSON = MaybeGetRawJSON(cx, obj)) {
804 return scx->sb.append(rawJSON);
807 MOZ_ASSERT(
808 !scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(),
809 "input to JS::ToJSONMaybeSafely must not include reachable "
810 "objects that are neither arrays nor plain objects");
812 scx->depth++;
813 auto dec = mozilla::MakeScopeExit([&] { scx->depth--; });
815 #ifdef ENABLE_RECORD_TUPLE
816 if (v.isExtendedPrimitive()) {
817 if (obj->is<RecordType>()) {
818 return SerializeJSONObject<JOType::Record>(cx, obj, scx);
820 if (obj->is<TupleType>()) {
821 return SerializeJSONArray(cx, obj, scx);
823 MOZ_CRASH("Unexpected extended primitive - boxes cannot be stringified.");
825 #endif
827 bool isArray;
828 if (!IsArray(cx, obj, &isArray)) {
829 return false;
832 return isArray ? SerializeJSONArray(cx, obj, scx)
833 : SerializeJSONObject(cx, obj, scx);
836 static bool CanFastStringifyObject(NativeObject* obj) {
837 if (ClassCanHaveExtraEnumeratedProperties(obj->getClass())) {
838 return false;
841 if (obj->is<ArrayObject>()) {
842 // Arrays will look up all keys [0..length) so disallow anything that could
843 // find those keys anywhere but in the dense elements.
844 if (!IsPackedArray(obj) && ObjectMayHaveExtraIndexedProperties(obj)) {
845 return false;
847 } else {
848 // Non-Arrays will only look at own properties, but still disallow any
849 // indexed properties other than in the dense elements because they would
850 // require sorting.
851 if (ObjectMayHaveExtraIndexedOwnProperties(obj)) {
852 return false;
856 // Only used for internal environment objects that should never be passed to
857 // JSON.stringify.
858 MOZ_ASSERT(!obj->getOpsLookupProperty());
860 #ifdef ENABLE_RECORD_TUPLE
861 if (ObjectValue(*obj).isExtendedPrimitive()) {
862 return false;
864 #endif
866 return true;
869 #define FOR_EACH_STRINGIFY_BAIL_REASON(MACRO) \
870 MACRO(NO_REASON) \
871 MACRO(INELIGIBLE_OBJECT) \
872 MACRO(DEEP_RECURSION) \
873 MACRO(NON_DATA_PROPERTY) \
874 MACRO(TOO_MANY_PROPERTIES) \
875 MACRO(BIGINT) \
876 MACRO(API) \
877 MACRO(HAVE_REPLACER) \
878 MACRO(HAVE_SPACE) \
879 MACRO(PRIMITIVE) \
880 MACRO(HAVE_TOJSON) \
881 MACRO(IMPURE_LOOKUP) \
882 MACRO(INTERRUPT)
884 enum class BailReason : uint8_t {
885 #define DECLARE_ENUM(name) name,
886 FOR_EACH_STRINGIFY_BAIL_REASON(DECLARE_ENUM)
887 #undef DECLARE_ENUM
890 static const char* DescribeStringifyBailReason(BailReason whySlow) {
891 switch (whySlow) {
892 #define ENUM_NAME(name) \
893 case BailReason::name: \
894 return #name;
895 FOR_EACH_STRINGIFY_BAIL_REASON(ENUM_NAME)
896 #undef ENUM_NAME
897 default:
898 return "Unknown";
902 // Iterator over all the dense elements of an object. Used
903 // for both Arrays and non-Arrays.
904 class DenseElementsIteratorForJSON {
905 HeapSlotArray elements;
906 uint32_t element;
908 // Arrays can have a length less than getDenseInitializedLength(), in which
909 // case the remaining Array elements are treated as UndefinedValue.
910 uint32_t numElements;
911 uint32_t length;
913 public:
914 explicit DenseElementsIteratorForJSON(NativeObject* nobj)
915 : elements(nobj->getDenseElements()),
916 element(0),
917 numElements(nobj->getDenseInitializedLength()) {
918 length = nobj->is<ArrayObject>() ? nobj->as<ArrayObject>().length()
919 : numElements;
922 bool done() const { return element == length; }
924 Value next() {
925 // For Arrays, steps 6-8 of
926 // https://262.ecma-international.org/14.0/#sec-serializejsonarray. For
927 // non-Arrays, step 6a of
928 // https://262.ecma-international.org/14.0/#sec-serializejsonobject
929 // following the order from
930 // https://262.ecma-international.org/14.0/#sec-ordinaryownpropertykeys
932 MOZ_ASSERT(!done());
933 auto i = element++;
934 // Consider specializing the iterator for Arrays vs non-Arrays to avoid this
935 // branch.
936 return i < numElements ? elements.begin()[i] : UndefinedValue();
939 uint32_t getIndex() const { return element; }
942 // An iterator over the non-element properties of a Shape, returned in forward
943 // (creation) order. Note that it is fallible, so after iteration is complete
944 // isOverflowed() should be called to verify that the results are actually
945 // complete.
947 class ShapePropertyForwardIterNoGC {
948 // Pointer to the current PropMap with length and an index within it.
949 PropMap* map_;
950 uint32_t mapLength_;
951 uint32_t i_ = 0;
953 // Stack of PropMaps to iterate through, oldest properties on top. The current
954 // map (map_, above) is never on this stack.
955 mozilla::Vector<PropMap*> stack_;
957 const NativeShape* shape_;
959 MOZ_ALWAYS_INLINE void settle() {
960 while (true) {
961 if (MOZ_UNLIKELY(i_ == mapLength_)) {
962 i_ = 0;
963 if (stack_.empty()) {
964 mapLength_ = 0; // Done
965 return;
967 map_ = stack_.back();
968 stack_.popBack();
969 mapLength_ =
970 stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
971 } else if (MOZ_UNLIKELY(shape_->isDictionary() && !map_->hasKey(i_))) {
972 // Dictionary maps can have "holes" for removed properties, so keep
973 // going until we find a non-hole slot.
974 i_++;
975 } else {
976 return;
981 public:
982 explicit ShapePropertyForwardIterNoGC(NativeShape* shape) : shape_(shape) {
983 // Set map_ to the PropMap containing the first property (the deepest map in
984 // the previous() chain). Push pointers to all other PropMaps onto stack_.
985 map_ = shape->propMap();
986 if (!map_) {
987 // No properties.
988 i_ = mapLength_ = 0;
989 return;
991 while (map_->hasPrevious()) {
992 if (!stack_.append(map_)) {
993 // Overflowed.
994 i_ = mapLength_ = UINT32_MAX;
995 return;
997 map_ = map_->asLinked()->previous();
1000 // Set mapLength_ to the number of properties in map_ (including dictionary
1001 // holes, if any.)
1002 mapLength_ = stack_.empty() ? shape_->propMapLength() : PropMap::Capacity;
1004 settle();
1007 bool done() const { return i_ == mapLength_; }
1008 bool isOverflowed() const { return i_ == UINT32_MAX; }
1010 void operator++(int) {
1011 MOZ_ASSERT(!done());
1012 i_++;
1013 settle();
1016 PropertyInfoWithKey get() const {
1017 MOZ_ASSERT(!done());
1018 return map_->getPropertyInfoWithKey(i_);
1021 PropertyInfoWithKey operator*() const { return get(); }
1023 // Fake pointer struct to make operator-> work.
1024 // See https://stackoverflow.com/a/52856349.
1025 struct FakePtr {
1026 PropertyInfoWithKey val_;
1027 const PropertyInfoWithKey* operator->() const { return &val_; }
1029 FakePtr operator->() const { return {get()}; }
1032 // Iterator over EnumerableOwnProperties
1033 // https://262.ecma-international.org/14.0/#sec-enumerableownproperties
1034 // that fails if it encounters any accessor properties, as they are not handled
1035 // by JSON FastSerializeJSONProperty, or if it sees too many properties on one
1036 // object.
1037 class OwnNonIndexKeysIterForJSON {
1038 ShapePropertyForwardIterNoGC shapeIter;
1039 bool done_ = false;
1040 BailReason fastFailed_ = BailReason::NO_REASON;
1042 void settle() {
1043 // Skip over any non-enumerable or Symbol properties, and permanently fail
1044 // if any enumerable non-data properties are encountered.
1045 for (; !shapeIter.done(); shapeIter++) {
1046 if (!shapeIter->enumerable()) {
1047 continue;
1049 if (!shapeIter->isDataProperty()) {
1050 fastFailed_ = BailReason::NON_DATA_PROPERTY;
1051 done_ = true;
1052 return;
1054 PropertyKey id = shapeIter->key();
1055 if (!id.isSymbol()) {
1056 return;
1059 done_ = true;
1062 public:
1063 explicit OwnNonIndexKeysIterForJSON(const NativeObject* nobj)
1064 : shapeIter(nobj->shape()) {
1065 if (MOZ_UNLIKELY(shapeIter.isOverflowed())) {
1066 fastFailed_ = BailReason::TOO_MANY_PROPERTIES;
1067 done_ = true;
1068 return;
1070 if (!nobj->hasEnumerableProperty()) {
1071 // Non-Arrays with no enumerable properties can just be skipped.
1072 MOZ_ASSERT(!nobj->is<ArrayObject>());
1073 done_ = true;
1074 return;
1076 settle();
1079 bool done() const { return done_ || shapeIter.done(); }
1080 BailReason cannotFastStringify() const { return fastFailed_; }
1082 PropertyInfoWithKey next() {
1083 MOZ_ASSERT(!done());
1084 PropertyInfoWithKey prop = shapeIter.get();
1085 shapeIter++;
1086 settle();
1087 return prop;
1091 // Steps from https://262.ecma-international.org/14.0/#sec-serializejsonproperty
1092 static bool EmitSimpleValue(JSContext* cx, StringBuffer& sb, const Value& v) {
1093 /* Step 8. */
1094 if (v.isString()) {
1095 return QuoteJSONString(cx, sb, v.toString());
1098 /* Step 5. */
1099 if (v.isNull()) {
1100 return sb.append("null");
1103 /* Steps 6-7. */
1104 if (v.isBoolean()) {
1105 return v.toBoolean() ? sb.append("true") : sb.append("false");
1108 /* Step 9. */
1109 if (v.isNumber()) {
1110 if (v.isDouble()) {
1111 if (!std::isfinite(v.toDouble())) {
1112 return sb.append("null");
1116 return NumberValueToStringBuffer(v, sb);
1119 // Unrepresentable values.
1120 if (v.isUndefined() || v.isMagic()) {
1121 MOZ_ASSERT_IF(v.isMagic(), v.isMagic(JS_ELEMENTS_HOLE));
1122 return sb.append("null");
1125 /* Step 10. */
1126 MOZ_CRASH("should have validated printable simple value already");
1129 // https://262.ecma-international.org/14.0/#sec-serializejsonproperty step 8b
1130 // where K is an integer index.
1131 static bool EmitQuotedIndexColon(StringBuffer& sb, uint32_t index) {
1132 Int32ToCStringBuf cbuf;
1133 size_t cstrlen;
1134 const char* cstr = ::Int32ToCString(&cbuf, index, &cstrlen);
1135 if (!sb.reserve(sb.length() + 1 + cstrlen + 1 + 1)) {
1136 return false;
1138 sb.infallibleAppend('"');
1139 sb.infallibleAppend(cstr, cstrlen);
1140 sb.infallibleAppend('"');
1141 sb.infallibleAppend(':');
1142 return true;
1145 // Similar to PreprocessValue: replace the value with a simpler one to
1146 // stringify, but also detect whether the value is compatible with the fast
1147 // path. If not, bail out by setting *whySlow and returning true.
1148 static bool PreprocessFastValue(JSContext* cx, Value* vp, StringifyContext* scx,
1149 BailReason* whySlow) {
1150 MOZ_ASSERT(!scx->maybeSafely);
1152 // Steps are from
1153 // https://262.ecma-international.org/14.0/#sec-serializejsonproperty
1155 // Disallow BigInts to avoid caring about BigInt.prototype.toJSON.
1156 if (vp->isBigInt()) {
1157 *whySlow = BailReason::BIGINT;
1158 return true;
1161 if (!vp->isObject()) {
1162 return true;
1165 if (!vp->toObject().is<NativeObject>()) {
1166 *whySlow = BailReason::INELIGIBLE_OBJECT;
1167 return true;
1170 // Step 2: lookup a .toJSON property (and bail if found).
1171 NativeObject* obj = &vp->toObject().as<NativeObject>();
1172 PropertyResult toJSON;
1173 NativeObject* holder;
1174 PropertyKey id = NameToId(cx->names().toJSON);
1175 if (!NativeLookupPropertyInline<NoGC, LookupResolveMode::CheckMayResolve>(
1176 cx, obj, id, &holder, &toJSON)) {
1177 // Looking up this property would require a side effect.
1178 *whySlow = BailReason::IMPURE_LOOKUP;
1179 return true;
1181 if (toJSON.isFound()) {
1182 *whySlow = BailReason::HAVE_TOJSON;
1183 return true;
1186 // Step 4: convert primitive wrapper objects to primitives. Disallowed for
1187 // fast path.
1188 if (obj->is<NumberObject>() || obj->is<StringObject>() ||
1189 obj->is<BooleanObject>() || obj->is<BigIntObject>() ||
1190 IF_RECORD_TUPLE(obj->is<RecordObject>() || obj->is<TupleObject>(),
1191 false)) {
1192 // Primitive wrapper objects can invoke arbitrary code when being coerced to
1193 // their primitive values (eg via @@toStringTag).
1194 *whySlow = BailReason::INELIGIBLE_OBJECT;
1195 return true;
1198 if (obj->isCallable()) {
1199 // Steps 11,12: Callable objects are treated as undefined.
1200 vp->setUndefined();
1201 return true;
1204 if (!CanFastStringifyObject(obj)) {
1205 *whySlow = BailReason::INELIGIBLE_OBJECT;
1206 return true;
1209 return true;
1212 // FastSerializeJSONProperty maintains an explicit stack to handle nested
1213 // objects. For each object, first the dense elements are iterated, then the
1214 // named properties (included sparse indexes, which will cause
1215 // FastSerializeJSONProperty to bail out.)
1217 // The iterators for each of those parts are not merged into a single common
1218 // iterator because the interface is different for the two parts, and they are
1219 // handled separately in the FastSerializeJSONProperty code.
1220 struct FastStackEntry {
1221 NativeObject* nobj;
1222 Variant<DenseElementsIteratorForJSON, OwnNonIndexKeysIterForJSON> iter;
1223 bool isArray; // Cached nobj->is<ArrayObject>()
1225 // Given an object, a FastStackEntry starts with the dense elements. The
1226 // caller is expected to inspect the variant to use it differently based on
1227 // which iterator is active.
1228 explicit FastStackEntry(NativeObject* obj)
1229 : nobj(obj),
1230 iter(AsVariant(DenseElementsIteratorForJSON(obj))),
1231 isArray(obj->is<ArrayObject>()) {}
1233 // Called by Vector when moving data around.
1234 FastStackEntry(FastStackEntry&& other) noexcept
1235 : nobj(other.nobj), iter(std::move(other.iter)), isArray(other.isArray) {}
1237 // Move assignment, called when updating the `top` entry.
1238 void operator=(FastStackEntry&& other) noexcept {
1239 nobj = other.nobj;
1240 iter = std::move(other.iter);
1241 isArray = other.isArray;
1244 // Advance from dense elements to the named properties.
1245 void advanceToProperties() {
1246 iter = AsVariant(OwnNonIndexKeysIterForJSON(nobj));
1250 /* https://262.ecma-international.org/14.0/#sec-serializejsonproperty */
1251 static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v,
1252 StringifyContext* scx,
1253 BailReason* whySlow) {
1254 MOZ_ASSERT(*whySlow == BailReason::NO_REASON);
1255 MOZ_ASSERT(v.isObject());
1257 if (JSString* rawJSON = MaybeGetRawJSON(cx, &v.toObject())) {
1258 return scx->sb.append(rawJSON);
1262 * FastSerializeJSONProperty is an optimistic fast path for the
1263 * SerializeJSONProperty algorithm that applies in limited situations. It
1264 * falls back to SerializeJSONProperty() if:
1266 * * Any externally visible code attempts to run: getter, enumerate
1267 * hook, toJSON property.
1268 * * Sparse index found (this would require accumulating props and sorting.)
1269 * * Max stack depth is reached. (This will also detect self-referential
1270 * input.)
1272 * Algorithm:
1274 * stack = []
1275 * top = iter(obj)
1276 * wroteMember = false
1277 * OUTER: while true:
1278 * if !wroteMember:
1279 * emit("[" or "{")
1280 * while !top.done():
1281 * key, value = top.next()
1282 * if top is a non-Array and value is skippable:
1283 * continue
1284 * if wroteMember:
1285 * emit(",")
1286 * wroteMember = true
1287 * if value is object:
1288 * emit(key + ":") if top is iterating a non-Array
1289 * stack.push(top)
1290 * top <- value
1291 * wroteMember = false
1292 * continue OUTER
1293 * else:
1294 * emit(value) or emit(key + ":" + value)
1295 * emit("]" or "}")
1296 * if stack is empty: done!
1297 * top <- stack.pop()
1298 * wroteMember = true
1300 * except:
1302 * * The `while !top.done()` loop is split into the dense element portion
1303 * and the slot portion. Each is iterated to completion before advancing
1304 * or finishing.
1306 * * For Arrays, the named properties are not output, but they are still
1307 * scanned to bail if any numeric keys are found that could be indexes.
1310 // FastSerializeJSONProperty will bail if an interrupt is requested in the
1311 // middle of an operation, so handle any interrupts now before starting. Note:
1312 // this can GC, but after this point nothing should be able to GC unless
1313 // something fails, so rooting is unnecessary.
1314 if (!CheckForInterrupt(cx)) {
1315 return false;
1318 constexpr size_t MAX_STACK_DEPTH = 20;
1319 Vector<FastStackEntry> stack(cx);
1320 if (!stack.reserve(MAX_STACK_DEPTH - 1)) {
1321 return false;
1323 // Construct an iterator for the object,
1324 // https://262.ecma-international.org/14.0/#sec-serializejsonobject step 6:
1325 // EnumerableOwnPropertyNames or
1326 // https://262.ecma-international.org/14.0/#sec-serializejsonarray step 7-8.
1327 FastStackEntry top(&v.toObject().as<NativeObject>());
1328 bool wroteMember = false;
1330 if (!CanFastStringifyObject(top.nobj)) {
1331 *whySlow = BailReason::INELIGIBLE_OBJECT;
1332 return true;
1335 while (true) {
1336 if (!wroteMember) {
1337 if (!scx->sb.append(top.isArray ? '[' : '{')) {
1338 return false;
1342 if (top.iter.is<DenseElementsIteratorForJSON>()) {
1343 auto& iter = top.iter.as<DenseElementsIteratorForJSON>();
1344 bool nestedObject = false;
1345 while (!iter.done()) {
1346 // Interrupts can GC and we are working with unrooted pointers.
1347 if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
1348 cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
1349 *whySlow = BailReason::INTERRUPT;
1350 return true;
1353 uint32_t index = iter.getIndex();
1354 Value val = iter.next();
1356 if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
1357 return false;
1359 if (*whySlow != BailReason::NO_REASON) {
1360 return true;
1362 if (IsFilteredValue(val)) {
1363 if (top.isArray) {
1364 // Arrays convert unrepresentable values to "null".
1365 val = UndefinedValue();
1366 } else {
1367 // Objects skip unrepresentable values.
1368 continue;
1372 if (wroteMember && !scx->sb.append(',')) {
1373 return false;
1375 wroteMember = true;
1377 if (!top.isArray) {
1378 if (!EmitQuotedIndexColon(scx->sb, index)) {
1379 return false;
1383 if (val.isObject()) {
1384 if (JSString* rawJSON = MaybeGetRawJSON(cx, &val.toObject())) {
1385 if (!scx->sb.append(rawJSON)) {
1386 return false;
1388 } else {
1389 if (stack.length() >= MAX_STACK_DEPTH - 1) {
1390 *whySlow = BailReason::DEEP_RECURSION;
1391 return true;
1393 // Save the current iterator position on the stack and
1394 // switch to processing the nested value.
1395 stack.infallibleAppend(std::move(top));
1396 top = FastStackEntry(&val.toObject().as<NativeObject>());
1397 wroteMember = false;
1398 nestedObject = true; // Break out to the outer loop.
1399 break;
1401 } else if (!EmitSimpleValue(cx, scx->sb, val)) {
1402 return false;
1406 if (nestedObject) {
1407 continue; // Break out to outer loop.
1410 MOZ_ASSERT(iter.done());
1411 if (top.isArray) {
1412 MOZ_ASSERT(!top.nobj->isIndexed() || IsPackedArray(top.nobj));
1413 } else {
1414 top.advanceToProperties();
1418 if (top.iter.is<OwnNonIndexKeysIterForJSON>()) {
1419 auto& iter = top.iter.as<OwnNonIndexKeysIterForJSON>();
1420 bool nesting = false;
1421 while (!iter.done()) {
1422 // Interrupts can GC and we are working with unrooted pointers.
1423 if (cx->hasPendingInterrupt(InterruptReason::CallbackUrgent) ||
1424 cx->hasPendingInterrupt(InterruptReason::CallbackCanWait)) {
1425 *whySlow = BailReason::INTERRUPT;
1426 return true;
1429 PropertyInfoWithKey prop = iter.next();
1431 // A non-Array with indexed elements would need to sort the indexes
1432 // numerically, which this code does not support. These objects are
1433 // skipped when obj->isIndexed(), so no index properties should be found
1434 // here.
1435 mozilla::DebugOnly<uint32_t> index = -1;
1436 MOZ_ASSERT(!IdIsIndex(prop.key(), &index));
1438 Value val = top.nobj->getSlot(prop.slot());
1439 if (!PreprocessFastValue(cx, &val, scx, whySlow)) {
1440 return false;
1442 if (*whySlow != BailReason::NO_REASON) {
1443 return true;
1445 if (IsFilteredValue(val)) {
1446 // Undefined check in
1447 // https://262.ecma-international.org/14.0/#sec-serializejsonobject
1448 // step 8b, covering undefined, symbol
1449 continue;
1452 if (wroteMember && !scx->sb.append(",")) {
1453 return false;
1455 wroteMember = true;
1457 MOZ_ASSERT(prop.key().isString());
1458 if (!QuoteJSONString(cx, scx->sb, prop.key().toString())) {
1459 return false;
1462 if (!scx->sb.append(':')) {
1463 return false;
1465 if (val.isObject()) {
1466 if (JSString* rawJSON = MaybeGetRawJSON(cx, &val.toObject())) {
1467 if (!scx->sb.append(rawJSON)) {
1468 return false;
1470 } else {
1471 if (stack.length() >= MAX_STACK_DEPTH - 1) {
1472 *whySlow = BailReason::DEEP_RECURSION;
1473 return true;
1475 // Save the current iterator position on the stack and
1476 // switch to processing the nested value.
1477 stack.infallibleAppend(std::move(top));
1478 top = FastStackEntry(&val.toObject().as<NativeObject>());
1479 wroteMember = false;
1480 nesting = true; // Break out to the outer loop.
1481 break;
1483 } else if (!EmitSimpleValue(cx, scx->sb, val)) {
1484 return false;
1487 *whySlow = iter.cannotFastStringify();
1488 if (*whySlow != BailReason::NO_REASON) {
1489 return true;
1491 if (nesting) {
1492 continue; // Break out to outer loop.
1494 MOZ_ASSERT(iter.done());
1497 if (!scx->sb.append(top.isArray ? ']' : '}')) {
1498 return false;
1500 if (stack.empty()) {
1501 return true; // Success!
1503 top = std::move(stack.back());
1505 stack.popBack();
1506 wroteMember = true;
1510 /* https://262.ecma-international.org/14.0/#sec-json.stringify */
1511 bool js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_,
1512 const Value& space_, StringBuffer& sb,
1513 StringifyBehavior stringifyBehavior) {
1514 RootedObject replacer(cx, replacer_);
1515 RootedValue space(cx, space_);
1517 MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
1518 space.isNull());
1519 MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
1520 vp.isObject());
1522 * This uses MOZ_ASSERT, since it's actually asserting something jsapi
1523 * consumers could get wrong, so needs a better error message.
1525 MOZ_ASSERT(stringifyBehavior != StringifyBehavior::RestrictedSafe ||
1526 vp.toObject().is<PlainObject>() ||
1527 vp.toObject().is<ArrayObject>(),
1528 "input to JS::ToJSONMaybeSafely must be a plain object or array");
1530 /* Step 5. */
1531 RootedIdVector propertyList(cx);
1532 BailReason whySlow = BailReason::NO_REASON;
1533 if (stringifyBehavior == StringifyBehavior::SlowOnly ||
1534 stringifyBehavior == StringifyBehavior::RestrictedSafe) {
1535 whySlow = BailReason::API;
1537 if (replacer) {
1538 whySlow = BailReason::HAVE_REPLACER;
1539 bool isArray;
1540 if (replacer->isCallable()) {
1541 /* Step 5a(i): use replacer to transform values. */
1542 } else if (!IsArray(cx, replacer, &isArray)) {
1543 return false;
1544 } else if (isArray) {
1545 /* Step 5b(ii). */
1547 /* Step 5b(ii)(2). */
1548 uint32_t len;
1549 if (!GetLengthPropertyForArrayLike(cx, replacer, &len)) {
1550 return false;
1553 // Cap the initial size to a moderately small value. This avoids
1554 // ridiculous over-allocation if an array with bogusly-huge length
1555 // is passed in. If we end up having to add elements past this
1556 // size, the set will naturally resize to accommodate them.
1557 const uint32_t MaxInitialSize = 32;
1558 Rooted<GCHashSet<jsid>> idSet(
1559 cx, GCHashSet<jsid>(cx, std::min(len, MaxInitialSize)));
1561 /* Step 5b(ii)(3). */
1562 uint32_t k = 0;
1564 /* Step 5b(ii)(4). */
1565 RootedValue item(cx);
1566 for (; k < len; k++) {
1567 if (!CheckForInterrupt(cx)) {
1568 return false;
1571 /* Step 5b(ii)(4)(a-b). */
1572 if (!GetElement(cx, replacer, k, &item)) {
1573 return false;
1576 /* Step 5b(ii)(4)(c-g). */
1577 RootedId id(cx);
1578 if (item.isNumber() || item.isString()) {
1579 if (!PrimitiveValueToId<CanGC>(cx, item, &id)) {
1580 return false;
1582 } else {
1583 ESClass cls;
1584 if (!GetClassOfValue(cx, item, &cls)) {
1585 return false;
1588 if (cls != ESClass::String && cls != ESClass::Number) {
1589 continue;
1592 JSAtom* atom = ToAtom<CanGC>(cx, item);
1593 if (!atom) {
1594 return false;
1597 id.set(AtomToId(atom));
1600 /* Step 5b(ii)(4)(g). */
1601 auto p = idSet.lookupForAdd(id);
1602 if (!p) {
1603 /* Step 5b(ii)(4)(g)(i). */
1604 if (!idSet.add(p, id) || !propertyList.append(id)) {
1605 return false;
1609 } else {
1610 replacer = nullptr;
1614 /* Step 6. */
1615 if (space.isObject()) {
1616 RootedObject spaceObj(cx, &space.toObject());
1618 ESClass cls;
1619 if (!JS::GetBuiltinClass(cx, spaceObj, &cls)) {
1620 return false;
1623 if (cls == ESClass::Number) {
1624 double d;
1625 if (!ToNumber(cx, space, &d)) {
1626 return false;
1628 space = NumberValue(d);
1629 } else if (cls == ESClass::String) {
1630 JSString* str = ToStringSlow<CanGC>(cx, space);
1631 if (!str) {
1632 return false;
1634 space = StringValue(str);
1638 StringBuffer gap(cx);
1640 if (space.isNumber()) {
1641 /* Step 7. */
1642 double d;
1643 MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
1644 d = std::min(10.0, d);
1645 if (d >= 1 && !gap.appendN(' ', uint32_t(d))) {
1646 return false;
1648 } else if (space.isString()) {
1649 /* Step 8. */
1650 JSLinearString* str = space.toString()->ensureLinear(cx);
1651 if (!str) {
1652 return false;
1654 size_t len = std::min(size_t(10), str->length());
1655 if (!gap.appendSubstring(str, 0, len)) {
1656 return false;
1658 } else {
1659 /* Step 9. */
1660 MOZ_ASSERT(gap.empty());
1662 if (!gap.empty()) {
1663 whySlow = BailReason::HAVE_SPACE;
1666 Rooted<PlainObject*> wrapper(cx);
1667 RootedId emptyId(cx, NameToId(cx->names().empty_));
1668 if (replacer && replacer->isCallable()) {
1669 // We can skip creating the initial wrapper object if no replacer
1670 // function is present.
1672 /* Step 10. */
1673 wrapper = NewPlainObject(cx);
1674 if (!wrapper) {
1675 return false;
1678 /* Step 11. */
1679 if (!NativeDefineDataProperty(cx, wrapper, emptyId, vp, JSPROP_ENUMERATE)) {
1680 return false;
1684 /* Step 13. */
1685 Rooted<JSAtom*> fastJSON(cx);
1686 if (whySlow == BailReason::NO_REASON) {
1687 MOZ_ASSERT(propertyList.empty());
1688 MOZ_ASSERT(stringifyBehavior != StringifyBehavior::RestrictedSafe);
1689 StringifyContext scx(cx, sb, gap, nullptr, propertyList, false);
1690 if (!PreprocessFastValue(cx, vp.address(), &scx, &whySlow)) {
1691 return false;
1693 if (!vp.isObject()) {
1694 // "Fast" stringify of primitives would create a wrapper object and thus
1695 // be slower than regular stringify.
1696 whySlow = BailReason::PRIMITIVE;
1698 if (whySlow == BailReason::NO_REASON) {
1699 if (!FastSerializeJSONProperty(cx, vp, &scx, &whySlow)) {
1700 return false;
1702 if (whySlow == BailReason::NO_REASON) {
1703 // Fast stringify succeeded!
1704 if (stringifyBehavior != StringifyBehavior::Compare) {
1705 return true;
1707 fastJSON = scx.sb.finishAtom();
1708 if (!fastJSON) {
1709 return false;
1712 scx.sb.clear(); // Preserves allocated space.
1716 if (MOZ_UNLIKELY((stringifyBehavior == StringifyBehavior::FastOnly) &&
1717 (whySlow != BailReason::NO_REASON))) {
1718 JS_ReportErrorASCII(cx, "JSON stringify failed mandatory fast path: %s",
1719 DescribeStringifyBailReason(whySlow));
1720 return false;
1723 // Slow, general path.
1725 StringifyContext scx(cx, sb, gap, replacer, propertyList,
1726 stringifyBehavior == StringifyBehavior::RestrictedSafe);
1727 if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx)) {
1728 return false;
1730 if (IsFilteredValue(vp)) {
1731 return true;
1734 if (!SerializeJSONProperty(cx, vp, &scx)) {
1735 return false;
1738 // For StringBehavior::Compare, when the fast path succeeded.
1739 if (MOZ_UNLIKELY(fastJSON)) {
1740 JSAtom* slowJSON = scx.sb.finishAtom();
1741 if (!slowJSON) {
1742 return false;
1744 if (fastJSON != slowJSON) {
1745 MOZ_CRASH("JSON.stringify mismatch between fast and slow paths");
1747 // Put the JSON back into the StringBuffer for returning.
1748 if (!sb.append(slowJSON)) {
1749 return false;
1753 return true;
1756 /* https://262.ecma-international.org/14.0/#sec-internalizejsonproperty */
1757 static bool InternalizeJSONProperty(
1758 JSContext* cx, HandleObject holder, HandleId name, HandleValue reviver,
1759 MutableHandle<ParseRecordObject> parseRecord, MutableHandleValue vp) {
1760 AutoCheckRecursionLimit recursion(cx);
1761 if (!recursion.check(cx)) {
1762 return false;
1765 /* Step 1. */
1766 RootedValue val(cx);
1767 if (!GetProperty(cx, holder, holder, name, &val)) {
1768 return false;
1771 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1772 RootedObject context(cx);
1773 Rooted<UniquePtr<ParseRecordObject::EntryMap>> entries(cx);
1774 if (JS::Prefs::experimental_json_parse_with_source()) {
1775 // https://tc39.es/proposal-json-parse-with-source/#sec-internalizejsonproperty
1776 bool sameVal = false;
1777 Rooted<Value> parsedValue(cx, parseRecord.get().value);
1778 if (!SameValue(cx, parsedValue, val, &sameVal)) {
1779 return false;
1781 if (!parseRecord.get().isEmpty() && sameVal) {
1782 if (parseRecord.get().parseNode) {
1783 MOZ_ASSERT(!val.isObject());
1784 Rooted<IdValueVector> props(cx, cx);
1785 if (!props.emplaceBack(
1786 IdValuePair(NameToId(cx->names().source),
1787 StringValue(parseRecord.get().parseNode)))) {
1788 return false;
1790 context = NewPlainObjectWithUniqueNames(cx, props);
1791 if (!context) {
1792 return false;
1795 entries = std::move(parseRecord.get().entries);
1797 if (!context) {
1798 context = NewPlainObject(cx);
1799 if (!context) {
1800 return false;
1804 #endif
1806 /* Step 2. */
1807 if (val.isObject()) {
1808 RootedObject obj(cx, &val.toObject());
1810 bool isArray;
1811 if (!IsArray(cx, obj, &isArray)) {
1812 return false;
1815 if (isArray) {
1816 /* Step 2b(i). */
1817 uint32_t length;
1818 if (!GetLengthPropertyForArrayLike(cx, obj, &length)) {
1819 return false;
1822 /* Steps 2b(ii-iii). */
1823 RootedId id(cx);
1824 RootedValue newElement(cx);
1825 for (uint32_t i = 0; i < length; i++) {
1826 if (!CheckForInterrupt(cx)) {
1827 return false;
1830 if (!IndexToId(cx, i, &id)) {
1831 return false;
1834 /* Step 2a(iii)(1). */
1835 Rooted<ParseRecordObject> elementRecord(cx);
1836 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1837 if (entries) {
1838 if (auto entry = entries->lookup(id)) {
1839 elementRecord = std::move(entry->value());
1842 #endif
1843 if (!InternalizeJSONProperty(cx, obj, id, reviver, &elementRecord,
1844 &newElement)) {
1845 return false;
1848 ObjectOpResult ignored;
1849 if (newElement.isUndefined()) {
1850 /* Step 2b(iii)(3). The spec deliberately ignores strict failure. */
1851 if (!DeleteProperty(cx, obj, id, ignored)) {
1852 return false;
1854 } else {
1855 /* Step 2b(iii)(4). The spec deliberately ignores strict failure. */
1856 Rooted<PropertyDescriptor> desc(
1857 cx, PropertyDescriptor::Data(newElement,
1858 {JS::PropertyAttribute::Configurable,
1859 JS::PropertyAttribute::Enumerable,
1860 JS::PropertyAttribute::Writable}));
1861 if (!DefineProperty(cx, obj, id, desc, ignored)) {
1862 return false;
1866 } else {
1867 /* Step 2c(i). */
1868 RootedIdVector keys(cx);
1869 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) {
1870 return false;
1873 /* Step 2c(ii). */
1874 RootedId id(cx);
1875 RootedValue newElement(cx);
1876 for (size_t i = 0, len = keys.length(); i < len; i++) {
1877 if (!CheckForInterrupt(cx)) {
1878 return false;
1881 /* Step 2c(ii)(1). */
1882 id = keys[i];
1883 Rooted<ParseRecordObject> entryRecord(cx);
1884 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1885 if (entries) {
1886 if (auto entry = entries->lookup(id)) {
1887 entryRecord = std::move(entry->value());
1890 #endif
1891 if (!InternalizeJSONProperty(cx, obj, id, reviver, &entryRecord,
1892 &newElement)) {
1893 return false;
1896 ObjectOpResult ignored;
1897 if (newElement.isUndefined()) {
1898 /* Step 2c(ii)(2). The spec deliberately ignores strict failure. */
1899 if (!DeleteProperty(cx, obj, id, ignored)) {
1900 return false;
1902 } else {
1903 /* Step 2c(ii)(3). The spec deliberately ignores strict failure. */
1904 Rooted<PropertyDescriptor> desc(
1905 cx, PropertyDescriptor::Data(newElement,
1906 {JS::PropertyAttribute::Configurable,
1907 JS::PropertyAttribute::Enumerable,
1908 JS::PropertyAttribute::Writable}));
1909 if (!DefineProperty(cx, obj, id, desc, ignored)) {
1910 return false;
1917 /* Step 3. */
1918 RootedString key(cx, IdToString(cx, name));
1919 if (!key) {
1920 return false;
1923 RootedValue keyVal(cx, StringValue(key));
1924 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1925 if (JS::Prefs::experimental_json_parse_with_source()) {
1926 RootedValue contextVal(cx, ObjectValue(*context));
1927 return js::Call(cx, reviver, holder, keyVal, val, contextVal, vp);
1929 #endif
1930 return js::Call(cx, reviver, holder, keyVal, val, vp);
1933 static bool Revive(JSContext* cx, HandleValue reviver,
1934 MutableHandle<ParseRecordObject> pro,
1935 MutableHandleValue vp) {
1936 Rooted<PlainObject*> obj(cx, NewPlainObject(cx));
1937 if (!obj) {
1938 return false;
1941 if (!DefineDataProperty(cx, obj, cx->names().empty_, vp)) {
1942 return false;
1945 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1946 MOZ_ASSERT_IF(JS::Prefs::experimental_json_parse_with_source(),
1947 pro.get().value == vp.get());
1948 #endif
1949 Rooted<jsid> id(cx, NameToId(cx->names().empty_));
1950 return InternalizeJSONProperty(cx, obj, id, reviver, pro, vp);
1953 template <typename CharT>
1954 bool ParseJSON(JSContext* cx, const mozilla::Range<const CharT> chars,
1955 MutableHandleValue vp) {
1956 Rooted<JSONParser<CharT>> parser(cx, cx, chars,
1957 JSONParser<CharT>::ParseType::JSONParse);
1958 return parser.parse(vp);
1961 template <typename CharT>
1962 bool js::ParseJSONWithReviver(JSContext* cx,
1963 const mozilla::Range<const CharT> chars,
1964 HandleValue reviver, MutableHandleValue vp) {
1965 /* https://262.ecma-international.org/14.0/#sec-json.parse steps 2-10. */
1966 Rooted<ParseRecordObject> pro(cx);
1967 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
1968 if (JS::Prefs::experimental_json_parse_with_source() && IsCallable(reviver)) {
1969 Rooted<JSONReviveParser<CharT>> parser(cx, cx, chars);
1970 if (!parser.get().parse(vp, &pro)) {
1971 return false;
1973 } else
1974 #endif
1975 if (!ParseJSON(cx, chars, vp)) {
1976 return false;
1979 /* Steps 11-12. */
1980 if (IsCallable(reviver)) {
1981 return Revive(cx, reviver, &pro, vp);
1983 return true;
1986 template bool js::ParseJSONWithReviver(
1987 JSContext* cx, const mozilla::Range<const Latin1Char> chars,
1988 HandleValue reviver, MutableHandleValue vp);
1990 template bool js::ParseJSONWithReviver(
1991 JSContext* cx, const mozilla::Range<const char16_t> chars,
1992 HandleValue reviver, MutableHandleValue vp);
1994 static bool json_toSource(JSContext* cx, unsigned argc, Value* vp) {
1995 CallArgs args = CallArgsFromVp(argc, vp);
1996 args.rval().setString(cx->names().JSON);
1997 return true;
2000 /* https://262.ecma-international.org/14.0/#sec-json.parse */
2001 static bool json_parse(JSContext* cx, unsigned argc, Value* vp) {
2002 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parse");
2003 CallArgs args = CallArgsFromVp(argc, vp);
2005 /* Step 1. */
2006 JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0])
2007 : cx->names().undefined;
2008 if (!str) {
2009 return false;
2012 JSLinearString* linear = str->ensureLinear(cx);
2013 if (!linear) {
2014 return false;
2017 AutoStableStringChars linearChars(cx);
2018 if (!linearChars.init(cx, linear)) {
2019 return false;
2022 HandleValue reviver = args.get(1);
2024 /* Steps 2-12. */
2025 return linearChars.isLatin1()
2026 ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver,
2027 args.rval())
2028 : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver,
2029 args.rval());
2032 #ifdef ENABLE_RECORD_TUPLE
2033 bool BuildImmutableProperty(JSContext* cx, HandleValue value, HandleId name,
2034 HandleValue reviver,
2035 MutableHandleValue immutableRes) {
2036 MOZ_ASSERT(!name.isSymbol());
2038 // Step 1
2039 if (value.isObject()) {
2040 RootedValue childValue(cx), newElement(cx);
2041 RootedId childName(cx);
2043 // Step 1.a-1.b
2044 if (value.toObject().is<ArrayObject>()) {
2045 Rooted<ArrayObject*> arr(cx, &value.toObject().as<ArrayObject>());
2047 // Step 1.b.iii
2048 uint32_t len = arr->length();
2050 TupleType* tup = TupleType::createUninitialized(cx, len);
2051 if (!tup) {
2052 return false;
2054 immutableRes.setExtendedPrimitive(*tup);
2056 // Step 1.b.iv
2057 for (uint32_t i = 0; i < len; i++) {
2058 // Step 1.b.iv.1
2059 childName.set(PropertyKey::Int(i));
2061 // Step 1.b.iv.2
2062 if (!GetProperty(cx, arr, value, childName, &childValue)) {
2063 return false;
2066 // Step 1.b.iv.3
2067 if (!BuildImmutableProperty(cx, childValue, childName, reviver,
2068 &newElement)) {
2069 return false;
2071 MOZ_ASSERT(newElement.isPrimitive());
2073 // Step 1.b.iv.5
2074 if (!tup->initializeNextElement(cx, newElement)) {
2075 return false;
2079 // Step 1.b.v
2080 tup->finishInitialization(cx);
2081 } else {
2082 RootedObject obj(cx, &value.toObject());
2084 // Step 1.c.i - We only get the property keys rather than the
2085 // entries, but the difference is not observable from user code
2086 // because `obj` is a plan object not exposed externally
2087 RootedIdVector props(cx);
2088 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &props)) {
2089 return false;
2092 RecordType* rec = RecordType::createUninitialized(cx, props.length());
2093 if (!rec) {
2094 return false;
2096 immutableRes.setExtendedPrimitive(*rec);
2098 for (uint32_t i = 0; i < props.length(); i++) {
2099 // Step 1.c.iii.1
2100 childName.set(props[i]);
2102 // Step 1.c.iii.2
2103 if (!GetProperty(cx, obj, value, childName, &childValue)) {
2104 return false;
2107 // Step 1.c.iii.3
2108 if (!BuildImmutableProperty(cx, childValue, childName, reviver,
2109 &newElement)) {
2110 return false;
2112 MOZ_ASSERT(newElement.isPrimitive());
2114 // Step 1.c.iii.5
2115 if (!newElement.isUndefined()) {
2116 // Step 1.c.iii.5.a-b
2117 rec->initializeNextProperty(cx, childName, newElement);
2121 // Step 1.c.iv
2122 rec->finishInitialization(cx);
2124 } else {
2125 // Step 2.a
2126 immutableRes.set(value);
2129 // Step 3
2130 if (IsCallable(reviver)) {
2131 RootedValue keyVal(cx, StringValue(IdToString(cx, name)));
2133 // Step 3.a
2134 if (!Call(cx, reviver, UndefinedHandleValue, keyVal, immutableRes,
2135 immutableRes)) {
2136 return false;
2139 // Step 3.b
2140 if (!immutableRes.isPrimitive()) {
2141 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
2142 JSMSG_RECORD_TUPLE_NO_OBJECT);
2143 return false;
2147 return true;
2150 static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) {
2151 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parseImmutable");
2152 CallArgs args = CallArgsFromVp(argc, vp);
2154 /* Step 1. */
2155 JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0])
2156 : cx->names().undefined;
2157 if (!str) {
2158 return false;
2161 JSLinearString* linear = str->ensureLinear(cx);
2162 if (!linear) {
2163 return false;
2166 AutoStableStringChars linearChars(cx);
2167 if (!linearChars.init(cx, linear)) {
2168 return false;
2171 HandleValue reviver = args.get(1);
2172 RootedValue unfiltered(cx);
2174 if (linearChars.isLatin1()) {
2175 if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered)) {
2176 return false;
2178 } else {
2179 if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered)) {
2180 return false;
2184 RootedId id(cx, NameToId(cx->names().empty_));
2185 return BuildImmutableProperty(cx, unfiltered, id, reviver, args.rval());
2187 #endif
2189 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
2190 /* https://tc39.es/proposal-json-parse-with-source/#sec-json.israwjson */
2191 static bool json_isRawJSON(JSContext* cx, unsigned argc, Value* vp) {
2192 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "isRawJSON");
2193 CallArgs args = CallArgsFromVp(argc, vp);
2195 /* Step 1. */
2196 if (args.get(0).isObject()) {
2197 Rooted<JSObject*> obj(cx, &args[0].toObject());
2198 # ifdef DEBUG
2199 if (obj->is<RawJSONObject>()) {
2200 bool objIsFrozen = false;
2201 MOZ_ASSERT(js::TestIntegrityLevel(cx, obj, IntegrityLevel::Frozen,
2202 &objIsFrozen));
2203 MOZ_ASSERT(objIsFrozen);
2205 # endif // DEBUG
2206 args.rval().setBoolean(obj->is<RawJSONObject>() ||
2207 obj->canUnwrapAs<RawJSONObject>());
2208 return true;
2211 /* Step 2. */
2212 args.rval().setBoolean(false);
2213 return true;
2216 static inline bool IsJSONWhitespace(char16_t ch) {
2217 return ch == '\t' || ch == '\n' || ch == '\r' || ch == ' ';
2220 /* https://tc39.es/proposal-json-parse-with-source/#sec-json.rawjson */
2221 static bool json_rawJSON(JSContext* cx, unsigned argc, Value* vp) {
2222 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "rawJSON");
2223 CallArgs args = CallArgsFromVp(argc, vp);
2225 /* Step 1. */
2226 JSString* jsonString = ToString<CanGC>(cx, args.get(0));
2227 if (!jsonString) {
2228 return false;
2231 Rooted<JSLinearString*> linear(cx, jsonString->ensureLinear(cx));
2232 if (!linear) {
2233 return false;
2236 AutoStableStringChars linearChars(cx);
2237 if (!linearChars.init(cx, linear)) {
2238 return false;
2241 /* Step 2. */
2242 if (linear->empty()) {
2243 JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
2244 JSMSG_JSON_RAW_EMPTY);
2245 return false;
2247 if (IsJSONWhitespace(linear->latin1OrTwoByteChar(0)) ||
2248 IsJSONWhitespace(linear->latin1OrTwoByteChar(linear->length() - 1))) {
2249 JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
2250 JSMSG_JSON_RAW_WHITESPACE);
2251 return false;
2254 /* Step 3. */
2255 RootedValue parsedValue(cx);
2256 if (linearChars.isLatin1()) {
2257 if (!ParseJSON(cx, linearChars.latin1Range(), &parsedValue)) {
2258 return false;
2260 } else {
2261 if (!ParseJSON(cx, linearChars.twoByteRange(), &parsedValue)) {
2262 return false;
2266 if (parsedValue.isObject()) {
2267 JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr,
2268 JSMSG_JSON_RAW_ARRAY_OR_OBJECT);
2269 return false;
2272 /* Steps 4-6. */
2273 Rooted<RawJSONObject*> obj(cx, RawJSONObject::create(cx, linear));
2274 if (!obj) {
2275 return false;
2278 /* Step 7. */
2279 if (!js::FreezeObject(cx, obj)) {
2280 return false;
2283 args.rval().setObject(*obj);
2284 return true;
2286 #endif // ENABLE_JSON_PARSE_WITH_SOURCE
2288 /* https://262.ecma-international.org/14.0/#sec-json.stringify */
2289 bool json_stringify(JSContext* cx, unsigned argc, Value* vp) {
2290 AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "stringify");
2291 CallArgs args = CallArgsFromVp(argc, vp);
2293 RootedObject replacer(cx,
2294 args.get(1).isObject() ? &args[1].toObject() : nullptr);
2295 RootedValue value(cx, args.get(0));
2296 RootedValue space(cx, args.get(2));
2298 #ifdef DEBUG
2299 StringifyBehavior behavior = StringifyBehavior::Compare;
2300 #else
2301 StringifyBehavior behavior = StringifyBehavior::Normal;
2302 #endif
2304 JSStringBuilder sb(cx);
2305 if (!Stringify(cx, &value, replacer, space, sb, behavior)) {
2306 return false;
2309 // XXX This can never happen to nsJSON.cpp, but the JSON object
2310 // needs to support returning undefined. So this is a little awkward
2311 // for the API, because we want to support streaming writers.
2312 if (!sb.empty()) {
2313 JSString* str = sb.finishString();
2314 if (!str) {
2315 return false;
2317 args.rval().setString(str);
2318 } else {
2319 args.rval().setUndefined();
2322 return true;
2325 static const JSFunctionSpec json_static_methods[] = {
2326 JS_FN("toSource", json_toSource, 0, 0),
2327 JS_FN("parse", json_parse, 2, 0),
2328 JS_FN("stringify", json_stringify, 3, 0),
2329 #ifdef ENABLE_RECORD_TUPLE
2330 JS_FN("parseImmutable", json_parseImmutable, 2, 0),
2331 #endif
2332 #ifdef ENABLE_JSON_PARSE_WITH_SOURCE
2333 JS_FN("isRawJSON", json_isRawJSON, 1, 0),
2334 JS_FN("rawJSON", json_rawJSON, 1, 0),
2335 #endif
2336 JS_FS_END};
2338 static const JSPropertySpec json_static_properties[] = {
2339 JS_STRING_SYM_PS(toStringTag, "JSON", JSPROP_READONLY), JS_PS_END};
2341 static JSObject* CreateJSONObject(JSContext* cx, JSProtoKey key) {
2342 RootedObject proto(cx, &cx->global()->getObjectPrototype());
2343 return NewTenuredObjectWithGivenProto(cx, &JSONClass, proto);
2346 static const ClassSpec JSONClassSpec = {
2347 CreateJSONObject, nullptr, json_static_methods, json_static_properties};
2349 const JSClass js::JSONClass = {"JSON", JSCLASS_HAS_CACHED_PROTO(JSProto_JSON),
2350 JS_NULL_CLASS_OPS, &JSONClassSpec};