1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * vim: set ts=8 sts=4 et sw=4 tw=99:
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 "json.h"
8
9 #include "mozilla/FloatingPoint.h"
10 #include "mozilla/Range.h"
11 #include "mozilla/ScopeExit.h"
12
13 #include "jsarray.h"
14 #include "jsatom.h"
15 #include "jscntxt.h"
16 #include "jsnum.h"
17 #include "jsobj.h"
18 #include "jsstr.h"
19 #include "jstypes.h"
20 #include "jsutil.h"
21
22 #include "vm/Interpreter.h"
23 #include "vm/JSONParser.h"
24 #include "vm/StringBuffer.h"
25
26 #include "jsatominlines.h"
27 #include "jsboolinlines.h"
28
29 #include "vm/NativeObject-inl.h"
30
31 using namespace js;
32 using namespace js::gc;
33
34 using mozilla::IsFinite;
35 using mozilla::Maybe;
36 using mozilla::RangedPtr;
37
38 const Class js::JSONClass = {
39 js_JSON_str,
40 JSCLASS_HAS_CACHED_PROTO(JSProto_JSON)
41 };
42
43 static inline bool
IsQuoteSpecialCharacter(char16_t c)44 IsQuoteSpecialCharacter(char16_t c)
45 {
46 static_assert('\b' < ' ', "'\\b' must be treated as special below");
47 static_assert('\f' < ' ', "'\\f' must be treated as special below");
48 static_assert('\n' < ' ', "'\\n' must be treated as special below");
49 static_assert('\r' < ' ', "'\\r' must be treated as special below");
50 static_assert('\t' < ' ', "'\\t' must be treated as special below");
51
52 return c == '"' || c == '\\' || c < ' ';
53 }
54
55 /* ES5 15.12.3 Quote. */
56 template <typename CharT>
57 static bool
Quote(StringBuffer & sb,JSLinearString * str)58 Quote(StringBuffer& sb, JSLinearString* str)
59 {
60 size_t len = str->length();
61
62 /* Step 1. */
63 if (!sb.append('"'))
64 return false;
65
66 /* Step 2. */
67 JS::AutoCheckCannotGC nogc;
68 const RangedPtr<const CharT> buf(str->chars<CharT>(nogc), len);
69 for (size_t i = 0; i < len; ++i) {
70 /* Batch-append maximal character sequences containing no escapes. */
71 size_t mark = i;
72 do {
73 if (IsQuoteSpecialCharacter(buf[i]))
74 break;
75 } while (++i < len);
76 if (i > mark) {
77 if (!sb.appendSubstring(str, mark, i - mark))
78 return false;
79 if (i == len)
80 break;
81 }
82
83 char16_t c = buf[i];
84 if (c == '"' || c == '\\') {
85 if (!sb.append('\\') || !sb.append(c))
86 return false;
87 } else if (c == '\b' || c == '\f' || c == '\n' || c == '\r' || c == '\t') {
88 char16_t abbrev = (c == '\b')
89 ? 'b'
90 : (c == '\f')
91 ? 'f'
92 : (c == '\n')
93 ? 'n'
94 : (c == '\r')
95 ? 'r'
96 : 't';
97 if (!sb.append('\\') || !sb.append(abbrev))
98 return false;
99 } else {
100 MOZ_ASSERT(c < ' ');
101 if (!sb.append("\\u00"))
102 return false;
103 MOZ_ASSERT((c >> 4) < 10);
104 uint8_t x = c >> 4, y = c % 16;
105 if (!sb.append(Latin1Char('0' + x)) ||
106 !sb.append(Latin1Char(y < 10 ? '0' + y : 'a' + (y - 10))))
107 {
108 return false;
109 }
110 }
111 }
112
113 /* Steps 3-4. */
114 return sb.append('"');
115 }
116
117 static bool
Quote(JSContext * cx,StringBuffer & sb,JSString * str)118 Quote(JSContext* cx, StringBuffer& sb, JSString* str)
119 {
120 JSLinearString* linear = str->ensureLinear(cx);
121 if (!linear)
122 return false;
123
124 return linear->hasLatin1Chars()
125 ? Quote<Latin1Char>(sb, linear)
126 : Quote<char16_t>(sb, linear);
127 }
128
129 namespace {
130
131 class StringifyContext
132 {
133 public:
StringifyContext(JSContext * cx,StringBuffer & sb,const StringBuffer & gap,HandleObject replacer,const AutoIdVector & propertyList)134 StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap,
135 HandleObject replacer, const AutoIdVector& propertyList)
136 : sb(sb),
137 gap(gap),
138 replacer(cx, replacer),
139 stack(cx, GCHashSet<JSObject*, MovableCellHasher<JSObject*>>(cx)),
140 propertyList(propertyList),
141 depth(0)
142 {}
143
init()144 bool init() {
145 return stack.init(8);
146 }
147
148 StringBuffer& sb;
149 const StringBuffer& gap;
150 RootedObject replacer;
151 Rooted<GCHashSet<JSObject*, MovableCellHasher<JSObject*>>> stack;
152 const AutoIdVector& propertyList;
153 uint32_t depth;
154 };
155
156 } /* anonymous namespace */
157
158 static bool Str(JSContext* cx, const Value& v, StringifyContext* scx);
159
160 static bool
WriteIndent(JSContext * cx,StringifyContext * scx,uint32_t limit)161 WriteIndent(JSContext* cx, StringifyContext* scx, uint32_t limit)
162 {
163 if (!scx->gap.empty()) {
164 if (!scx->sb.append('\n'))
165 return false;
166
167 if (scx->gap.isUnderlyingBufferLatin1()) {
168 for (uint32_t i = 0; i < limit; i++) {
169 if (!scx->sb.append(scx->gap.rawLatin1Begin(), scx->gap.rawLatin1End()))
170 return false;
171 }
172 } else {
173 for (uint32_t i = 0; i < limit; i++) {
174 if (!scx->sb.append(scx->gap.rawTwoByteBegin(), scx->gap.rawTwoByteEnd()))
175 return false;
176 }
177 }
178 }
179
180 return true;
181 }
182
183 namespace {
184
185 template<typename KeyType>
186 class KeyStringifier {
187 };
188
189 template<>
190 class KeyStringifier<uint32_t> {
191 public:
toString(JSContext * cx,uint32_t index)192 static JSString* toString(JSContext* cx, uint32_t index) {
193 return IndexToString(cx, index);
194 }
195 };
196
197 template<>
198 class KeyStringifier<HandleId> {
199 public:
toString(JSContext * cx,HandleId id)200 static JSString* toString(JSContext* cx, HandleId id) {
201 return IdToString(cx, id);
202 }
203 };
204
205 } /* anonymous namespace */
206
207 /*
208 * ES5 15.12.3 Str, steps 2-4, extracted to enable preprocessing of property
209 * values when stringifying objects in JO.
210 */
211 template<typename KeyType>
212 static bool
PreprocessValue(JSContext * cx,HandleObject holder,KeyType key,MutableHandleValue vp,StringifyContext * scx)213 PreprocessValue(JSContext* cx, HandleObject holder, KeyType key, MutableHandleValue vp, StringifyContext* scx)
214 {
215 RootedString keyStr(cx);
216
217 /* Step 2. */
218 if (vp.isObject()) {
219 RootedValue toJSON(cx);
220 RootedObject obj(cx, &vp.toObject());
221 if (!GetProperty(cx, obj, obj, cx->names().toJSON, &toJSON))
222 return false;
223
224 if (IsCallable(toJSON)) {
225 keyStr = KeyStringifier<KeyType>::toString(cx, key);
226 if (!keyStr)
227 return false;
228
229 InvokeArgs args(cx);
230 if (!args.init(cx, 1))
231 return false;
232
233 args.setCallee(toJSON);
234 args.setThis(vp);
235 args[0].setString(keyStr);
236
237 if (!Invoke(cx, args))
238 return false;
239 vp.set(args.rval());
240 }
241 }
242
243 /* Step 3. */
244 if (scx->replacer && scx->replacer->isCallable()) {
245 if (!keyStr) {
246 keyStr = KeyStringifier<KeyType>::toString(cx, key);
247 if (!keyStr)
248 return false;
249 }
250
251 InvokeArgs args(cx);
252 if (!args.init(cx, 2))
253 return false;
254
255 args.setCallee(ObjectValue(*scx->replacer));
256 args.setThis(ObjectValue(*holder));
257 args[0].setString(keyStr);
258 args[1].set(vp);
259
260 if (!Invoke(cx, args))
261 return false;
262 vp.set(args.rval());
263 }
264
265 /* Step 4. */
266 if (vp.get().isObject()) {
267 RootedObject obj(cx, &vp.get().toObject());
268
269 ESClassValue cls;
270 if (!GetBuiltinClass(cx, obj, &cls))
271 return false;
272
273 if (cls == ESClass_Number) {
274 double d;
275 if (!ToNumber(cx, vp, &d))
276 return false;
277 vp.setNumber(d);
278 } else if (cls == ESClass_String) {
279 JSString* str = ToStringSlow<CanGC>(cx, vp);
280 if (!str)
281 return false;
282 vp.setString(str);
283 } else if (cls == ESClass_Boolean) {
284 if (!Unbox(cx, obj, vp))
285 return false;
286 }
287 }
288
289 return true;
290 }
291
292 /*
293 * Determines whether a value which has passed by ES5 150.2.3 Str steps 1-4's
294 * gauntlet will result in Str returning |undefined|. This function is used to
295 * properly omit properties resulting in such values when stringifying objects,
296 * while properly stringifying such properties as null when they're encountered
297 * in arrays.
298 */
299 static inline bool
IsFilteredValue(const Value & v)300 IsFilteredValue(const Value& v)
301 {
302 return v.isUndefined() || v.isSymbol() || IsCallable(v);
303 }
304
305 class CycleDetector
306 {
307 public:
CycleDetector(StringifyContext * scx,HandleObject obj)308 CycleDetector(StringifyContext* scx, HandleObject obj)
309 : stack(&scx->stack), obj_(obj) {
310 }
311
foundCycle(JSContext * cx)312 bool foundCycle(JSContext* cx) {
313 auto addPtr = stack.lookupForAdd(obj_);
314 if (addPtr) {
315 JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_JSON_CYCLIC_VALUE,
316 js_object_str);
317 return false;
318 }
319 return stack.add(addPtr, obj_);
320 }
321
~CycleDetector()322 ~CycleDetector() {
323 stack.remove(obj_);
324 }
325
326 private:
327 MutableHandle<GCHashSet<JSObject*, MovableCellHasher<JSObject*>>> stack;
328 HandleObject obj_;
329 };
330
331 /* ES5 15.12.3 JO. */
332 static bool
JO(JSContext * cx,HandleObject obj,StringifyContext * scx)333 JO(JSContext* cx, HandleObject obj, StringifyContext* scx)
334 {
335 /*
336 * This method implements the JO algorithm in ES5 15.12.3, but:
337 *
338 * * The algorithm is somewhat reformulated to allow the final string to
339 * be streamed into a single buffer, rather than be created and copied
340 * into place incrementally as the ES5 algorithm specifies it. This
341 * requires moving portions of the Str call in 8a into this algorithm
342 * (and in JA as well).
343 */
344
345 /* Steps 1-2, 11. */
346 CycleDetector detect(scx, obj);
347 if (!detect.foundCycle(cx))
348 return false;
349
350 if (!scx->sb.append('{'))
351 return false;
352
353 /* Steps 5-7. */
354 Maybe<AutoIdVector> ids;
355 const AutoIdVector* props;
356 if (scx->replacer && !scx->replacer->isCallable()) {
357 // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer
358 // might have been a revocable proxy to an array. Such a proxy
359 // satisfies |IsArray|, but any side effect of JSON.stringify
360 // could revoke the proxy so that |!IsArray(scx->replacer)|. See
361 // bug 1196497.
362 props = &scx->propertyList;
363 } else {
364 MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0);
365 ids.emplace(cx);
366 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr()))
367 return false;
368 props = ids.ptr();
369 }
370
371 /* My kingdom for not-quite-initialized-from-the-start references. */
372 const AutoIdVector& propertyList = *props;
373
374 /* Steps 8-10, 13. */
375 bool wroteMember = false;
376 RootedId id(cx);
377 for (size_t i = 0, len = propertyList.length(); i < len; i++) {
378 /*
379 * Steps 8a-8b. Note that the call to Str is broken up into 1) getting
380 * the property; 2) processing for toJSON, calling the replacer, and
381 * handling boxed Number/String/Boolean objects; 3) filtering out
382 * values which process to |undefined|, and 4) stringifying all values
383 * which pass the filter.
384 */
385 id = propertyList[i];
386 RootedValue outputValue(cx);
387 if (!GetProperty(cx, obj, obj, id, &outputValue))
388 return false;
389 if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx))
390 return false;
391 if (IsFilteredValue(outputValue))
392 continue;
393
394 /* Output a comma unless this is the first member to write. */
395 if (wroteMember && !scx->sb.append(','))
396 return false;
397 wroteMember = true;
398
399 if (!WriteIndent(cx, scx, scx->depth))
400 return false;
401
402 JSString* s = IdToString(cx, id);
403 if (!s)
404 return false;
405
406 if (!Quote(cx, scx->sb, s) ||
407 !scx->sb.append(':') ||
408 !(scx->gap.empty() || scx->sb.append(' ')) ||
409 !Str(cx, outputValue, scx))
410 {
411 return false;
412 }
413 }
414
415 if (wroteMember && !WriteIndent(cx, scx, scx->depth - 1))
416 return false;
417
418 return scx->sb.append('}');
419 }
420
421 /* ES5 15.12.3 JA. */
422 static bool
JA(JSContext * cx,HandleObject obj,StringifyContext * scx)423 JA(JSContext* cx, HandleObject obj, StringifyContext* scx)
424 {
425 /*
426 * This method implements the JA algorithm in ES5 15.12.3, but:
427 *
428 * * The algorithm is somewhat reformulated to allow the final string to
429 * be streamed into a single buffer, rather than be created and copied
430 * into place incrementally as the ES5 algorithm specifies it. This
431 * requires moving portions of the Str call in 8a into this algorithm
432 * (and in JO as well).
433 */
434
435 /* Steps 1-2, 11. */
436 CycleDetector detect(scx, obj);
437 if (!detect.foundCycle(cx))
438 return false;
439
440 if (!scx->sb.append('['))
441 return false;
442
443 /* Step 6. */
444 uint32_t length;
445 if (!GetLengthProperty(cx, obj, &length))
446 return false;
447
448 /* Steps 7-10. */
449 if (length != 0) {
450 /* Steps 4, 10b(i). */
451 if (!WriteIndent(cx, scx, scx->depth))
452 return false;
453
454 /* Steps 7-10. */
455 RootedValue outputValue(cx);
456 for (uint32_t i = 0; i < length; i++) {
457 /*
458 * Steps 8a-8c. Again note how the call to the spec's Str method
459 * is broken up into getting the property, running it past toJSON
460 * and the replacer and maybe unboxing, and interpreting some
461 * values as |null| in separate steps.
462 */
463 if (!GetElement(cx, obj, obj, i, &outputValue))
464 return false;
465 if (!PreprocessValue(cx, obj, i, &outputValue, scx))
466 return false;
467 if (IsFilteredValue(outputValue)) {
468 if (!scx->sb.append("null"))
469 return false;
470 } else {
471 if (!Str(cx, outputValue, scx))
472 return false;
473 }
474
475 /* Steps 3, 4, 10b(i). */
476 if (i < length - 1) {
477 if (!scx->sb.append(','))
478 return false;
479 if (!WriteIndent(cx, scx, scx->depth))
480 return false;
481 }
482 }
483
484 /* Step 10(b)(iii). */
485 if (!WriteIndent(cx, scx, scx->depth - 1))
486 return false;
487 }
488
489 return scx->sb.append(']');
490 }
491
492 static bool
Str(JSContext * cx,const Value & v,StringifyContext * scx)493 Str(JSContext* cx, const Value& v, StringifyContext* scx)
494 {
495 /* Step 11 must be handled by the caller. */
496 MOZ_ASSERT(!IsFilteredValue(v));
497
498 JS_CHECK_RECURSION(cx, return false);
499
500 /*
501 * This method implements the Str algorithm in ES5 15.12.3, but:
502 *
503 * * We move property retrieval (step 1) into callers to stream the
504 * stringification process and avoid constantly copying strings.
505 * * We move the preprocessing in steps 2-4 into a helper function to
506 * allow both JO and JA to use this method. While JA could use it
507 * without this move, JO must omit any |undefined|-valued property per
508 * so it can't stream out a value using the Str method exactly as
509 * defined by ES5.
510 * * We move step 11 into callers, again to ease streaming.
511 */
512
513 /* Step 8. */
514 if (v.isString())
515 return Quote(cx, scx->sb, v.toString());
516
517 /* Step 5. */
518 if (v.isNull())
519 return scx->sb.append("null");
520
521 /* Steps 6-7. */
522 if (v.isBoolean())
523 return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false");
524
525 /* Step 9. */
526 if (v.isNumber()) {
527 if (v.isDouble()) {
528 if (!IsFinite(v.toDouble()))
529 return scx->sb.append("null");
530 }
531
532 return NumberValueToStringBuffer(cx, v, scx->sb);
533 }
534
535 /* Step 10. */
536 MOZ_ASSERT(v.isObject());
537 RootedObject obj(cx, &v.toObject());
538
539 scx->depth++;
540 auto dec = mozilla::MakeScopeExit([&] { scx->depth--; });
541
542 bool isArray;
543 if (!IsArray(cx, obj, &isArray))
544 return false;
545
546 return isArray ? JA(cx, obj, scx) : JO(cx, obj, scx);
547 }
548
549 /* ES6 24.3.2. */
550 bool
Stringify(JSContext * cx,MutableHandleValue vp,JSObject * replacer_,Value space_,StringBuffer & sb)551 js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_, Value space_,
552 StringBuffer& sb)
553 {
554 RootedObject replacer(cx, replacer_);
555 RootedValue space(cx, space_);
556
557 /* Step 4. */
558 AutoIdVector propertyList(cx);
559 if (replacer) {
560 bool isArray;
561 if (replacer->isCallable()) {
562 /* Step 4a(i): use replacer to transform values. */
563 } else if (!IsArray(cx, replacer, &isArray)) {
564 return false;
565 } else if (isArray) {
566 /* Step 4b(iii). */
567
568 /* Step 4b(iii)(2-3). */
569 uint32_t len;
570 if (!GetLengthProperty(cx, replacer, &len))
571 return false;
572
573 // Cap the initial size to a moderately small value. This avoids
574 // ridiculous over-allocation if an array with bogusly-huge length
575 // is passed in. If we end up having to add elements past this
576 // size, the set will naturally resize to accommodate them.
577 const uint32_t MaxInitialSize = 32;
578 HashSet<jsid, JsidHasher> idSet(cx);
579 if (!idSet.init(Min(len, MaxInitialSize)))
580 return false;
581
582 /* Step 4b(iii)(4). */
583 uint32_t k = 0;
584
585 /* Step 4b(iii)(5). */
586 RootedValue item(cx);
587 for (; k < len; k++) {
588 if (!CheckForInterrupt(cx))
589 return false;
590
591 /* Step 4b(iii)(5)(a-b). */
592 if (!GetElement(cx, replacer, replacer, k, &item))
593 return false;
594
595 RootedId id(cx);
596
597 /* Step 4b(iii)(5)(c-f). */
598 if (item.isNumber()) {
599 /* Step 4b(iii)(5)(e). */
600 int32_t n;
601 if (ValueFitsInInt32(item, &n) && INT_FITS_IN_JSID(n)) {
602 id = INT_TO_JSID(n);
603 } else {
604 if (!ValueToId<CanGC>(cx, item, &id))
605 return false;
606 }
607 } else {
608 bool shouldAdd = item.isString();
609 if (!shouldAdd) {
610 ESClassValue cls;
611 if (!GetClassOfValue(cx, item, &cls))
612 return false;
613
614 shouldAdd = cls == ESClass_String || cls == ESClass_Number;
615 }
616
617 if (shouldAdd) {
618 /* Step 4b(iii)(5)(f). */
619 if (!ValueToId<CanGC>(cx, item, &id))
620 return false;
621 } else {
622 /* Step 4b(iii)(5)(g). */
623 continue;
624 }
625 }
626
627 /* Step 4b(iii)(5)(g). */
628 auto p = idSet.lookupForAdd(id);
629 if (!p) {
630 /* Step 4b(iii)(5)(g)(i). */
631 if (!idSet.add(p, id) || !propertyList.append(id))
632 return false;
633 }
634 }
635 } else {
636 replacer = nullptr;
637 }
638 }
639
640 /* Step 5. */
641 if (space.isObject()) {
642 RootedObject spaceObj(cx, &space.toObject());
643
644 ESClassValue cls;
645 if (!GetBuiltinClass(cx, spaceObj, &cls))
646 return false;
647
648 if (cls == ESClass_Number) {
649 double d;
650 if (!ToNumber(cx, space, &d))
651 return false;
652 space = NumberValue(d);
653 } else if (cls == ESClass_String) {
654 JSString* str = ToStringSlow<CanGC>(cx, space);
655 if (!str)
656 return false;
657 space = StringValue(str);
658 }
659 }
660
661 StringBuffer gap(cx);
662
663 if (space.isNumber()) {
664 /* Step 6. */
665 double d;
666 MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d));
667 d = Min(10.0, d);
668 if (d >= 1 && !gap.appendN(' ', uint32_t(d)))
669 return false;
670 } else if (space.isString()) {
671 /* Step 7. */
672 JSLinearString* str = space.toString()->ensureLinear(cx);
673 if (!str)
674 return false;
675 size_t len = Min(size_t(10), str->length());
676 if (!gap.appendSubstring(str, 0, len))
677 return false;
678 } else {
679 /* Step 8. */
680 MOZ_ASSERT(gap.empty());
681 }
682
683 /* Step 9. */
684 RootedPlainObject wrapper(cx, NewBuiltinClassInstance<PlainObject>(cx));
685 if (!wrapper)
686 return false;
687
688 /* Steps 10-11. */
689 RootedId emptyId(cx, NameToId(cx->names().empty));
690 if (!NativeDefineProperty(cx, wrapper, emptyId, vp, nullptr, nullptr, JSPROP_ENUMERATE))
691 return false;
692
693 /* Step 12. */
694 StringifyContext scx(cx, sb, gap, replacer, propertyList);
695 if (!scx.init())
696 return false;
697 if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx))
698 return false;
699 if (IsFilteredValue(vp))
700 return true;
701
702 return Str(cx, vp, &scx);
703 }
704
705 /* ES5 15.12.2 Walk. */
706 static bool
Walk(JSContext * cx,HandleObject holder,HandleId name,HandleValue reviver,MutableHandleValue vp)707 Walk(JSContext* cx, HandleObject holder, HandleId name, HandleValue reviver, MutableHandleValue vp)
708 {
709 JS_CHECK_RECURSION(cx, return false);
710
711 /* Step 1. */
712 RootedValue val(cx);
713 if (!GetProperty(cx, holder, holder, name, &val))
714 return false;
715
716 /* Step 2. */
717 if (val.isObject()) {
718 RootedObject obj(cx, &val.toObject());
719
720 bool isArray;
721 if (!IsArray(cx, obj, &isArray))
722 return false;
723
724 if (isArray) {
725 /* Step 2a(ii). */
726 uint32_t length;
727 if (!GetLengthProperty(cx, obj, &length))
728 return false;
729
730 /* Step 2a(i), 2a(iii-iv). */
731 RootedId id(cx);
732 RootedValue newElement(cx);
733 for (uint32_t i = 0; i < length; i++) {
734 if (!IndexToId(cx, i, &id))
735 return false;
736
737 /* Step 2a(iii)(1). */
738 if (!Walk(cx, obj, id, reviver, &newElement))
739 return false;
740
741 ObjectOpResult ignored;
742 if (newElement.isUndefined()) {
743 /* Step 2a(iii)(2). The spec deliberately ignores strict failure. */
744 if (!DeleteProperty(cx, obj, id, ignored))
745 return false;
746 } else {
747 /* Step 2a(iii)(3). The spec deliberately ignores strict failure. */
748 Rooted<PropertyDescriptor> desc(cx);
749 desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
750 if (!DefineProperty(cx, obj, id, desc, ignored))
751 return false;
752 }
753 }
754 } else {
755 /* Step 2b(i). */
756 AutoIdVector keys(cx);
757 if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys))
758 return false;
759
760 /* Step 2b(ii). */
761 RootedId id(cx);
762 RootedValue newElement(cx);
763 for (size_t i = 0, len = keys.length(); i < len; i++) {
764 /* Step 2b(ii)(1). */
765 id = keys[i];
766 if (!Walk(cx, obj, id, reviver, &newElement))
767 return false;
768
769 ObjectOpResult ignored;
770 if (newElement.isUndefined()) {
771 /* Step 2b(ii)(2). The spec deliberately ignores strict failure. */
772 if (!DeleteProperty(cx, obj, id, ignored))
773 return false;
774 } else {
775 /* Step 2b(ii)(3). The spec deliberately ignores strict failure. */
776 Rooted<PropertyDescriptor> desc(cx);
777 desc.setDataDescriptor(newElement, JSPROP_ENUMERATE);
778 if (!DefineProperty(cx, obj, id, desc, ignored))
779 return false;
780 }
781 }
782 }
783 }
784
785 /* Step 3. */
786 RootedString key(cx, IdToString(cx, name));
787 if (!key)
788 return false;
789
790 InvokeArgs args(cx);
791 if (!args.init(cx, 2))
792 return false;
793
794 args.setCallee(reviver);
795 args.setThis(ObjectValue(*holder));
796 args[0].setString(key);
797 args[1].set(val);
798
799 if (!Invoke(cx, args))
800 return false;
801 vp.set(args.rval());
802 return true;
803 }
804
805 static bool
Revive(JSContext * cx,HandleValue reviver,MutableHandleValue vp)806 Revive(JSContext* cx, HandleValue reviver, MutableHandleValue vp)
807 {
808 RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
809 if (!obj)
810 return false;
811
812 if (!DefineProperty(cx, obj, cx->names().empty, vp))
813 return false;
814
815 Rooted<jsid> id(cx, NameToId(cx->names().empty));
816 return Walk(cx, obj, id, reviver, vp);
817 }
818
819 template <typename CharT>
820 bool
ParseJSONWithReviver(JSContext * cx,const mozilla::Range<const CharT> chars,HandleValue reviver,MutableHandleValue vp)821 js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const CharT> chars, HandleValue reviver,
822 MutableHandleValue vp)
823 {
824 /* 15.12.2 steps 2-3. */
825 Rooted<JSONParser<CharT>> parser(cx, JSONParser<CharT>(cx, chars));
826 if (!parser.parse(vp))
827 return false;
828
829 /* 15.12.2 steps 4-5. */
830 if (IsCallable(reviver))
831 return Revive(cx, reviver, vp);
832 return true;
833 }
834
835 template bool
836 js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const Latin1Char> chars,
837 HandleValue reviver, MutableHandleValue vp);
838
839 template bool
840 js::ParseJSONWithReviver(JSContext* cx, const mozilla::Range<const char16_t> chars, HandleValue reviver,
841 MutableHandleValue vp);
842
843 #if JS_HAS_TOSOURCE
844 static bool
json_toSource(JSContext * cx,unsigned argc,Value * vp)845 json_toSource(JSContext* cx, unsigned argc, Value* vp)
846 {
847 CallArgs args = CallArgsFromVp(argc, vp);
848 args.rval().setString(cx->names().JSON);
849 return true;
850 }
851 #endif
852
853 /* ES5 15.12.2. */
854 static bool
json_parse(JSContext * cx,unsigned argc,Value * vp)855 json_parse(JSContext* cx, unsigned argc, Value* vp)
856 {
857 CallArgs args = CallArgsFromVp(argc, vp);
858
859 /* Step 1. */
860 JSString* str = (args.length() >= 1)
861 ? ToString<CanGC>(cx, args[0])
862 : cx->names().undefined;
863 if (!str)
864 return false;
865
866 JSLinearString* linear = str->ensureLinear(cx);
867 if (!linear)
868 return false;
869
870 AutoStableStringChars linearChars(cx);
871 if (!linearChars.init(cx, linear))
872 return false;
873
874 HandleValue reviver = args.get(1);
875
876 /* Steps 2-5. */
877 return linearChars.isLatin1()
878 ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver, args.rval())
879 : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver, args.rval());
880 }
881
882 /* ES6 24.3.2. */
883 bool
json_stringify(JSContext * cx,unsigned argc,Value * vp)884 json_stringify(JSContext* cx, unsigned argc, Value* vp)
885 {
886 CallArgs args = CallArgsFromVp(argc, vp);
887
888 RootedObject replacer(cx, args.get(1).isObject() ? &args[1].toObject() : nullptr);
889 RootedValue value(cx, args.get(0));
890 RootedValue space(cx, args.get(2));
891
892 StringBuffer sb(cx);
893 if (!Stringify(cx, &value, replacer, space, sb))
894 return false;
895
896 // XXX This can never happen to nsJSON.cpp, but the JSON object
897 // needs to support returning undefined. So this is a little awkward
898 // for the API, because we want to support streaming writers.
899 if (!sb.empty()) {
900 JSString* str = sb.finishString();
901 if (!str)
902 return false;
903 args.rval().setString(str);
904 } else {
905 args.rval().setUndefined();
906 }
907
908 return true;
909 }
910
911 static const JSFunctionSpec json_static_methods[] = {
912 #if JS_HAS_TOSOURCE
913 JS_FN(js_toSource_str, json_toSource, 0, 0),
914 #endif
915 JS_FN("parse", json_parse, 2, 0),
916 JS_FN("stringify", json_stringify, 3, 0),
917 JS_FS_END
918 };
919
920 JSObject*
InitJSONClass(JSContext * cx,HandleObject obj)921 js::InitJSONClass(JSContext* cx, HandleObject obj)
922 {
923 Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>());
924
925 RootedObject proto(cx, global->getOrCreateObjectPrototype(cx));
926 if (!proto)
927 return nullptr;
928 RootedObject JSON(cx, NewObjectWithGivenProto(cx, &JSONClass, proto, SingletonObject));
929 if (!JSON)
930 return nullptr;
931
932 if (!JS_DefineProperty(cx, global, js_JSON_str, JSON, JSPROP_RESOLVING,
933 JS_STUBGETTER, JS_STUBSETTER))
934 {
935 return nullptr;
936 }
937
938 if (!JS_DefineFunctions(cx, JSON, json_static_methods))
939 return nullptr;
940
941 global->setConstructor(JSProto_JSON, ObjectValue(*JSON));
942
943 return JSON;
944 }
945