You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
8.4 KiB
268 lines
8.4 KiB
/* |
|
Copyright (c) 2014, Yahoo! Inc. All rights reserved. |
|
Copyrights licensed under the New BSD License. |
|
See the accompanying LICENSE file for terms. |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var randomBytes = require('randombytes'); |
|
|
|
// Generate an internal UID to make the regexp pattern harder to guess. |
|
var UID_LENGTH = 16; |
|
var UID = generateUID(); |
|
var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID + '-(\\d+)__@"', 'g'); |
|
|
|
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; |
|
var IS_PURE_FUNCTION = /function.*?\(/; |
|
var IS_ARROW_FUNCTION = /.*?=>.*?/; |
|
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; |
|
|
|
var RESERVED_SYMBOLS = ['*', 'async']; |
|
|
|
// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their |
|
// Unicode char counterparts which are safe to use in JavaScript strings. |
|
var ESCAPED_CHARS = { |
|
'<' : '\\u003C', |
|
'>' : '\\u003E', |
|
'/' : '\\u002F', |
|
'\u2028': '\\u2028', |
|
'\u2029': '\\u2029' |
|
}; |
|
|
|
function escapeUnsafeChars(unsafeChar) { |
|
return ESCAPED_CHARS[unsafeChar]; |
|
} |
|
|
|
function generateUID() { |
|
var bytes = randomBytes(UID_LENGTH); |
|
var result = ''; |
|
for(var i=0; i<UID_LENGTH; ++i) { |
|
result += bytes[i].toString(16); |
|
} |
|
return result; |
|
} |
|
|
|
function deleteFunctions(obj){ |
|
var functionKeys = []; |
|
for (var key in obj) { |
|
if (typeof obj[key] === "function") { |
|
functionKeys.push(key); |
|
} |
|
} |
|
for (var i = 0; i < functionKeys.length; i++) { |
|
delete obj[functionKeys[i]]; |
|
} |
|
} |
|
|
|
module.exports = function serialize(obj, options) { |
|
options || (options = {}); |
|
|
|
// Backwards-compatibility for `space` as the second argument. |
|
if (typeof options === 'number' || typeof options === 'string') { |
|
options = {space: options}; |
|
} |
|
|
|
var functions = []; |
|
var regexps = []; |
|
var dates = []; |
|
var maps = []; |
|
var sets = []; |
|
var arrays = []; |
|
var undefs = []; |
|
var infinities= []; |
|
var bigInts = []; |
|
var urls = []; |
|
|
|
// Returns placeholders for functions and regexps (identified by index) |
|
// which are later replaced by their string representation. |
|
function replacer(key, value) { |
|
|
|
// For nested function |
|
if(options.ignoreFunction){ |
|
deleteFunctions(value); |
|
} |
|
|
|
if (!value && value !== undefined) { |
|
return value; |
|
} |
|
|
|
// If the value is an object w/ a toJSON method, toJSON is called before |
|
// the replacer runs, so we use this[key] to get the non-toJSONed value. |
|
var origValue = this[key]; |
|
var type = typeof origValue; |
|
|
|
if (type === 'object') { |
|
if(origValue instanceof RegExp) { |
|
return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Date) { |
|
return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Map) { |
|
return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Set) { |
|
return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if(origValue instanceof Array) { |
|
var isSparse = origValue.filter(function(){return true}).length !== origValue.length; |
|
if (isSparse) { |
|
return '@__A-' + UID + '-' + (arrays.push(origValue) - 1) + '__@'; |
|
} |
|
} |
|
|
|
if(origValue instanceof URL) { |
|
return '@__L-' + UID + '-' + (urls.push(origValue) - 1) + '__@'; |
|
} |
|
} |
|
|
|
if (type === 'function') { |
|
return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if (type === 'undefined') { |
|
return '@__U-' + UID + '-' + (undefs.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if (type === 'number' && !isNaN(origValue) && !isFinite(origValue)) { |
|
return '@__I-' + UID + '-' + (infinities.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
if (type === 'bigint') { |
|
return '@__B-' + UID + '-' + (bigInts.push(origValue) - 1) + '__@'; |
|
} |
|
|
|
return value; |
|
} |
|
|
|
function serializeFunc(fn) { |
|
var serializedFn = fn.toString(); |
|
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) { |
|
throw new TypeError('Serializing native function: ' + fn.name); |
|
} |
|
|
|
// pure functions, example: {key: function() {}} |
|
if(IS_PURE_FUNCTION.test(serializedFn)) { |
|
return serializedFn; |
|
} |
|
|
|
// arrow functions, example: arg1 => arg1+5 |
|
if(IS_ARROW_FUNCTION.test(serializedFn)) { |
|
return serializedFn; |
|
} |
|
|
|
var argsStartsAt = serializedFn.indexOf('('); |
|
var def = serializedFn.substr(0, argsStartsAt) |
|
.trim() |
|
.split(' ') |
|
.filter(function(val) { return val.length > 0 }); |
|
|
|
var nonReservedSymbols = def.filter(function(val) { |
|
return RESERVED_SYMBOLS.indexOf(val) === -1 |
|
}); |
|
|
|
// enhanced literal objects, example: {key() {}} |
|
if(nonReservedSymbols.length > 0) { |
|
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function' |
|
+ (def.join('').indexOf('*') > -1 ? '*' : '') |
|
+ serializedFn.substr(argsStartsAt); |
|
} |
|
|
|
// arrow functions |
|
return serializedFn; |
|
} |
|
|
|
// Check if the parameter is function |
|
if (options.ignoreFunction && typeof obj === "function") { |
|
obj = undefined; |
|
} |
|
// Protects against `JSON.stringify()` returning `undefined`, by serializing |
|
// to the literal string: "undefined". |
|
if (obj === undefined) { |
|
return String(obj); |
|
} |
|
|
|
var str; |
|
|
|
// Creates a JSON string representation of the value. |
|
// NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args. |
|
if (options.isJSON && !options.space) { |
|
str = JSON.stringify(obj); |
|
} else { |
|
str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space); |
|
} |
|
|
|
// Protects against `JSON.stringify()` returning `undefined`, by serializing |
|
// to the literal string: "undefined". |
|
if (typeof str !== 'string') { |
|
return String(str); |
|
} |
|
|
|
// Replace unsafe HTML and invalid JavaScript line terminator chars with |
|
// their safe Unicode char counterpart. This _must_ happen before the |
|
// regexps and functions are serialized and added back to the string. |
|
if (options.unsafe !== true) { |
|
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars); |
|
} |
|
|
|
if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0 && arrays.length === 0 && undefs.length === 0 && infinities.length === 0 && bigInts.length === 0 && urls.length === 0) { |
|
return str; |
|
} |
|
|
|
// Replaces all occurrences of function, regexp, date, map and set placeholders in the |
|
// JSON string with their string representations. If the original value can |
|
// not be found, then `undefined` is used. |
|
return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) { |
|
// The placeholder may not be preceded by a backslash. This is to prevent |
|
// replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting |
|
// invalid JS. |
|
if (backSlash) { |
|
return match; |
|
} |
|
|
|
if (type === 'D') { |
|
return "new Date(\"" + dates[valueIndex].toISOString() + "\")"; |
|
} |
|
|
|
if (type === 'R') { |
|
return "new RegExp(" + serialize(regexps[valueIndex].source) + ", \"" + regexps[valueIndex].flags + "\")"; |
|
} |
|
|
|
if (type === 'M') { |
|
return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")"; |
|
} |
|
|
|
if (type === 'S') { |
|
return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")"; |
|
} |
|
|
|
if (type === 'A') { |
|
return "Array.prototype.slice.call(" + serialize(Object.assign({ length: arrays[valueIndex].length }, arrays[valueIndex]), options) + ")"; |
|
} |
|
|
|
if (type === 'U') { |
|
return 'undefined' |
|
} |
|
|
|
if (type === 'I') { |
|
return infinities[valueIndex]; |
|
} |
|
|
|
if (type === 'B') { |
|
return "BigInt(\"" + bigInts[valueIndex] + "\")"; |
|
} |
|
|
|
if (type === 'L') { |
|
return "new URL(\"" + urls[valueIndex].toString() + "\")"; |
|
} |
|
|
|
var fn = functions[valueIndex]; |
|
|
|
return serializeFunc(fn); |
|
}); |
|
}
|
|
|