| 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 | }; |