pax_global_header00006660000000000000000000000064152666722120014523gustar00rootroot0000000000000052 comment=05c5ceb53e1e876c483710f9f3f3a2e9e0ab1dc2 yahoo-serialize-javascript-cd47d56/000077500000000000000000000000001526667221200174155ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/.github/000077500000000000000000000000001526667221200207555ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/.github/dependabot.yml000066400000000000000000000002351526667221200236050ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: monthly time: "13:00" open-pull-requests-limit: 10 yahoo-serialize-javascript-cd47d56/.github/workflows/000077500000000000000000000000001526667221200230125ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/.github/workflows/publish.yml000066400000000000000000000005761526667221200252130ustar00rootroot00000000000000name: Publish on: push: tags: - 'v*' permissions: id-token: write contents: read jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: 24 - run: npm ci - run: npm test - run: npm publish yahoo-serialize-javascript-cd47d56/.github/workflows/test.yml000066400000000000000000000007101526667221200245120ustar00rootroot00000000000000name: Test on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [20, 22, 24, 26] steps: - name: Checkout uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test yahoo-serialize-javascript-cd47d56/.gitignore000066400000000000000000000000151526667221200214010ustar00rootroot00000000000000node_modules yahoo-serialize-javascript-cd47d56/.npmignore000066400000000000000000000000441526667221200214120ustar00rootroot00000000000000test/ .gitignore .npmignore .github yahoo-serialize-javascript-cd47d56/LICENSE000066400000000000000000000027161526667221200204300ustar00rootroot00000000000000Copyright 2014 Yahoo! Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Yahoo! Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. yahoo-serialize-javascript-cd47d56/README.md000066400000000000000000000146101526667221200206760ustar00rootroot00000000000000Serialize JavaScript ==================== Serialize JavaScript to a _superset_ of JSON that includes regular expressions, dates and functions. [![npm Version][npm-badge]][npm] ![Test](https://github.com/yahoo/serialize-javascript/workflows/Test/badge.svg) ## Overview The code in this package began its life as an internal module to [express-state][]. To expand its usefulness, it now lives as `serialize-javascript` — an independent package on npm. You're probably wondering: **What about `JSON.stringify()`!?** We've found that sometimes we need to serialize JavaScript **functions**, **regexps**, **dates**, **sets** or **maps**. A great example is a web app that uses client-side URL routing where the route definitions are regexps that need to be shared from the server to the client. The string returned from this package's single export function is literal JavaScript which can be saved to a `.js` file, or be embedded into an HTML document by making the content of a `' }); ``` The above will produce the following string, HTML-escaped output which is safe to put into an HTML document as it will not cause the inline script element to terminate: ```js '{"haxorXSS":"\\u003C\\u002Fscript\\u003E"}' ``` > You can pass an optional `unsafe` argument to `serialize()` for straight serialization. ### Options The `serialize()` function accepts an `options` object as its second argument. All options are being defaulted to `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}); ``` #### `options.unsafe` This option is to signal `serialize()` that we want to do a straight conversion, without the XSS protection. This options needs to be explicitly set to `true`. HTML characters and JavaScript line terminators will not be escaped. You will have to roll your own. ```js serialize(obj, {unsafe: true}); ``` #### `options.ignoreFunction` This option is to signal `serialize()` that we do not want serialize JavaScript function. Just treat function like `JSON.stringify` do, but other features will work as expected. ```js serialize(obj, {ignoreFunction: true}); ``` ## Deserializing For some use cases you might also need to deserialize the string. This is explicitly not part of this module. However, you can easily write it yourself: ```js function deserialize(serializedJavascript){ return eval('(' + serializedJavascript + ')'); } ``` **Note:** Don't forget the parentheses around the serialized javascript, as the opening bracket `{` will be considered to be the start of a body. ## License This software is free to use under the Yahoo! Inc. BSD license. See the [LICENSE file][LICENSE] for license text and copyright information. [npm]: https://www.npmjs.org/package/serialize-javascript [npm-badge]: https://img.shields.io/npm/v/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/main/LICENSE [worker threads]: https://nodejs.org/api/worker_threads.html [closed-over variables]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures yahoo-serialize-javascript-cd47d56/SECURITY.md000066400000000000000000000032131526667221200212050ustar00rootroot00000000000000# Security Policy ## Supported Versions Security fixes are released for the latest major version only. | Version | Supported | | ------- | --------- | | 7.x | Yes | | < 7.0 | No | ## Reporting a Vulnerability Please **do not** report security vulnerabilities through public GitHub issues, pull requests, or discussions. Instead, report them privately through GitHub's [private vulnerability reporting](https://github.com/yahoo/serialize-javascript/security/advisories/new). This opens a draft security advisory that only the maintainers can see, where we can discuss the issue with you, develop a fix, and coordinate disclosure. Please include: - The affected version(s) - A minimal reproduction (input, options, and the resulting output) - The context the output is used in (e.g. embedded in an HTML ``, or a bare `` could be supplied by a different // serialized value later in the output, so escaping stops there without // waiting for a closing `>`. For the same reason the bare form also matches // at the very end of the string: each function body is escaped on its own, so // a body ending in `]*` in the first alternative excludes `<`, which is load-bearing: // it was previously `[^>]*`, letting a single match run from one `` anywhere in the source and swallow a complete `` // in between. Only one replacement is emitted per match, so the swallowed tag // survived verbatim in the output (PSECBUGS-117112). Because a match can no // longer contain a second `<`, every `]*>|<\/script(?=$|[\t\n\f\r \/>])/gi; 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]; } // Roughly matches string literals, template literals, regex literals, and // comments, so `escapeFunctionBody` can treat their contents differently // from plain code. This is a heuristic, not a full parser, with two known // limitations: // - A template literal is treated as one opaque span, including any // `${...}` inside it. A `), comparison operators, and regex literals: only script end // tags and line terminators are escaped. function escapeFunctionBody(str) { // Record the [start, end) span of every string/template/regex literal // and comment, so a script-close match inside one can be escaped // differently from a match in plain code (see below). var stringAndCommentSpans = []; var match; STRING_OR_COMMENT_REGEXP.lastIndex = 0; while ((match = STRING_OR_COMMENT_REGEXP.exec(str))) { stringAndCommentSpans.push([match.index, match.index + match[0].length]); } // Matches and spans are both in increasing offset order, so a single // forward-moving cursor classifies every match in O(n) total instead of // rescanning all spans for each match. var spanCursor = 0; str = str.replace(SCRIPT_CLOSE_REGEXP, function(scriptCloseMatch, offset) { while (spanCursor < stringAndCommentSpans.length && stringAndCommentSpans[spanCursor][1] <= offset) { spanCursor++; } var span = stringAndCommentSpans[spanCursor]; var inStringOrComment = !!span && offset >= span[0] && offset < span[1]; if (!inStringOrComment) { // In plain code, `<` and `/` may be real tokens (comparison, // regex delimiter, division, ...), so they can't be rewritten // as unicode escapes without breaking syntax. A space is a // no-op here but still breaks up the `/g, '\\u003E'); }); str = str.replace(/\u2028/g, '\\u2028'); str = str.replace(/\u2029/g, '\\u2029'); return str; } function generateUID() { var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH)); var result = ''; for(var i=0; i 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--0__@"` and thus outputting // invalid JS. if (backSlash) { return match; } if (type === 'D') { // Validate ISO string format to prevent code injection via spoofed toISOString() var isoStr = String(dates[valueIndex].toISOString()); if (!/^(?:\d{4}|[+-]\d{6})-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(isoStr)) { throw new TypeError('Invalid Date ISO string'); } return "new Date(\"" + isoStr + "\")"; } if (type === 'R') { // Sanitize flags to prevent code injection (only allow valid RegExp flag characters) var flags = String(regexps[valueIndex].flags).replace(/[^gimsuydv]/g, ''); var regexpSource = regexps[valueIndex].source; if (typeof regexpSource !== 'string') { throw new TypeError('RegExp.source must be a string'); } return "new RegExp(" + serialize(regexpSource) + ", \"" + 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') { var urlStr = urls[valueIndex].toString(); if (typeof urlStr !== 'string') { throw new TypeError('URL.toString() must return a string'); } return "new URL(" + serialize(urlStr, options) + ")"; } var fn = functions[valueIndex]; return serializeFunc(fn, options); }); } yahoo-serialize-javascript-cd47d56/package-lock.json000066400000000000000000000023331526667221200226320ustar00rootroot00000000000000{ "name": "serialize-javascript", "version": "7.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serialize-javascript", "version": "7.1.4", "license": "BSD-3-Clause", "devDependencies": { "benchmark": "^2.1.4" }, "engines": { "node": ">=20.0.0" } }, "node_modules/benchmark": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", "dev": true, "dependencies": { "lodash": "^4.17.4", "platform": "^1.3.3" } }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "node_modules/platform": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", "dev": true } } } yahoo-serialize-javascript-cd47d56/package.json000066400000000000000000000015131526667221200217030ustar00rootroot00000000000000{ "name": "serialize-javascript", "version": "7.1.4", "description": "Serialize JavaScript to a superset of JSON that includes regular expressions and functions.", "main": "index.js", "scripts": { "benchmark": "node -v && node test/benchmark/serialize.js", "test": "node --test test/unit/*.js" }, "repository": { "type": "git", "url": "git+https://github.com/yahoo/serialize-javascript.git" }, "keywords": [ "serialize", "serialization", "javascript", "js", "json" ], "author": "Eric Ferraiuolo ", "license": "BSD-3-Clause", "bugs": { "url": "https://github.com/yahoo/serialize-javascript/issues" }, "homepage": "https://github.com/yahoo/serialize-javascript", "devDependencies": { "benchmark": "^2.1.4" }, "engines": { "node": ">=20.0.0" } } yahoo-serialize-javascript-cd47d56/test/000077500000000000000000000000001526667221200203745ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/test/benchmark/000077500000000000000000000000001526667221200223265ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/test/benchmark/serialize.js000066400000000000000000000024521526667221200246560ustar00rootroot00000000000000'use strict'; var Benchmark = require('benchmark'); var serialize = require('../../'); var suiteConfig = { onStart: function (e) { console.log(e.currentTarget.name + ':'); }, onCycle: function (e) { console.log(String(e.target)); }, onComplete: function () { console.log(''); } }; // -- simpleOjb ---------------------------------------------------------------- var simpleObj = { foo: 'foo', bar: false, num: 100, arr: [1, 2, 3, 4], obj: {baz: 'baz'} }; 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, {unsafe: true} )', function () { serialize(simpleObj, {unsafe: true}); }) .add('serialize( simpleObj, {unsafe: true, isJSON: true} )', function () { serialize(simpleObj, {unsafe: true, isJSON: true}); }) .add('serialize( simpleObj )', function () { serialize(simpleObj); }) .run(); yahoo-serialize-javascript-cd47d56/test/unit/000077500000000000000000000000001526667221200213535ustar00rootroot00000000000000yahoo-serialize-javascript-cd47d56/test/unit/expanded-date-years.js000066400000000000000000000011461526667221200255370ustar00rootroot00000000000000const { describe, it } = require('node:test'); const { strictEqual } = require('node:assert'); const serialize = require('../../'); describe('Dates with expanded ISO years', function () { for (const timestamp of [-8640000000000000, -62198755200000, 253402300800000, 8640000000000000]) { it('round trips timestamp ' + timestamp, function () { const date = new Date(timestamp); const decoded = eval('(' + serialize(date) + ')'); strictEqual(decoded.getTime(), date.getTime()); strictEqual(decoded.toISOString(), date.toISOString()); }); } }); yahoo-serialize-javascript-cd47d56/test/unit/method-bodies.js000066400000000000000000000060231526667221200244350ustar00rootroot00000000000000const { describe, it } = require('node:test'); const { strictEqual } = require('node:assert'); const serialize = require('../../'); describe('method bodies containing other functions', function () { it('round trips a method containing an arrow with destructured parameters', function () { const input = { method() { const nested = ({ value }) => value; return nested({ value: 42 }); } }; const output = eval('(' + serialize(input) + ')'); strictEqual(output.method(), 42); }); it('round trips a method containing a function declaration', function () { const input = { method() { function nested() { return 42; } return nested(); } }; const output = eval('(' + serialize(input) + ')'); strictEqual(output.method(), 42); }); it('preserves async methods containing arrows', async function () { const input = { async method() { const nested = () => 42; return nested(); } }; const output = eval('(' + serialize(input) + ')'); strictEqual(await output.method(), 42); }); it('preserves generator methods containing arrows', function () { const input = { *method() { const nested = () => 42; yield nested(); } }; const output = eval('(' + serialize(input) + ')'); strictEqual(output.method().next().value, 42); }); }); describe('outer function signatures', function () { it('round trips a class with a nested arrow', function () { class A { method() { return value => value; } } const output = eval('(' + serialize({ A }) + ')'); strictEqual(new output.A().method()(42), 42); strictEqual(output.A.toString(), A.toString()); }); it('round trips a class without nested functions', function () { class A { method() { return 42; } } const output = eval('(' + serialize({ A }) + ')'); strictEqual(new output.A().method(), 42); strictEqual(output.A.toString(), A.toString()); }); it('still converts concise methods named class', function () { const input = { class () { return 42; } }; const output = eval('(' + serialize(input) + ')'); strictEqual(output.class(), 42); }); it('preserves async signatures with comments instead of spaces', async function () { for (const fn of [async/**/function () { return 42; }, async/**/() => 42]) { const output = eval('(' + serialize(fn) + ')'); strictEqual(await output(), 42); strictEqual(output.constructor.name, 'AsyncFunction'); } }); it('preserves arrows whose default arguments contain functions', function () { const fn = (callback = function () { return 42; }) => callback(); const output = eval('(' + serialize(fn) + ')'); strictEqual(output(), 42); }); }); yahoo-serialize-javascript-cd47d56/test/unit/serialize.js000066400000000000000000001222611526667221200237040ustar00rootroot00000000000000const { beforeEach, describe, it } = require('node:test'); const { deepStrictEqual, strictEqual, throws } = require('node:assert'); var serialize = require('../../'); describe('serialize( obj )', function () { it('should be a function', function () { strictEqual(typeof serialize, 'function'); }); describe('undefined', function () { it('should serialize `undefined` to a string', function () { strictEqual(typeof serialize(), 'string'); strictEqual(serialize(), 'undefined'); strictEqual(typeof serialize(undefined), 'string'); strictEqual(serialize(undefined), 'undefined'); }); it('should deserialize "undefined" to `undefined`', function () { strictEqual(eval(serialize()), undefined); strictEqual(eval(serialize(undefined)), undefined); }); }); describe('null', function () { it('should serialize `null` to a string', function () { strictEqual(typeof serialize(null), 'string'); strictEqual(serialize(null), 'null'); }); it('should deserialize "null" to `null`', function () { strictEqual(eval(serialize(null)), null); }); }); describe('JSON', function () { var data; beforeEach(function () { data = { str : 'string', num : 0, obj : {foo: 'foo'}, arr : [1, 2, 3], bool: true, nil : null }; }); it('should serialize JSON to a JSON string', function () { deepStrictEqual(serialize(data), JSON.stringify(data)); }); it('should deserialize a JSON string to a JSON object', function () { deepStrictEqual(JSON.parse(serialize(data)), data); }); it('should serialize weird whitespace characters correctly', function () { var ws = String.fromCharCode(8232); deepStrictEqual(eval(serialize(ws)), ws); }); it('should serialize undefined correctly', function () { var obj; var str = '{"undef":undefined,"nest":{"undef":undefined}}'; eval('obj = ' + str); deepStrictEqual(serialize(obj), str); }); }); describe('functions', function () { it('should serialize annonymous functions', function () { var fn = function () {}; strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'function () {}'); }); it('should deserialize annonymous functions', function () { var fn; eval('fn = ' + serialize(function () {})); strictEqual(typeof fn, 'function'); }); it('should serialize named functions', function () { function fn() {} strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'function fn() {}'); }); it('should deserialize named functions', function () { var fn; eval('fn = ' + serialize(function fn() {})); strictEqual(typeof fn, 'function'); strictEqual(fn.name, 'fn'); }); it('should serialize functions with arguments', function () { function fn(arg1, arg2) {} strictEqual(serialize(fn), 'function fn(arg1, arg2) {}'); }); it('should deserialize functions with arguments', function () { var fn; eval('fn = ' + serialize(function (arg1, arg2) {})); strictEqual(typeof fn, 'function'); strictEqual(fn.length, 2); }); it('should serialize functions with bodies', function () { function fn() { return true; } strictEqual(serialize(fn), 'function fn() { return true; }'); }); it('should deserialize functions with bodies', function () { var fn; eval('fn = ' + serialize(function () { return true; })); strictEqual(typeof fn, 'function'); strictEqual(fn(), true); }); it('should throw a TypeError when serializing native built-ins', function () { var err; strictEqual(Number.toString(), 'function Number() { [native code] }'); try { serialize(Number); } catch (e) { err = e; } strictEqual(err instanceof TypeError, true); }); it('should keep rejecting native built-ins after an earlier rejection', function () { // The native-code check must not carry `lastIndex` state between // calls, or every other native function would slip through. throws(function () { serialize(Math.max); }, TypeError); throws(function () { serialize(Math.min); }, TypeError); throws(function () { serialize(Math.max); }, TypeError); }); it('should throw when serializing a function with a spoofed non-string toString()', function () { var fn = function () {}; fn.toString = function () { return { toString: function () { return 'function(){}'; } }; }; throws(function () { serialize({ fn: fn }); }, TypeError); }); it('should throw when a spoofed toString() returns an object with its own replace()', function () { // Without a type check, `escapeFunctionBody()` would call this // object's `replace()`, which returns the payload unescaped. var fn = function () {}; fn.toString = function () { return { replace: function () { return this; }, toString: function () { return 'function(){}'; } }; }; throws(function () { serialize({ fn: fn }); }, TypeError); }); it('should escape script close tags from a spoofed string toString()', function () { var fn = function () {}; fn.toString = function () { return 'function(){}'; }; strictEqual(serialize({ fn: fn }).indexOf(''), -1); }); it('should serialize enhanced literal objects', function () { var obj = { foo() { return true; }, *bar() { return true; } }; deepStrictEqual(serialize(obj), '{"foo":function() { return true; },"bar":function*() { return true; }}'); }); it('should deserialize enhanced literal objects', function () { var obj; eval('obj = ' + serialize({ hello() { return true; } })); strictEqual(obj.hello(), true); }); it('should serialize functions that contain dates', function () { function fn(arg1) {return new Date('2016-04-28T22:02:17.156Z')}; strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'function fn(arg1) {return new Date(\'2016-04-28T22:02:17.156Z\')}'); }); it('should deserialize functions that contain dates', function () { var fn; eval('fn = ' + serialize(function () { return new Date('2016-04-28T22:02:17.156Z') })); strictEqual(typeof fn, 'function'); strictEqual(fn().getTime(), new Date('2016-04-28T22:02:17.156Z').getTime()); }); it('should serialize functions that return other functions', function () { function fn() {return function(arg1) {return arg1 + 5}}; strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'function fn() {return function(arg1) {return arg1 + 5}}'); }); it('should deserialize functions that return other functions', function () { var fn; eval('fn = ' + serialize(function () { return function(arg1) {return arg1 + 5} })); strictEqual(typeof fn, 'function'); strictEqual(fn()(7), 12); }); }); describe('arrow-functions', function () { it('should serialize arrow functions', function () { var fn = () => {}; strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), '() => {}'); }); it('should deserialize arrow functions', function () { var fn; eval('fn = ' + serialize(() => true)); strictEqual(typeof fn, 'function'); strictEqual(fn(), true); }); it('should serialize arrow functions with one argument', function () { var fn = arg1 => {} strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'arg1 => {}'); }); it('should deserialize arrow functions with one argument', function () { var fn; eval('fn = ' + serialize(arg1 => {})); strictEqual(typeof fn, 'function'); strictEqual(fn.length, 1); }); it('should serialize arrow functions with multiple arguments', function () { var fn = (arg1, arg2) => {} strictEqual(serialize(fn), '(arg1, arg2) => {}'); }); it('should deserialize arrow functions with multiple arguments', function () { var fn; eval('fn = ' + serialize( (arg1, arg2) => {})); strictEqual(typeof fn, 'function'); strictEqual(fn.length, 2); }); it('should serialize arrow functions with bodies', function () { var fn = () => { return true; } strictEqual(serialize(fn), '() => { return true; }'); }); it('should deserialize arrow functions with bodies', function () { var fn; eval('fn = ' + serialize( () => { return true; })); strictEqual(typeof fn, 'function'); strictEqual(fn(), true); }); it('should serialize enhanced literal objects', function () { var obj = { foo: () => { return true; }, bar: arg1 => { return true; }, baz: (arg1, arg2) => { return true; } }; strictEqual(serialize(obj), '{"foo":() => { return true; },"bar":arg1 => { return true; },"baz":(arg1, arg2) => { return true; }}'); }); it('should deserialize enhanced literal objects', function () { var obj; eval('obj = ' + serialize({ foo: () => { return true; }, foo: () => { return true; }, bar: arg1 => { return true; }, baz: (arg1, arg2) => { return true; } })); strictEqual(obj.foo(), true); strictEqual(obj.bar('arg1'), true); strictEqual(obj.baz('arg1', 'arg1'), true); }); it('should serialize arrow functions with added properties', function () { var fn = () => {}; fn.property1 = 'a string' strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), '() => {}'); }); it('should deserialize arrow functions with added properties', function () { var fn; eval('fn = ' + serialize( () => { this.property1 = 'a string'; return 5 })); strictEqual(typeof fn, 'function'); strictEqual(fn(), 5); }); it('should serialize arrow functions that return other functions', function () { var fn = arg1 => { return arg2 => arg1 + arg2 }; strictEqual(typeof serialize(fn), 'string'); strictEqual(serialize(fn), 'arg1 => { return arg2 => arg1 + arg2 }'); }); it('should deserialize arrow functions that return other functions', function () { var fn; eval('fn = ' + serialize(arg1 => { return arg2 => arg1 + arg2 } )); strictEqual(typeof fn, 'function'); strictEqual(fn(2)(3), 5); }); }); describe('regexps', function () { it('should serialize constructed regexps', function () { var re = new RegExp('asdf'); strictEqual(typeof serialize(re), 'string'); strictEqual(serialize(re), 'new RegExp("asdf", "")'); }); it('should deserialize constructed regexps', function () { var re = eval(serialize(new RegExp('asdf'))); strictEqual(re instanceof RegExp, true); strictEqual(re.source, 'asdf'); }); it('should serialize literal regexps', function () { var re = /asdf/; strictEqual(typeof serialize(re), 'string'); strictEqual(serialize(re), 'new RegExp("asdf", "")'); }); it('should deserialize literal regexps', function () { var re = eval(serialize(/asdf/)); strictEqual(re instanceof RegExp, true); strictEqual(re.source, 'asdf'); }); it('should serialize regexps with flags', function () { var re = /^asdf$/gi; strictEqual(serialize(re), 'new RegExp("^asdf$", "gi")'); }); it('should deserialize regexps with flags', function () { var re = eval(serialize(/^asdf$/gi)); strictEqual(re instanceof RegExp, true); strictEqual(re.global, true); strictEqual(re.ignoreCase, true); strictEqual(re.multiline, false); }); it('should serialize regexps with escaped chars', function () { strictEqual(serialize(/\..*/), 'new RegExp("\\\\..*", "")'); strictEqual(serialize(new RegExp('\\..*')), 'new RegExp("\\\\..*", "")'); }); it('should deserialize regexps with escaped chars', function () { var re = eval(serialize(/\..*/)); strictEqual(re instanceof RegExp, true); strictEqual(re.source, '\\..*'); re = eval(serialize(new RegExp('\\..*'))); strictEqual(re instanceof RegExp, true); strictEqual(re.source, '\\..*'); }); it('should serialize dangerous regexps', function () { var re = /[<\/script>'), '"\\u003C\\u002Fscript\\u003E"'); strictEqual(JSON.parse(serialize('')), ''); strictEqual(eval(serialize('')), ''); strictEqual(serialize(new URL('x:')), 'new URL("x:\\u003C\\u002Fscript\\u003E")'); strictEqual(eval(serialize(new URL('x:'))).href, 'x:'); }); it('should encode unsafe HTML chars in function bodies', function () { function fn() { return ''; } var serialized = serialize(fn); strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); strictEqual(serialized.includes(''), false); // Verify the function still works after deserialization var deserialized; eval('deserialized = ' + serialized); strictEqual(typeof deserialized, 'function'); strictEqual(deserialized(), ''); }); // A `]/i; it('should not leak a second script close tag swallowed by the first match (PSECBUGS-117112)', function () { // `x` after it until the one inside the later string. The // first regex alternative used to match across that whole span in // one go, emitting the inner `` raw. var src = "function f(x){ return x' }"; var fn = new Function('return ' + src)(); var serialized = serialize({ h: fn }); strictEqual(CLOSES_SCRIPT.test(serialized), false); // The inner tag is inside a string literal, so it unicode-escapes. strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); }); it('should escape a script close tag at the very end of a function body (PSECBUGS-118415)', function () { // Each body is escaped on its own, so one ending in a bare // ``. var f = function () {}; f.toString = function () { return 'function f(){ return 1 }// '; }; var serialized = serialize({ a: { f: f }, g: g }, { space: 2 }); strictEqual(CLOSES_SCRIPT.test(serialized), false); // Inside the line comment, so it unicode-escapes. strictEqual(serialized.includes('//\\u003C\\u002Fscript\n'), true); }); it('should neutralize a script close tag ending a function body in code position', function () { var fn = function () {}; fn.toString = function () { return 'function f(x){ return x } { return ''; }; var serialized = serialize(fn); strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); strictEqual(serialized.includes(''), false); // Verify the function still works after deserialization var deserialized; eval('deserialized = ' + serialized); strictEqual(typeof deserialized, 'function'); strictEqual(deserialized(), ''); }); it('should encode unsafe HTML chars in enhanced literal object methods', function () { var obj = { fn() { return ''; } }; var serialized = serialize(obj); strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); strictEqual(serialized.includes(''), false); // Verify the function still works after deserialization var deserialized; eval('deserialized = ' + serialized); strictEqual(deserialized.fn(), ''); }); it('should not escape function bodies when unsafe option is true', function () { function fn() { return ''; } var serialized = serialize(fn, {unsafe: true}); strictEqual(serialized.includes(''), true); strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), false); }); it('should encode with space before >', function () { function fn() { return ''; } var serialized = serialize(fn); strictEqual(serialized.includes('\\u003C\\u002Fscript'), true); strictEqual(serialized.includes(''); }); it('should encode with attributes', function () { function fn() { return ''; } var serialized = serialize(fn); strictEqual(serialized.includes('\\u003C\\u002Fscript'), true); strictEqual(serialized.includes(''); }); it('should encode ', function () { function fn() { return ''; } var serialized = serialize(fn); strictEqual(serialized.includes('\\u003C\\u002Fscript'), true); strictEqual(serialized.includes(''); }); it('should encode split script-closing payload across function bodies', function () { var serialized = serialize({ a: function () { /* */ } }); strictEqual(serialized.includes(']/i.test(serialized), false); var deserialized; eval('deserialized = ' + serialized); strictEqual(deserialized("'"), fn("'")); strictEqual(deserialized('x'), fn('x')); }); it('should not let a `/` inside a regex character class end the regex literal early', function () { // `/` inside `[...]` doesn't need to be escaped and doesn't // terminate the regex literal. If it were mistaken for the // closing delimiter, the real string literal that follows would // be misidentified and its `]/i.test(serialized), false); var deserialized; eval('deserialized = ' + serialized); strictEqual(deserialized("'"), fn("'")); strictEqual(deserialized('/'), fn('/')); strictEqual(deserialized('x'), fn('x')); }); }); describe('options', function () { it('should accept options as the second argument', function () { strictEqual(serialize('foo', {}), '"foo"'); }); it('should accept a `space` option', function () { strictEqual(serialize([1], {space: 0}), '[1]'); strictEqual(serialize([1], {space: ''}), '[1]'); strictEqual(serialize([1], {space: undefined}), '[1]'); strictEqual(serialize([1], {space: null}), '[1]'); strictEqual(serialize([1], {space: false}), '[1]'); strictEqual(serialize([1], {space: 1}), '[\n 1\n]'); strictEqual(serialize([1], {space: ' '}), '[\n 1\n]'); strictEqual(serialize([1], {space: 2}), '[\n 1\n]'); }); it('should accept a `isJSON` option', function () { strictEqual(serialize('foo', {isJSON: true}), '"foo"'); strictEqual(serialize('foo', {isJSON: false}), '"foo"'); function fn() { return true; } strictEqual(serialize(fn), 'function fn() { return true; }'); strictEqual(serialize(fn, {isJSON: false}), 'function fn() { return true; }'); strictEqual(serialize(fn, {isJSON: true}), 'undefined'); strictEqual(serialize([1], {isJSON: true, space: 2}), '[\n 1\n]'); }); it('should accept a `unsafe` option', function () { strictEqual(serialize('foo', {unsafe: true}), '"foo"'); strictEqual(serialize('foo', {unsafe: false}), '"foo"'); function fn() { return true; } strictEqual(serialize(fn), 'function fn() { return true; }'); strictEqual(serialize(fn, {unsafe: false}), 'function fn() { return true; }'); strictEqual(serialize(fn, {unsafe: undefined}), 'function fn() { return true; }'); strictEqual(serialize(fn, {unsafe: "true"}), 'function fn() { return true; }'); strictEqual(serialize(fn, {unsafe: true}), 'function fn() { return true; }'); strictEqual(serialize(["1"], {unsafe: false, space: 2}), '[\n "1"\n]'); strictEqual(serialize(["1"], {unsafe: true, space: 2}), '[\n "1"\n]'); strictEqual(serialize(["<"], {space: 2}), '[\n "\\u003C"\n]'); strictEqual(serialize(["<"], {unsafe: true, space: 2}), '[\n "<"\n]'); }); it("should accept a `ignoreFunction` option", function() { function fn() { return true; } var obj = { fn: fn, fn_arrow: () => { return true; } }; var obj2 = { num: 123, str: 'str', fn: fn } // case 1. Pass function to serialize strictEqual(serialize(fn, { ignoreFunction: true }), 'undefined'); // case 2. Pass function(arrow) in object to serialze strictEqual(serialize(obj, { ignoreFunction: true }), '{}'); // case 3. Other features should work strictEqual(serialize(obj2, { ignoreFunction: true }), '{"num":123,"str":"str"}' ); }); }); describe('ignoring functions without changing the input', function () { it('should preserve function properties in nested input objects', function () { var fn = function () {}; var obj = {nested: {fn: fn, value: 1}}; strictEqual(serialize(obj, {ignoreFunction: true}), '{"nested":{"value":1}}'); strictEqual(obj.nested.fn, fn); }); it('should accept frozen objects containing functions', function () { var obj = Object.freeze({fn: function () {}, value: 1}); strictEqual(serialize(obj, {ignoreFunction: true}), '{"value":1}'); }); it('should replace array functions with null without deleting elements', function () { var fn = function () {}; var values = [fn, 1]; strictEqual(serialize(values, {ignoreFunction: true}), '[null,1]'); strictEqual(values[0], fn); }); }); describe('backwards-compatability', function () { it('should accept `space` as the second argument', function () { strictEqual(serialize([1], 0), '[1]'); strictEqual(serialize([1], ''), '[1]'); strictEqual(serialize([1], undefined), '[1]'); strictEqual(serialize([1], null), '[1]'); strictEqual(serialize([1], false), '[1]'); strictEqual(serialize([1], 1), '[\n 1\n]'); strictEqual(serialize([1], ' '), '[\n 1\n]'); strictEqual(serialize([1], 2), '[\n 1\n]'); }); }); describe('placeholders', function() { it('should not be replaced within string literals', function () { // Since we made the UID deterministic this should always be the placeholder var fakePlaceholder = '"@__R-0000000000000000-0__@'; var serialized = serialize({bar: /1/i, foo: fakePlaceholder}, {uid: 'foo'}); var obj = eval('(' + serialized + ')'); strictEqual(typeof obj, 'object'); strictEqual(typeof obj.foo, 'string'); strictEqual(obj.foo, fakePlaceholder); }); }); });