Source code

Revision control

Other Tools

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/. */
6
7
#include "builtin/JSON.h"
8
9
#include "mozilla/CheckedInt.h"
10
#include "mozilla/FloatingPoint.h"
11
#include "mozilla/Range.h"
12
#include "mozilla/ScopeExit.h"
13
14
#include <algorithm>
15
16
#include "jsnum.h"
17
#include "jstypes.h"
18
19
#include "builtin/Array.h"
20
#include "builtin/BigInt.h"
21
#include "js/PropertySpec.h"
22
#include "js/StableStringChars.h"
23
#include "util/StringBuffer.h"
24
#include "vm/Interpreter.h"
25
#include "vm/JSAtom.h"
26
#include "vm/JSContext.h"
27
#include "vm/JSObject.h"
28
#include "vm/JSONParser.h"
29
30
#include "builtin/Array-inl.h"
31
#include "builtin/Boolean-inl.h"
32
#include "vm/JSAtom-inl.h"
33
#include "vm/NativeObject-inl.h"
34
35
using namespace js;
36
37
using mozilla::CheckedInt;
38
using mozilla::IsFinite;
39
using mozilla::Maybe;
40
using mozilla::RangedPtr;
41
42
using JS::AutoStableStringChars;
43
44
const JSClass js::JSONClass = {js_JSON_str,
45
JSCLASS_HAS_CACHED_PROTO(JSProto_JSON)};
46
47
/* ES5 15.12.3 Quote.
48
* Requires that the destination has enough space allocated for src after
49
* escaping (that is, `2 + 6 * (srcEnd - srcBegin)` characters).
50
*/
51
template <typename SrcCharT, typename DstCharT>
52
static MOZ_ALWAYS_INLINE RangedPtr<DstCharT> InfallibleQuote(
53
RangedPtr<const SrcCharT> srcBegin, RangedPtr<const SrcCharT> srcEnd,
54
RangedPtr<DstCharT> dstPtr) {
55
// Maps characters < 256 to the value that must follow the '\\' in the quoted
56
// string. Entries with 'u' are handled as \\u00xy, and entries with 0 are not
57
// escaped in any way. Characters >= 256 are all assumed to be unescaped.
58
static const Latin1Char escapeLookup[256] = {
59
// clang-format off
60
'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'b', 't',
61
'n', 'u', 'f', 'r', 'u', 'u', 'u', 'u', 'u', 'u',
62
'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u',
63
'u', 'u', 0, 0, '\"', 0, 0, 0, 0, 0,
64
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
65
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
66
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
67
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
68
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
69
0, 0, '\\', // rest are all zeros
70
// clang-format on
71
};
72
73
/* Step 1. */
74
*dstPtr++ = '"';
75
76
auto ToLowerHex = [](uint8_t u) {
77
MOZ_ASSERT(u <= 0xF);
78
return "0123456789abcdef"[u];
79
};
80
81
/* Step 2. */
82
while (srcBegin != srcEnd) {
83
const SrcCharT c = *srcBegin++;
84
85
// Handle the Latin-1 cases.
86
if (MOZ_LIKELY(c < sizeof(escapeLookup))) {
87
Latin1Char escaped = escapeLookup[c];
88
89
// Directly copy non-escaped code points.
90
if (escaped == 0) {
91
*dstPtr++ = c;
92
continue;
93
}
94
95
// Escape the rest, elaborating Unicode escapes when needed.
96
*dstPtr++ = '\\';
97
*dstPtr++ = escaped;
98
if (escaped == 'u') {
99
*dstPtr++ = '0';
100
*dstPtr++ = '0';
101
102
uint8_t x = c >> 4;
103
MOZ_ASSERT(x < 10);
104
*dstPtr++ = '0' + x;
105
106
*dstPtr++ = ToLowerHex(c & 0xF);
107
}
108
109
continue;
110
}
111
112
// Non-ASCII non-surrogates are directly copied.
113
if (!unicode::IsSurrogate(c)) {
114
*dstPtr++ = c;
115
continue;
116
}
117
118
// So too for complete surrogate pairs.
119
if (MOZ_LIKELY(unicode::IsLeadSurrogate(c) && srcBegin < srcEnd &&
120
unicode::IsTrailSurrogate(*srcBegin))) {
121
*dstPtr++ = c;
122
*dstPtr++ = *srcBegin++;
123
continue;
124
}
125
126
// But lone surrogates are Unicode-escaped.
127
char32_t as32 = char32_t(c);
128
*dstPtr++ = '\\';
129
*dstPtr++ = 'u';
130
*dstPtr++ = ToLowerHex(as32 >> 12);
131
*dstPtr++ = ToLowerHex((as32 >> 8) & 0xF);
132
*dstPtr++ = ToLowerHex((as32 >> 4) & 0xF);
133
*dstPtr++ = ToLowerHex(as32 & 0xF);
134
}
135
136
/* Steps 3-4. */
137
*dstPtr++ = '"';
138
return dstPtr;
139
}
140
141
template <typename SrcCharT, typename DstCharT>
142
static size_t QuoteHelper(const JSLinearString& linear, StringBuffer& sb,
143
size_t sbOffset) {
144
size_t len = linear.length();
145
146
JS::AutoCheckCannotGC nogc;
147
RangedPtr<const SrcCharT> srcBegin{linear.chars<SrcCharT>(nogc), len};
148
RangedPtr<DstCharT> dstBegin{sb.begin<DstCharT>(), sb.begin<DstCharT>(),
149
sb.end<DstCharT>()};
150
RangedPtr<DstCharT> dstEnd =
151
InfallibleQuote(srcBegin, srcBegin + len, dstBegin + sbOffset);
152
153
return dstEnd - dstBegin;
154
}
155
156
static bool Quote(JSContext* cx, StringBuffer& sb, JSString* str) {
157
JSLinearString* linear = str->ensureLinear(cx);
158
if (!linear) {
159
return false;
160
}
161
162
if (linear->hasTwoByteChars() && !sb.ensureTwoByteChars()) {
163
return false;
164
}
165
166
// We resize the backing buffer to the maximum size we could possibly need,
167
// write the escaped string into it, and shrink it back to the size we ended
168
// up needing.
169
170
size_t len = linear->length();
171
size_t sbInitialLen = sb.length();
172
173
CheckedInt<size_t> reservedLen = CheckedInt<size_t>(len) * 6 + 2;
174
if (MOZ_UNLIKELY(!reservedLen.isValid())) {
175
ReportAllocationOverflow(cx);
176
return false;
177
}
178
179
if (!sb.growByUninitialized(reservedLen.value())) {
180
return false;
181
}
182
183
size_t newSize;
184
185
if (linear->hasTwoByteChars()) {
186
newSize = QuoteHelper<char16_t, char16_t>(*linear, sb, sbInitialLen);
187
} else if (sb.isUnderlyingBufferLatin1()) {
188
newSize = QuoteHelper<Latin1Char, Latin1Char>(*linear, sb, sbInitialLen);
189
} else {
190
newSize = QuoteHelper<Latin1Char, char16_t>(*linear, sb, sbInitialLen);
191
}
192
193
sb.shrinkTo(newSize);
194
195
return true;
196
}
197
198
namespace {
199
200
using ObjectVector = GCVector<JSObject*, 8>;
201
202
class StringifyContext {
203
public:
204
StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
205
HandleObject replacer, const RootedIdVector& propertyList,
206
bool maybeSafely)
207
: sb(sb),
208
gap(gap),
209
replacer(cx, replacer),
210
stack(cx, ObjectVector(cx)),
211
propertyList(propertyList),
212
depth(0),
213
maybeSafely(maybeSafely) {
214
MOZ_ASSERT_IF(maybeSafely, !replacer);
215
MOZ_ASSERT_IF(maybeSafely, gap.empty());
216
}
217
218
StringBuffer& sb;
219
const StringBuffer& gap;
220
RootedObject replacer;
221
Rooted<ObjectVector> stack;
222
const RootedIdVector& propertyList;
223
uint32_t depth;
224
bool maybeSafely;
225
};
226
227
} /* anonymous namespace */
228
229
static bool Str(JSContext* cx, const Value& v, StringifyContext* scx);
230
231
static bool WriteIndent(StringifyContext* scx, uint32_t limit) {
232
if (!scx->gap.empty()) {
233
if (!scx->sb.append('\n')) {
234
return false;
235
}
236
237
if (scx->gap.isUnderlyingBufferLatin1()) {
238
for (uint32_t i = 0; i < limit; i++) {
239
if (!scx->sb.append(scx->gap.rawLatin1Begin(),
240
scx->gap.rawLatin1End())) {
241
return false;
242
}
243
}
244
} else {
245
for (uint32_t i = 0; i < limit; i++) {
246
if (!scx->sb.append(scx->gap.rawTwoByteBegin(),
247
scx->gap.rawTwoByteEnd())) {
248
return false;
249
}
250
}
251
}
252
}
253
254
return true;
255
}
256
257
namespace {
258
259
template <typename KeyType>
260
class KeyStringifier {};
261
262
template <>
263
class KeyStringifier<uint32_t> {
264
public:
265
static JSString* toString(JSContext* cx, uint32_t index) {
266
return IndexToString(cx, index);
267
}
268
};
269
270
template <>
271
class KeyStringifier<HandleId> {
272
public:
273
static JSString* toString(JSContext* cx, HandleId id) {
274
return IdToString(cx, id);
275
}
276
};
277
278
} /* anonymous namespace */
279
280
/*
281
* ES5 15.12.3 Str, steps 2-4, extracted to enable preprocessing of property
282
* values when stringifying objects in JO.
283
*/
284
template <typename KeyType>
285
static bool PreprocessValue(JSContext* cx, HandleObject holder, KeyType key,
286
MutableHandleValue vp, StringifyContext* scx) {
287
// We don't want to do any preprocessing here if scx->maybeSafely,
288
// since the stuff we do here can have side-effects.
289
if (scx->maybeSafely) {
290
return true;
291
}
292
293
RootedString keyStr(cx);
294
295
// Step 2. Modified by BigInt spec 6.1 to check for a toJSON method on the
296
// BigInt prototype when the value is a BigInt, and to pass the BigInt
297
// primitive value as receiver.
298
if (vp.isObject() || vp.isBigInt()) {
299
RootedValue toJSON(cx);
300
RootedObject obj(cx, JS::ToObject(cx, vp));
301
if (!obj) {
302
return false;
303
}
304
305
if (!GetProperty(cx, obj, vp, cx->names().toJSON, &toJSON)) {
306
return false;
307
}
308
309
if (IsCallable(toJSON)) {
310
keyStr = KeyStringifier<KeyType>::toString(cx, key);
311
if (!keyStr) {
312
return false;
313
}
314
315
RootedValue arg0(cx, StringValue(keyStr));
316
if (!js::Call(cx, toJSON, vp, arg0, vp)) {
317
return false;
318
}
319
}
320
}
321
322
/* Step 3. */
323
if (scx->replacer && scx->replacer->isCallable()) {
324
MOZ_ASSERT(holder != nullptr,
325
"holder object must be present when replacer is callable");
326
327
if (!keyStr) {
328
keyStr = KeyStringifier<KeyType>::toString(cx, key);
329
if (!keyStr) {
330
return false;
331
}
332
}
333
334
RootedValue arg0(cx, StringValue(keyStr));
335
RootedValue replacerVal(cx, ObjectValue(*scx->replacer));
336
if (!js::Call(cx, replacerVal, holder, arg0, vp, vp)) {
337
return false;
338
}
339
}
340
341
/* Step 4. */
342
if (vp.get().isObject()) {
343
RootedObject obj(cx, &vp.get().toObject());
344
345
ESClass cls;
346
if (!GetBuiltinClass(cx, obj, &cls)) {
347
return false;
348
}
349
350
if (cls == ESClass::Number) {
351
double d;
352
if (!ToNumber(cx, vp, &d)) {
353
return false;
354
}
355
vp.setNumber(d);
356
} else if (cls == ESClass::String) {
357
JSString* str = ToStringSlow<CanGC>(cx, vp);
358
if (!str) {
359
return false;
360
}
361
vp.setString(str);
362
} else if (cls == ESClass::Boolean) {
363
if (!Unbox(cx, obj, vp)) {
364
return false;
365
}
366
} else if (cls == ESClass::BigInt) {
367
if (!Unbox(cx, obj, vp)) {
368
return false;
369
}
370
}
371
}
372
373
return true;
374
}
375
376
/*
377
* Determines whether a value which has passed by ES5 150.2.3 Str steps 1-4's
378
* gauntlet will result in Str returning |undefined|. This function is used to
379
* properly omit properties resulting in such values when stringifying objects,
380
* while properly stringifying such properties as null when they're encountered
381
* in arrays.
382
*/
383
static inline bool IsFilteredValue(const Value& v) {
384
return v.isUndefined() || v.isSymbol() || IsCallable(v);
385
}
386
387
class CycleDetector {
388
public:
389
CycleDetector(StringifyContext* scx, HandleObject obj)
390
: stack_(&scx->stack), obj_(obj), appended_(false) {}
391
392
MOZ_ALWAYS_INLINE bool foundCycle(JSContext* cx) {
393
JSObject* obj = obj_;
394
for (JSObject* obj2 : stack_) {
395
if (MOZ_UNLIKELY(obj == obj2)) {
396
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
397
JSMSG_JSON_CYCLIC_VALUE);
398
return false;
399
}
400
}
401
appended_ = stack_.append(obj);
402
return appended_;
403
}
404
405
~CycleDetector() {
406
if (MOZ_LIKELY(appended_)) {
407
MOZ_ASSERT(stack_.back() == obj_);
408
stack_.popBack();
409
}
410
}
411
412
private:
413
MutableHandle<ObjectVector> stack_;
414
HandleObject obj_;
415
bool appended_;
416
};
417
418
/* ES5 15.12.3 JO. */
419
static bool JO(JSContext* cx, HandleObject obj, StringifyContext* scx) {
420
/*
421
* This method implements the JO algorithm in ES5 15.12.3, but:
422
*
423
* * The algorithm is somewhat reformulated to allow the final string to
424
* be streamed into a single buffer, rather than be created and copied
425
* into place incrementally as the ES5 algorithm specifies it. This
426
* requires moving portions of the Str call in 8a into this algorithm
427
* (and in JA as well).
428
*/
429
430
MOZ_ASSERT_IF(scx->maybeSafely, obj->is<PlainObject>());
431
432
/* Steps 1-2, 11. */
433
CycleDetector detect(scx, obj);
434
if (!detect.foundCycle(cx)) {
435
return false;
436
}
437
438
if (!scx->sb.append('{')) {
439
return false;
440
}
441
442
/* Steps 5-7. */
443
Maybe<RootedIdVector> ids;
444
const RootedIdVector* props;
445
if (scx->replacer && !scx->replacer->isCallable()) {
446
// NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
447
// might have been a revocable proxy to an array. Such a proxy
448
// satisfies |IsArray|, but any side effect of JSON.stringify
449
// could revoke the proxy so that |!IsArray(scx->replacer)|. See
450
// bug 1196497.
451
props = &scx->propertyList;
452
} else {
453
MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0);
454
ids.emplace(cx);
455
if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr())) {
456
return false;
457
}
458
props = ids.ptr();
459
}
460
461
/* My kingdom for not-quite-initialized-from-the-start references. */
462
const RootedIdVector& propertyList = *props;
463
464
/* Steps 8-10, 13. */
465
bool wroteMember = false;
466
RootedId id(cx);
467
for (size_t i = 0, len = propertyList.length(); i < len; i++) {
468
if (!CheckForInterrupt(cx)) {
469
return false;
470
}
471
472
/*
473
* Steps 8a-8b. Note that the call to Str is broken up into 1) getting
474
* the property; 2) processing for toJSON, calling the replacer, and
475
* handling boxed Number/String/Boolean objects; 3) filtering out
476
* values which process to |undefined|, and 4) stringifying all values
477
* which pass the filter.
478
*/
479
id = propertyList[i];
480
RootedValue outputValue(cx);
481
#ifdef DEBUG
482
if (scx->maybeSafely) {
483
RootedNativeObject nativeObj(cx, &obj->as<NativeObject>());
484
Rooted<PropertyResult> prop(cx);
485
if (!NativeLookupOwnPropertyNoResolve(cx, nativeObj, id, &prop)) {
486
return false;
487
}
488
MOZ_ASSERT(prop && prop.isNativeProperty() &&
489
prop.shape()->isDataDescriptor());
490
}
491
#endif // DEBUG
492
if (!GetProperty(cx, obj, obj, id, &outputValue)) {
493
return false;
494
}
495
if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx)) {
496
return false;
497
}
498
if (IsFilteredValue(outputValue)) {
499
continue;
500
}
501
502
/* Output a comma unless this is the first member to write. */
503
if (wroteMember && !scx->sb.append(',')) {
504
return false;
505
}
506
wroteMember = true;
507
508
if (!WriteIndent(scx, scx->depth)) {
509
return false;
510
}
511
512
JSString* s = IdToString(cx, id);
513
if (!s) {
514
return false;
515
}
516
517
if (!Quote(cx, scx->sb, s) || !scx->sb.append(':') ||
518
!(scx->gap.empty() || scx->sb.append(' ')) ||
519
!Str(cx, outputValue, scx)) {
520
return false;
521
}
522
}
523
524
if (wroteMember && !WriteIndent(scx, scx->depth - 1)) {
525
return false;
526
}
527
528
return scx->sb.append('}');
529
}
530
531
/* ES5 15.12.3 JA. */
532
static bool JA(JSContext* cx, HandleObject obj, StringifyContext* scx) {
533
/*
534
* This method implements the JA algorithm in ES5 15.12.3, but:
535
*
536
* * The algorithm is somewhat reformulated to allow the final string to
537
* be streamed into a single buffer, rather than be created and copied
538
* into place incrementally as the ES5 algorithm specifies it. This
539
* requires moving portions of the Str call in 8a into this algorithm
540
* (and in JO as well).
541
*/
542
543
/* Steps 1-2, 11. */
544
CycleDetector detect(scx, obj);
545
if (!detect.foundCycle(cx)) {
546
return false;
547
}
548
549
if (!scx->sb.append('[')) {
550
return false;
551
}
552
553
/* Step 6. */
554
uint32_t length;
555
if (!GetLengthProperty(cx, obj, &length)) {
556
return false;
557
}
558
559
/* Steps 7-10. */
560
if (length != 0) {
561
/* Steps 4, 10b(i). */
562
if (!WriteIndent(scx, scx->depth)) {
563
return false;
564
}
565
566
/* Steps 7-10. */
567
RootedValue outputValue(cx);
568
for (uint32_t i = 0; i < length; i++) {
569
if (!CheckForInterrupt(cx)) {
570
return false;
571
}
572
573
/*
574
* Steps 8a-8c. Again note how the call to the spec's Str method
575
* is broken up into getting the property, running it past toJSON
576
* and the replacer and maybe unboxing, and interpreting some
577
* values as |null| in separate steps.
578
*/
579
#ifdef DEBUG
580
if (scx->maybeSafely) {
581
/*
582
* Trying to do a JS_AlreadyHasOwnElement runs the risk of
583
* hitting OOM on jsid creation. Let's just assert sanity for
584
* small enough indices.
585
*/
586
MOZ_ASSERT(obj->is<ArrayObject>());
587
MOZ_ASSERT(obj->is<NativeObject>());
588
RootedNativeObject nativeObj(cx, &obj->as<NativeObject>());
589
if (i <= JSID_INT_MAX) {
590
MOZ_ASSERT(
591
nativeObj->containsDenseElement(i) != nativeObj->isIndexed(),
592
"the array must either be small enough to remain "
593
"fully dense (and otherwise un-indexed), *or* "
594
"all its initially-dense elements were sparsified "
595
"and the object is indexed");
596
} else {
597
MOZ_ASSERT(nativeObj->isIndexed());
598
}
599
}
600
#endif
601
if (!GetElement(cx, obj, i, &outputValue)) {
602
return false;
603
}
604
if (!PreprocessValue(cx, obj, i, &outputValue, scx)) {
605
return false;
606
}
607
if (IsFilteredValue(outputValue)) {
608
if (!scx->sb.append("null")) {
609
return false;
610
}
611
} else {
612
if (!Str(cx, outputValue, scx)) {
613
return false;
614
}
615
}
616
617
/* Steps 3, 4, 10b(i). */
618
if (i < length - 1) {
619
if (!scx->sb.append(',')) {
620
return false;
621
}
622
if (!WriteIndent(scx, scx->depth)) {
623
return false;
624
}
625
}
626
}
627
628
/* Step 10(b)(iii). */
629
if (!WriteIndent(scx, scx->depth - 1)) {
630
return false;
631
}
632
}
633
634
return scx->sb.append(']');
635
}
636
637
static bool Str(JSContext* cx, const Value& v, StringifyContext* scx) {
638
/* Step 11 must be handled by the caller. */
639
MOZ_ASSERT(!IsFilteredValue(v));
640
641
if (!CheckRecursionLimit(cx)) {
642
return false;
643
}
644
645
/*
646
* This method implements the Str algorithm in ES5 15.12.3, but:
647
*
648
* * We move property retrieval (step 1) into callers to stream the
649
* stringification process and avoid constantly copying strings.
650
* * We move the preprocessing in steps 2-4 into a helper function to
651
* allow both JO and JA to use this method. While JA could use it
652
* without this move, JO must omit any |undefined|-valued property per
653
* so it can't stream out a value using the Str method exactly as
654
* defined by ES5.
655
* * We move step 11 into callers, again to ease streaming.
656
*/
657
658
/* Step 8. */
659
if (v.isString()) {
660
return Quote(cx, scx->sb, v.toString());
661
}
662
663
/* Step 5. */
664
if (v.isNull()) {
665
return scx->sb.append("null");
666
}
667
668
/* Steps 6-7. */
669
if (v.isBoolean()) {
670
return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false");
671
}
672
673
/* Step 9. */
674
if (v.isNumber()) {
675
if (v.isDouble()) {
676
if (!IsFinite(v.toDouble())) {
677
MOZ_ASSERT(!scx->maybeSafely,
678
"input JS::ToJSONMaybeSafely must not include "
679
"reachable non-finite numbers");
680
return scx->sb.append("null");
681
}
682
}
683
684
return NumberValueToStringBuffer(cx, v, scx->sb);
685
}
686
687
/* Step 10 in the BigInt proposal. */
688
if (v.isBigInt()) {
689
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
690
JSMSG_BIGINT_NOT_SERIALIZABLE);
691
return false;
692
}
693
694
/* Step 10. */
695
MOZ_ASSERT(v.isObject());
696
RootedObject obj(cx, &v.toObject());
697
698
MOZ_ASSERT(
699
!scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(),
700
"input to JS::ToJSONMaybeSafely must not include reachable "
701
"objects that are neither arrays nor plain objects");
702
703
scx->depth++;
704
auto dec = mozilla::MakeScopeExit([&] { scx->depth--; });
705
706
bool isArray;
707
if (!IsArray(cx, obj, &isArray)) {
708
return false;
709
}
710
711
return isArray ? JA(cx, obj, scx) : JO(cx, obj, scx);
712
}
713
714
/* ES6 24.3.2. */
715
bool js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_,
716
const Value& space_, StringBuffer& sb,
717
StringifyBehavior stringifyBehavior) {
718
RootedObject replacer(cx, replacer_);
719
RootedValue space(cx, space_);
720
721
MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
722
space.isNull());
723
MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe,
724
vp.isObject());
725
/**
726
* This uses MOZ_ASSERT, since it's actually asserting something jsapi
727
* consumers could get wrong, so needs a better error message.
728
*/
729
MOZ_ASSERT(stringifyBehavior == StringifyBehavior::Normal ||
730
vp.toObject().is<PlainObject>() ||
731
vp.toObject().is<ArrayObject>(),
732
"input to JS::ToJSONMaybeSafely must be a plain object or array");
733
734
/* Step 4. */
735
RootedIdVector propertyList(cx);
736
if (replacer) {
737
bool isArray;
738
if (replacer->isCallable()) {
739
/* Step 4a(i): use replacer to transform values. */
740
} else if (!IsArray(cx, replacer, &isArray)) {
741
return false;
742
} else if (isArray) {
743
/* Step 4b(iii). */
744
745
/* Step 4b(iii)(2-3). */
746
uint32_t len;
747
if (!GetLengthProperty(cx, replacer, &len)) {
748
return false;
749
}
750
751
// Cap the initial size to a moderately small value. This avoids
752
// ridiculous over-allocation if an array with bogusly-huge length
753
// is passed in. If we end up having to add elements past this
754
// size, the set will naturally resize to accommodate them.
755
const uint32_t MaxInitialSize = 32;
756
Rooted<GCHashSet<jsid>> idSet(
757
cx, GCHashSet<jsid>(cx, std::min(len, MaxInitialSize)));
758
759
/* Step 4b(iii)(4). */
760
uint32_t k = 0;
761
762
/* Step 4b(iii)(5). */
763
RootedValue item(cx);
764
for (; k < len; k++) {
765
if (!CheckForInterrupt(cx)) {
766
return false;
767
}
768
769
/* Step 4b(iii)(5)(a-b). */
770
if (!GetElement(cx, replacer, k, &item)) {
771
return false;
772
}
773
774
/* Step 4b(iii)(5)(c-g). */
775
if (!item.isNumber() && !item.isString()) {
776
ESClass cls;
777
if (!GetClassOfValue(cx, item, &cls)) {
778
return false;
779
}
780
781
if (cls != ESClass::String && cls != ESClass::Number) {
782
continue;
783
}
784
}
785
786
RootedId id(cx);
787
if (!ValueToId<CanGC>(cx, item, &id)) {
788
return false;
789
}
790
791
/* Step 4b(iii)(5)(g). */
792
auto p = idSet.lookupForAdd(id);
793
if (!p) {
794
/* Step 4b(iii)(5)(g)(i). */
795
if (!idSet.add(p, id) || !propertyList.append(id)) {
796
return false;
797
}
798
}
799
}
800
} else {
801
replacer = nullptr;
802
}
803
}
804
805
/* Step 5. */
806
if (space.isObject()) {
807
RootedObject spaceObj(cx, &space.toObject());
808
809
ESClass cls;
810
if (!GetBuiltinClass(cx, spaceObj, &cls)) {
811
return false;
812
}
813
814
if (cls == ESClass::Number) {
815
double d;
816
if (!ToNumber(cx, space, &d)) {
817
return false;
818
}
819
space = NumberValue(d);
820
} else if (cls == ESClass::String) {
821
JSString* str = ToStringSlow<CanGC>(cx, space);
822
if (!str) {
823
return false;
824
}
825
space = StringValue(str);
826
}
827
}
828
829
StringBuffer gap(cx);
830
831
if (space.isNumber()) {
832
/* Step 6. */
833
double d;
834
MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
835
d = std::min(10.0, d);
836
if (d >= 1 && !gap.appendN(' ', uint32_t(d))) {
837
return false;
838
}
839
} else if (space.isString()) {
840
/* Step 7. */
841
JSLinearString* str = space.toString()->ensureLinear(cx);
842
if (!str) {
843
return false;
844
}
845
size_t len = std::min(size_t(10), str->length());
846
if (!gap.appendSubstring(str, 0, len)) {
847
return false;
848
}
849
} else {
850
/* Step 8. */
851
MOZ_ASSERT(gap.empty());
852
}
853
854
RootedPlainObject wrapper(cx);
855
RootedId emptyId(cx, NameToId(cx->names().empty));
856
if (replacer && replacer->isCallable()) {
857
// We can skip creating the initial wrapper object if no replacer
858
// function is present.
859
860
/* Step 9. */
861
wrapper = NewBuiltinClassInstance<PlainObject>(cx);
862
if (!wrapper) {
863
return false;
864
}
865
866
/* Steps 10-11. */
867
if (!NativeDefineDataProperty(cx, wrapper, emptyId, vp, JSPROP_ENUMERATE)) {
868
return false;
869
}
870
}
871
872
/* Step 12. */
873
StringifyContext scx(cx, sb, gap, replacer, propertyList,
874
stringifyBehavior == StringifyBehavior::RestrictedSafe);
875
if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx)) {
876
return false;
877
}
878
if (IsFilteredValue(vp)) {
879
return true;
880
}
881
882
return Str(cx, vp, &scx);
883
}
884
885
/* ES5 15.12.2 Walk. */
886
static bool Walk(JSContext* cx, HandleObject holder, HandleId name,
887
HandleValue reviver, MutableHandleValue vp) {
888
if (!CheckRecursionLimit(cx)) {
889
return false;
890
}
891
892
/* Step 1. */
893
RootedValue val(cx);
894
if (!GetProperty(cx, holder, holder, name, &val)) {
895
return false;
896
}
897
898
/* Step 2. */
899
if (val.isObject()) {
900
RootedObject obj(cx, &val.toObject());
901
902
bool isArray;
903
if (!IsArray(cx, obj, &isArray)) {
904
return false;
905
}
906
907
if (isArray) {
908
/* Step 2a(ii). */
909
uint32_t length;
910
if (!GetLengthProperty(cx, obj, &length)) {
911
return false;
912
}
913
914
/* Step 2a(i), 2a(iii-iv). */
915
RootedId id(cx);
916
RootedValue newElement(cx);
917
for (uint32_t i = 0; i < length; i++) {
918
if (!CheckForInterrupt(cx)) {
919
return false;
920
}
921
922
if (!IndexToId(cx, i, &id)) {
923
return false;
924
}
925
926
/* Step 2a(iii)(1). */
927
if (!Walk(cx, obj, id, reviver, &newElement)) {
928
return false;
929
}
930
931
ObjectOpResult ignored;
932
if (newElement.isUndefined()) {
933
/* Step 2a(iii)(2). The spec deliberately ignores strict failure. */
934
if (!DeleteProperty(cx, obj, id, ignored)) {
935
return false;
936
}
937
} else {
938
/* Step 2a(iii)(3). The spec deliberately ignores strict failure. */
939
Rooted<PropertyDescriptor> desc(cx);
940
desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
941
if (!DefineProperty(cx, obj, id, desc, ignored)) {
942
return false;
943
}
944
}
945
}
946
} else {
947
/* Step 2b(i). */
948
RootedIdVector keys(cx);
949
if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) {
950
return false;
951
}
952
953
/* Step 2b(ii). */
954
RootedId id(cx);
955
RootedValue newElement(cx);
956
for (size_t i = 0, len = keys.length(); i < len; i++) {
957
if (!CheckForInterrupt(cx)) {
958
return false;
959
}
960
961
/* Step 2b(ii)(1). */
962
id = keys[i];
963
if (!Walk(cx, obj, id, reviver, &newElement)) {
964
return false;
965
}
966
967
ObjectOpResult ignored;
968
if (newElement.isUndefined()) {
969
/* Step 2b(ii)(2). The spec deliberately ignores strict failure. */
970
if (!DeleteProperty(cx, obj, id, ignored)) {
971
return false;
972
}
973
} else {
974
/* Step 2b(ii)(3). The spec deliberately ignores strict failure. */
975
Rooted<PropertyDescriptor> desc(cx);
976
desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
977
if (!DefineProperty(cx, obj, id, desc, ignored)) {
978
return false;
979
}
980
}
981
}
982
}
983
}
984
985
/* Step 3. */
986
RootedString key(cx, IdToString(cx, name));
987
if (!key) {
988
return false;
989
}
990
991
RootedValue keyVal(cx, StringValue(key));
992
return js::Call(cx, reviver, holder, keyVal, val, vp);
993
}
994
995
static bool Revive(JSContext* cx, HandleValue reviver, MutableHandleValue vp) {
996
RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
997
if (!obj) {
998
return false;
999
}
1000
1001
if (!DefineDataProperty(cx, obj, cx->names().empty, vp)) {
1002
return false;
1003
}
1004
1005
Rooted<jsid> id(cx, NameToId(cx->names().empty));
1006
return Walk(cx, obj, id, reviver, vp);
1007
}
1008
1009
template <typename CharT>
1010
bool js::ParseJSONWithReviver(JSContext* cx,
1011
const mozilla::Range<const CharT> chars,
1012
HandleValue reviver, MutableHandleValue vp) {
1013
/* 15.12.2 steps 2-3. */
1014
Rooted<JSONParser<CharT>> parser(
1015
cx, JSONParser<CharT>(cx, chars, JSONParserBase::ParseType::JSONParse));
1016
if (!parser.parse(vp)) {
1017
return false;
1018
}
1019
1020
/* 15.12.2 steps 4-5. */
1021
if (IsCallable(reviver)) {
1022
return Revive(cx, reviver, vp);
1023
}
1024
return true;
1025
}
1026
1027
template bool js::ParseJSONWithReviver(
1028
JSContext* cx, const mozilla::Range<const Latin1Char> chars,
1029
HandleValue reviver, MutableHandleValue vp);
1030
1031
template bool js::ParseJSONWithReviver(
1032
JSContext* cx, const mozilla::Range<const char16_t> chars,
1033
HandleValue reviver, MutableHandleValue vp);
1034
1035
static bool json_toSource(JSContext* cx, unsigned argc, Value* vp) {
1036
CallArgs args = CallArgsFromVp(argc, vp);
1037
args.rval().setString(cx->names().JSON);
1038
return true;
1039
}
1040
1041
/* ES5 15.12.2. */
1042
static bool json_parse(JSContext* cx, unsigned argc, Value* vp) {
1043
CallArgs args = CallArgsFromVp(argc, vp);
1044
1045
/* Step 1. */
1046
JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0])
1047
: cx->names().undefined;
1048
if (!str) {
1049
return false;
1050
}
1051
1052
JSLinearString* linear = str->ensureLinear(cx);
1053
if (!linear) {
1054
return false;
1055
}
1056
1057
AutoStableStringChars linearChars(cx);
1058
if (!linearChars.init(cx, linear)) {
1059
return false;
1060
}
1061
1062
HandleValue reviver = args.get(1);
1063
1064
/* Steps 2-5. */
1065
return linearChars.isLatin1()
1066
? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver,
1067
args.rval())
1068
: ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver,
1069
args.rval());
1070
}
1071
1072
/* ES6 24.3.2. */
1073
bool json_stringify(JSContext* cx, unsigned argc, Value* vp) {
1074
CallArgs args = CallArgsFromVp(argc, vp);
1075
1076
RootedObject replacer(cx,
1077
args.get(1).isObject() ? &args[1].toObject() : nullptr);
1078
RootedValue value(cx, args.get(0));
1079
RootedValue space(cx, args.get(2));
1080
1081
JSStringBuilder sb(cx);
1082
if (!Stringify(cx, &value, replacer, space, sb, StringifyBehavior::Normal)) {
1083
return false;
1084
}
1085
1086
// XXX This can never happen to nsJSON.cpp, but the JSON object
1087
// needs to support returning undefined. So this is a little awkward
1088
// for the API, because we want to support streaming writers.
1089
if (!sb.empty()) {
1090
JSString* str = sb.finishString();
1091
if (!str) {
1092
return false;
1093
}
1094
args.rval().setString(str);
1095
} else {
1096
args.rval().setUndefined();
1097
}
1098
1099
return true;
1100
}
1101
1102
static const JSFunctionSpec json_static_methods[] = {
1103
JS_FN(js_toSource_str, json_toSource, 0, 0),
1104
JS_FN("parse", json_parse, 2, 0), JS_FN("stringify", json_stringify, 3, 0),
1105
JS_FS_END};
1106
1107
JSObject* js::InitJSONClass(JSContext* cx, Handle<GlobalObject*> global) {
1108
RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global));
1109
if (!proto) {
1110
return nullptr;
1111
}
1112
RootedObject JSON(
1113
cx, NewObjectWithGivenProto(cx, &JSONClass, proto, SingletonObject));
1114
if (!JSON) {
1115
return nullptr;
1116
}
1117
1118
if (!JS_DefineProperty(cx, global, js_JSON_str, JSON, JSPROP_RESOLVING)) {
1119
return nullptr;
1120
}
1121
1122
if (!JS_DefineFunctions(cx, JSON, json_static_methods)) {
1123
return nullptr;
1124
}
1125
1126
if (!DefineToStringTag(cx, JSON, cx->names().JSON)) {
1127
return nullptr;
1128
}
1129
1130
global->setConstructor(JSProto_JSON, ObjectValue(*JSON));
1131
1132
return JSON;
1133
}