diff --git a/.travis.yml b/.travis.yml index 109ef3d..61e4237 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,4 @@ node_js: - "0.12" - "4" - "5" + - "6" diff --git a/README.md b/README.md index 215e378..ab38f70 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,28 @@ The above will produce the following string, HTML-escaped output which is safe t '{"haxorXSS":"\\u003C\\u002Fscript\\u003E"}' ``` +### Options + +The `serialize()` function accepts `options` as its second argument. There are two options, both default to being `undefined`: + +#### `options.space` + +This option is the same as the `space` argument that can be passed to [`JSON.stringify`][JSON.stringify]. It can be used to add whitespace and indentation to the serialized output to make it more readable. + +```js +serialize(obj, {space: 2}); +``` + +#### `options.isJSON` + +This option is a signal to `serialize()` that the object being serialized does not contain any function or regexps values. This enables a hot-path that allows serialization to be over 3x faster. If you're serializing a lot of data, and know its pure JSON, then you can enable this option for a speed-up. + +**Note:** That when using this option, the output will still be escaped to protect against XSS. + +```js +serialize(obj, {isJSON: true}); +``` + ## License This software is free to use under the Yahoo! Inc. BSD license. @@ -79,4 +101,5 @@ See the [LICENSE file][LICENSE] for license text and copyright information. [travis]: https://travis-ci.org/yahoo/serialize-javascript [travis-badge]: https://img.shields.io/travis/yahoo/serialize-javascript.svg?style=flat-square [express-state]: https://github.com/yahoo/express-state +[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify [LICENSE]: https://github.com/yahoo/serialize-javascript/blob/master/LICENSE diff --git a/index.js b/index.js index 12cee92..0f4c3d3 100644 --- a/index.js +++ b/index.js @@ -10,14 +10,14 @@ var isRegExp = require('util').isRegExp; // Generate an internal UID to make the regexp pattern harder to guess. var UID = Math.floor(Math.random() * 0x10000000000).toString(16); -var PLACE_HOLDER_REGEXP = new RegExp('"@__(FUNCTION|REGEXP)-' + UID + '-(\\d+)__@"', 'g'); +var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R)-' + UID + '-(\\d+)__@"', 'g'); var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; // Mapping of unsafe HTML and invalid JavaScript line terminator chars to their // Unicode char counterparts which are safe to use in JavaScript strings. -var UNICODE_CHARS = { +var ESCAPED_CHARS = { '<' : '\\u003C', '>' : '\\u003E', '/' : '\\u002F', @@ -25,25 +25,54 @@ var UNICODE_CHARS = { '\u2029': '\\u2029' }; -module.exports = function serialize(obj, space) { +function escapeUnsafeChars(unsafeChar) { + return ESCAPED_CHARS[unsafeChar]; +} + +module.exports = function serialize(obj, options) { + options || (options = {}); + + // Backwards-compatability for `space` as the second argument. + if (typeof options === 'number' || typeof options === 'string') { + options = {space: options}; + } + var functions = []; var regexps = []; - var str; - // Creates a JSON string representation of the object and uses placeholders - // for functions and regexps (identified by index) which are later - // replaced. - str = JSON.stringify(obj, function (key, value) { - if (typeof value === 'function') { - return '@__FUNCTION-' + UID + '-' + (functions.push(value) - 1) + '__@'; + // Returns placeholders for functions and regexps (identified by index) + // which are later replaced by their string representation. + function replacer(key, value) { + if (!value) { + return value; + } + + var type = typeof value; + + if (type === 'object') { + if (isRegExp(value)) { + return '@__R-' + UID + '-' + (regexps.push(value) - 1) + '__@'; + } + + return value; } - if (typeof value === 'object' && isRegExp(value)) { - return '@__REGEXP-' + UID + '-' + (regexps.push(value) - 1) + '__@'; + if (type === 'function') { + return '@__F-' + UID + '-' + (functions.push(value) - 1) + '__@'; } return value; - }, space); + } + + 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". @@ -54,9 +83,7 @@ module.exports = function serialize(obj, space) { // 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. - str = str.replace(UNSAFE_CHARS_REGEXP, function (unsafeChar) { - return UNICODE_CHARS[unsafeChar]; - }); + str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars); if (functions.length === 0 && regexps.length === 0) { return str; @@ -66,7 +93,7 @@ module.exports = function serialize(obj, space) { // 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, type, valueIndex) { - if (type === 'REGEXP') { + if (type === 'R') { return regexps[valueIndex].toString(); } diff --git a/package.json b/package.json index 3782e97..cd280e1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Serialize JavaScript to a superset of JSON that includes regular expressions and functions.", "main": "index.js", "scripts": { - "benchmark": "node test/benchmark/serialize.js", + "benchmark": "node -v && node test/benchmark/serialize.js", "test": "istanbul cover -- ./node_modules/mocha/bin/_mocha test/unit/ --reporter spec" }, "repository": { diff --git a/test/benchmark/serialize.js b/test/benchmark/serialize.js index 4a698af..aa98fcb 100644 --- a/test/benchmark/serialize.js +++ b/test/benchmark/serialize.js @@ -1,7 +1,7 @@ 'use strict'; -var Benchmark = require('benchmark'), - serialize = require('../../'); +var Benchmark = require('benchmark'); +var serialize = require('../../'); var suiteConfig = { onStart: function (e) { @@ -31,6 +31,14 @@ new Benchmark.Suite('simpleObj', suiteConfig) .add('JSON.stringify( simpleObj )', function () { JSON.stringify(simpleObj); }) + .add('JSON.stringify( simpleObj ) with replacer', function () { + JSON.stringify(simpleObj, function (key, value) { + return value; + }); + }) + .add('serialize( simpleObj, {isJSON: true} )', function () { + serialize(simpleObj, {isJSON: true}); + }) .add('serialize( simpleObj )', function () { serialize(simpleObj); }) diff --git a/test/unit/serialize.js b/test/unit/serialize.js index c694a65..882351e 100644 --- a/test/unit/serialize.js +++ b/test/unit/serialize.js @@ -169,4 +169,48 @@ describe('serialize( obj )', function () { expect(eval(serialize(''))).to.equal(''); }); }); + + describe('options', function () { + it('should accept options as the second argument', function () { + expect(serialize('foo', {})).to.equal('"foo"'); + }); + + it('should accept a `space` option', function () { + expect(serialize([1], {space: 0})).to.equal('[1]'); + expect(serialize([1], {space: ''})).to.equal('[1]'); + expect(serialize([1], {space: undefined})).to.equal('[1]'); + expect(serialize([1], {space: null})).to.equal('[1]'); + expect(serialize([1], {space: false})).to.equal('[1]'); + + expect(serialize([1], {space: 1})).to.equal('[\n 1\n]'); + expect(serialize([1], {space: ' '})).to.equal('[\n 1\n]'); + expect(serialize([1], {space: 2})).to.equal('[\n 1\n]'); + }); + + it('should accept a `isJSON` option', function () { + expect(serialize('foo', {isJSON: true})).to.equal('"foo"'); + expect(serialize('foo', {isJSON: false})).to.equal('"foo"'); + + function fn() { return true; } + + expect(serialize(fn)).to.equal('function fn() { return true; }'); + expect(serialize(fn, {isJSON: false})).to.equal('function fn() { return true; }'); + + expect(serialize(fn, {isJSON: true})).to.equal('undefined'); + }); + }); + + describe('backwards-compatability', function () { + it('should accept `space` as the second argument', function () { + expect(serialize([1], 0)).to.equal('[1]'); + expect(serialize([1], '')).to.equal('[1]'); + expect(serialize([1], undefined)).to.equal('[1]'); + expect(serialize([1], null)).to.equal('[1]'); + expect(serialize([1], false)).to.equal('[1]'); + + expect(serialize([1], 1)).to.equal('[\n 1\n]'); + expect(serialize([1], ' ')).to.equal('[\n 1\n]'); + expect(serialize([1], 2)).to.equal('[\n 1\n]'); + }); + }); });