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