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