Skip to content

Commit

Permalink
Merge pull request #17 from yahoo/perf
Browse files Browse the repository at this point in the history
Improve perf via `isJSON` option
  • Loading branch information
ericf committed May 31, 2016
2 parents c5b0b93 + e7015c3 commit 2d5c259
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 20 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_js:
- "0.12"
- "4"
- "5"
- "6"
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
61 changes: 44 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,69 @@ 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',
'\u2028': '\\u2028',
'\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".
Expand All @@ -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;
Expand All @@ -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();
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 10 additions & 2 deletions test/benchmark/serialize.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

var Benchmark = require('benchmark'),
serialize = require('../../');
var Benchmark = require('benchmark');
var serialize = require('../../');

var suiteConfig = {
onStart: function (e) {
Expand Down Expand Up @@ -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);
})
Expand Down
44 changes: 44 additions & 0 deletions test/unit/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,48 @@ describe('serialize( obj )', function () {
expect(eval(serialize('</script>'))).to.equal('</script>');
});
});

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]');
});
});
});

0 comments on commit 2d5c259

Please sign in to comment.