1 | // Copyright 2006 The Closure Library Authors. All Rights Reserved. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS-IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | /** |
16 | * @fileoverview JSON utility functions. |
17 | * @author arv@google.com (Erik Arvidsson) |
18 | */ |
19 | |
20 | |
21 | goog.provide('goog.json'); |
22 | goog.provide('goog.json.Serializer'); |
23 | |
24 | |
25 | /** |
26 | * Tests if a string is an invalid JSON string. This only ensures that we are |
27 | * not using any invalid characters |
28 | * @param {string} s The string to test. |
29 | * @return {boolean} True if the input is a valid JSON string. |
30 | * @private |
31 | */ |
32 | goog.json.isValid_ = function(s) { |
33 | // All empty whitespace is not valid. |
34 | if (/^\s*$/.test(s)) { |
35 | return false; |
36 | } |
37 | |
38 | // This is taken from http://www.json.org/json2.js which is released to the |
39 | // public domain. |
40 | // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator |
41 | // inside strings. We also treat \u2028 and \u2029 as whitespace which they |
42 | // are in the RFC but IE and Safari does not match \s to these so we need to |
43 | // include them in the reg exps in all places where whitespace is allowed. |
44 | // We allowed \x7f inside strings because some tools don't escape it, |
45 | // e.g. http://www.json.org/java/org/json/JSONObject.java |
46 | |
47 | // Parsing happens in three stages. In the first stage, we run the text |
48 | // against regular expressions that look for non-JSON patterns. We are |
49 | // especially concerned with '()' and 'new' because they can cause invocation, |
50 | // and '=' because it can cause mutation. But just to be safe, we want to |
51 | // reject all unexpected forms. |
52 | |
53 | // We split the first stage into 4 regexp operations in order to work around |
54 | // crippling inefficiencies in IE's and Safari's regexp engines. First we |
55 | // replace all backslash pairs with '@' (a non-JSON character). Second, we |
56 | // replace all simple value tokens with ']' characters. Third, we delete all |
57 | // open brackets that follow a colon or comma or that begin the text. Finally, |
58 | // we look to see that the remaining characters are only whitespace or ']' or |
59 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. |
60 | |
61 | // Don't make these static since they have the global flag. |
62 | var backslashesRe = /\\["\\\/bfnrtu]/g; |
63 | var simpleValuesRe = |
64 | /"[^"\\\n\r\u2028\u2029\x00-\x08\x0a-\x1f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; |
65 | var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g; |
66 | var remainderRe = /^[\],:{}\s\u2028\u2029]*$/; |
67 | |
68 | return remainderRe.test(s.replace(backslashesRe, '@'). |
69 | replace(simpleValuesRe, ']'). |
70 | replace(openBracketsRe, '')); |
71 | }; |
72 | |
73 | |
74 | /** |
75 | * Parses a JSON string and returns the result. This throws an exception if |
76 | * the string is an invalid JSON string. |
77 | * |
78 | * Note that this is very slow on large strings. If you trust the source of |
79 | * the string then you should use unsafeParse instead. |
80 | * |
81 | * @param {*} s The JSON string to parse. |
82 | * @return {Object} The object generated from the JSON string. |
83 | */ |
84 | goog.json.parse = function(s) { |
85 | var o = String(s); |
86 | if (goog.json.isValid_(o)) { |
87 | /** @preserveTry */ |
88 | try { |
89 | return /** @type {Object} */ (eval('(' + o + ')')); |
90 | } catch (ex) { |
91 | } |
92 | } |
93 | throw Error('Invalid JSON string: ' + o); |
94 | }; |
95 | |
96 | |
97 | /** |
98 | * Parses a JSON string and returns the result. This uses eval so it is open |
99 | * to security issues and it should only be used if you trust the source. |
100 | * |
101 | * @param {string} s The JSON string to parse. |
102 | * @return {Object} The object generated from the JSON string. |
103 | */ |
104 | goog.json.unsafeParse = function(s) { |
105 | return /** @type {Object} */ (eval('(' + s + ')')); |
106 | }; |
107 | |
108 | |
109 | /** |
110 | * JSON replacer, as defined in Section 15.12.3 of the ES5 spec. |
111 | * |
112 | * TODO(nicksantos): Array should also be a valid replacer. |
113 | * |
114 | * @typedef {function(this:Object, string, *): *} |
115 | */ |
116 | goog.json.Replacer; |
117 | |
118 | |
119 | /** |
120 | * JSON reviver, as defined in Section 15.12.2 of the ES5 spec. |
121 | * |
122 | * @typedef {function(this:Object, string, *): *} |
123 | */ |
124 | goog.json.Reviver; |
125 | |
126 | |
127 | /** |
128 | * Serializes an object or a value to a JSON string. |
129 | * |
130 | * @param {*} object The object to serialize. |
131 | * @param {?goog.json.Replacer=} opt_replacer A replacer function |
132 | * called for each (key, value) pair that determines how the value |
133 | * should be serialized. By defult, this just returns the value |
134 | * and allows default serialization to kick in. |
135 | * @throws Error if there are loops in the object graph. |
136 | * @return {string} A JSON string representation of the input. |
137 | */ |
138 | goog.json.serialize = function(object, opt_replacer) { |
139 | // NOTE(nicksantos): Currently, we never use JSON.stringify. |
140 | // |
141 | // The last time I evaluated this, JSON.stringify had subtle bugs and behavior |
142 | // differences on all browsers, and the performance win was not large enough |
143 | // to justify all the issues. This may change in the future as browser |
144 | // implementations get better. |
145 | // |
146 | // assertSerialize in json_test contains if branches for the cases |
147 | // that fail. |
148 | return new goog.json.Serializer(opt_replacer).serialize(object); |
149 | }; |
150 | |
151 | |
152 | |
153 | /** |
154 | * Class that is used to serialize JSON objects to a string. |
155 | * @param {?goog.json.Replacer=} opt_replacer Replacer. |
156 | * @constructor |
157 | */ |
158 | goog.json.Serializer = function(opt_replacer) { |
159 | /** |
160 | * @type {goog.json.Replacer|null|undefined} |
161 | * @private |
162 | */ |
163 | this.replacer_ = opt_replacer; |
164 | }; |
165 | |
166 | |
167 | /** |
168 | * Serializes an object or a value to a JSON string. |
169 | * |
170 | * @param {*} object The object to serialize. |
171 | * @throws Error if there are loops in the object graph. |
172 | * @return {string} A JSON string representation of the input. |
173 | */ |
174 | goog.json.Serializer.prototype.serialize = function(object) { |
175 | var sb = []; |
176 | this.serialize_(object, sb); |
177 | return sb.join(''); |
178 | }; |
179 | |
180 | |
181 | /** |
182 | * Serializes a generic value to a JSON string |
183 | * @private |
184 | * @param {*} object The object to serialize. |
185 | * @param {Array} sb Array used as a string builder. |
186 | * @throws Error if there are loops in the object graph. |
187 | */ |
188 | goog.json.Serializer.prototype.serialize_ = function(object, sb) { |
189 | switch (typeof object) { |
190 | case 'string': |
191 | this.serializeString_(/** @type {string} */ (object), sb); |
192 | break; |
193 | case 'number': |
194 | this.serializeNumber_(/** @type {number} */ (object), sb); |
195 | break; |
196 | case 'boolean': |
197 | sb.push(object); |
198 | break; |
199 | case 'undefined': |
200 | sb.push('null'); |
201 | break; |
202 | case 'object': |
203 | if (object == null) { |
204 | sb.push('null'); |
205 | break; |
206 | } |
207 | if (goog.isArray(object)) { |
208 | this.serializeArray(/** @type {!Array} */ (object), sb); |
209 | break; |
210 | } |
211 | // should we allow new String, new Number and new Boolean to be treated |
212 | // as string, number and boolean? Most implementations do not and the |
213 | // need is not very big |
214 | this.serializeObject_(/** @type {Object} */ (object), sb); |
215 | break; |
216 | case 'function': |
217 | // Skip functions. |
218 | // TODO(user) Should we return something here? |
219 | break; |
220 | default: |
221 | throw Error('Unknown type: ' + typeof object); |
222 | } |
223 | }; |
224 | |
225 | |
226 | /** |
227 | * Character mappings used internally for goog.string.quote |
228 | * @private |
229 | * @type {Object} |
230 | */ |
231 | goog.json.Serializer.charToJsonCharCache_ = { |
232 | '\"': '\\"', |
233 | '\\': '\\\\', |
234 | '/': '\\/', |
235 | '\b': '\\b', |
236 | '\f': '\\f', |
237 | '\n': '\\n', |
238 | '\r': '\\r', |
239 | '\t': '\\t', |
240 | |
241 | '\x0B': '\\u000b' // '\v' is not supported in JScript |
242 | }; |
243 | |
244 | |
245 | /** |
246 | * Regular expression used to match characters that need to be replaced. |
247 | * The S60 browser has a bug where unicode characters are not matched by |
248 | * regular expressions. The condition below detects such behaviour and |
249 | * adjusts the regular expression accordingly. |
250 | * @private |
251 | * @type {RegExp} |
252 | */ |
253 | goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ? |
254 | /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g; |
255 | |
256 | |
257 | /** |
258 | * Serializes a string to a JSON string |
259 | * @private |
260 | * @param {string} s The string to serialize. |
261 | * @param {Array} sb Array used as a string builder. |
262 | */ |
263 | goog.json.Serializer.prototype.serializeString_ = function(s, sb) { |
264 | // The official JSON implementation does not work with international |
265 | // characters. |
266 | sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) { |
267 | // caching the result improves performance by a factor 2-3 |
268 | if (c in goog.json.Serializer.charToJsonCharCache_) { |
269 | return goog.json.Serializer.charToJsonCharCache_[c]; |
270 | } |
271 | |
272 | var cc = c.charCodeAt(0); |
273 | var rv = '\\u'; |
274 | if (cc < 16) { |
275 | rv += '000'; |
276 | } else if (cc < 256) { |
277 | rv += '00'; |
278 | } else if (cc < 4096) { // \u1000 |
279 | rv += '0'; |
280 | } |
281 | return goog.json.Serializer.charToJsonCharCache_[c] = rv + cc.toString(16); |
282 | }), '"'); |
283 | }; |
284 | |
285 | |
286 | /** |
287 | * Serializes a number to a JSON string |
288 | * @private |
289 | * @param {number} n The number to serialize. |
290 | * @param {Array} sb Array used as a string builder. |
291 | */ |
292 | goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) { |
293 | sb.push(isFinite(n) && !isNaN(n) ? n : 'null'); |
294 | }; |
295 | |
296 | |
297 | /** |
298 | * Serializes an array to a JSON string |
299 | * @param {Array} arr The array to serialize. |
300 | * @param {Array} sb Array used as a string builder. |
301 | * @protected |
302 | */ |
303 | goog.json.Serializer.prototype.serializeArray = function(arr, sb) { |
304 | var l = arr.length; |
305 | sb.push('['); |
306 | var sep = ''; |
307 | for (var i = 0; i < l; i++) { |
308 | sb.push(sep); |
309 | |
310 | var value = arr[i]; |
311 | this.serialize_( |
312 | this.replacer_ ? this.replacer_.call(arr, String(i), value) : value, |
313 | sb); |
314 | |
315 | sep = ','; |
316 | } |
317 | sb.push(']'); |
318 | }; |
319 | |
320 | |
321 | /** |
322 | * Serializes an object to a JSON string |
323 | * @private |
324 | * @param {Object} obj The object to serialize. |
325 | * @param {Array} sb Array used as a string builder. |
326 | */ |
327 | goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) { |
328 | sb.push('{'); |
329 | var sep = ''; |
330 | for (var key in obj) { |
331 | if (Object.prototype.hasOwnProperty.call(obj, key)) { |
332 | var value = obj[key]; |
333 | // Skip functions. |
334 | // TODO(ptucker) Should we return something for function properties? |
335 | if (typeof value != 'function') { |
336 | sb.push(sep); |
337 | this.serializeString_(key, sb); |
338 | sb.push(':'); |
339 | |
340 | this.serialize_( |
341 | this.replacer_ ? this.replacer_.call(obj, key, value) : value, |
342 | sb); |
343 | |
344 | sep = ','; |
345 | } |
346 | } |
347 | } |
348 | sb.push('}'); |
349 | }; |