diff --git a/.prettierrc.json b/.prettierrc.json index 82f57e9..9ba687d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,4 @@ { - "tabWidth": 2 + "tabWidth": 2, + "singleQuote": true } \ No newline at end of file diff --git a/README.md b/README.md index 06d3dec..cf89f3d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -json-bigint -=========== +# json-bigint [![Build Status](https://secure.travis-ci.org/sidorares/json-bigint.png)](http://travis-ci.org/sidorares/json-bigint) [![NPM](https://nodei.co/npm/json-bigint.png?downloads=true&stars=true)](https://nodei.co/npm/json-bigint/) -JSON.parse/stringify with bigints support. Based on Douglas Crockford [JSON.js](https://github.com/douglascrockford/JSON-js) package and [bignumber.js](https://github.com/MikeMcl/bignumber.js) library. +JSON.parse/stringify with bigints support. Based on Douglas Crockford [JSON.js](https://github.com/douglascrockford/JSON-js) package and [bignumber.js](https://github.com/MikeMcl/bignumber.js) library. Native `Bigint` was added to JS recently, so we added an option to leverage it instead of `bignumber.js`. However, the parsing with native `BigInt` is kept an option for backward compability. @@ -21,7 +20,7 @@ var json = '{ "value" : 9223372036854775807, "v2": 123 }'; console.log('Input:', json); console.log(''); -console.log('node.js bult-in JSON:') +console.log('node.js bult-in JSON:'); var r = JSON.parse(json); console.log('JSON.parse(input).value : ', r.value.toString()); console.log('JSON.stringify(JSON.parse(input)):', JSON.stringify(r)); @@ -46,35 +45,43 @@ big number JSON: JSONbig.parse(input).value : 9223372036854775807 JSONbig.stringify(JSONbig.parse(input)): {"value":9223372036854775807,"v2":123} ``` + ### Options + The behaviour of the parser is somewhat configurable through 'options' #### options.strict, boolean, default false + Specifies the parsing should be "strict" towards reporting duplicate-keys in the parsed string. The default follows what is allowed in standard json and resembles the behavior of JSON.parse, but overwrites any previous values with the last one assigned to the duplicate-key. Setting options.strict = true will fail-fast on such duplicate-key occurances and thus warn you upfront of possible lost information. example: + ```js var JSONbig = require('json-bigint'); -var JSONstrict = require('json-bigint')({"strict": true}); +var JSONstrict = require('json-bigint')({ strict: true }); var dupkeys = '{ "dupkey": "value 1", "dupkey": "value 2"}'; console.log('\n\nDuplicate Key test with both lenient and strict JSON parsing'); console.log('Input:', dupkeys); var works = JSONbig.parse(dupkeys); console.log('JSON.parse(dupkeys).dupkey: %s', works.dupkey); -var fails = "will stay like this"; +var fails = 'will stay like this'; try { - fails = JSONstrict.parse(dupkeys); - console.log('ERROR!! Should never get here'); + fails = JSONstrict.parse(dupkeys); + console.log('ERROR!! Should never get here'); } catch (e) { - console.log('Succesfully catched expected exception on duplicate keys: %j', e); + console.log( + 'Succesfully catched expected exception on duplicate keys: %j', + e + ); } ``` Output + ``` Duplicate Key test with big number JSON Input: { "dupkey": "value 1", "dupkey": "value 2"} @@ -84,24 +91,30 @@ Succesfully catched expected exception on duplicate keys: {"name":"SyntaxError", ``` #### options.storeAsString, boolean, default false + Specifies if BigInts should be stored in the object as a string, rather than the default BigNumber. Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all BigInts to be-and-stay strings). example: + ```js var JSONbig = require('json-bigint'); -var JSONbigString = require('json-bigint')({"storeAsString": true}); +var JSONbigString = require('json-bigint')({ storeAsString: true }); var key = '{ "key": 1234567890123456789 }'; console.log('\n\nStoring the BigInt as a string, instead of a BigNumber'); console.log('Input:', key); var withInt = JSONbig.parse(key); var withString = JSONbigString.parse(key); -console.log('Default type: %s, With option type: %s', typeof withInt.key, typeof withString.key); - +console.log( + 'Default type: %s, With option type: %s', + typeof withInt.key, + typeof withString.key +); ``` Output + ``` Storing the BigInt as a string, instead of a BigNumber Input: { "key": 1234567890123456789 } @@ -110,48 +123,60 @@ Default type: object, With option type: string ``` #### options.useNativeBigInt, boolean, default false + Specifies if parser uses native BigInt instead of bignumber.js example: + ```js var JSONbig = require('json-bigint'); -var JSONbigNative = require('json-bigint')({"useNativeBigInt": true}); +var JSONbigNative = require('json-bigint')({ useNativeBigInt: true }); var key = '{ "key": 993143214321423154315154321 }'; console.log(`\n\nStoring the Number as native BigInt, instead of a BigNumber`); console.log('Input:', key); var normal = JSONbig.parse(key); var nativeBigInt = JSONbigNative.parse(key); -console.log('Default type: %s, With option type: %s', typeof normal.key, typeof nativeBigInt.key); - +console.log( + 'Default type: %s, With option type: %s', + typeof normal.key, + typeof nativeBigInt.key +); ``` Output + ``` Storing the Number as native BigInt, instead of a BigNumber Input: { "key": 993143214321423154315154321 } -Default type: object, With option type: bigint +Default type: object, With option type: bigint ``` #### options.alwaysParseAsBig, boolean, default false + Specifies if all numbers should be stored as BigNumber. Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all Number to be-and-stay BigNumber) example: + ```js var JSONbig = require('json-bigint'); -var JSONbigAlways = require('json-bigint')({"alwaysParseAsBig": true}); +var JSONbigAlways = require('json-bigint')({ alwaysParseAsBig: true }); var key = '{ "key": 123 }'; // there is no need for BigNumber by default, but we're forcing it console.log(`\n\nStoring the Number as a BigNumber, instead of a Number`); console.log('Input:', key); var normal = JSONbig.parse(key); var always = JSONbigAlways.parse(key); -console.log('Default type: %s, With option type: %s', typeof normal.key, typeof always.key); - +console.log( + 'Default type: %s, With option type: %s', + typeof normal.key, + typeof always.key +); ``` Output + ``` Storing the Number as a BigNumber, instead of a Number Input: { "key": 123 } @@ -161,11 +186,33 @@ Default type: number, With option type: object If you want to force all numbers to be parsed as native `BigInt` (you probably do! Otherwise any calulations become a real headache): + +```js +var JSONbig = require('json-bigint')({ + alwaysParseAsBig: true, + useNativeBigInt: true, +}); +``` + +#### options.protoAction, boolean, default: "error". Possible values: "error", "ignore", "preserve" + +#### options.constructorAction, boolean, default: "error". Possible values: "error", "ignore", "preserve" + +Controls how `__proto__` and `constructor` properties are treated. If set to "error" they are not allowed and +parse() call will throw an error. If set to "ignore" the prroperty and it;s value is skipped from parsing and object building. +If set to "preserve" the `__proto__` property is set. One should be extra careful and make sure any other library consuming generated data +is not vulnerable to prototype poisoning attacks. + +example: + ```js -var JSONbig = require('json-bigint')({"alwaysParseAsBig": true, "useNativeBigInt": true}); +var JSONbigAlways = require('json-bigint')({ protoAction: 'ignore' }); +const user = JSONbig.parse('{ "__proto__": { "admin": true }, "id": 12345 }'); +// => result is { id: 12345 } ``` ### Links: + - [RFC4627: The application/json Media Type for JavaScript Object Notation (JSON)](http://www.ietf.org/rfc/rfc4627.txt) - [Re: \[Json\] Limitations on number size?](http://www.ietf.org/mail-archive/web/json/current/msg00297.html) - [Is there any proper way to parse JSON with large numbers? (long, bigint, int64)](http://stackoverflow.com/questions/18755125/node-js-is-there-any-proper-way-to-parse-json-with-large-numbers-long-bigint) @@ -175,17 +222,19 @@ var JSONbig = require('json-bigint')({"alwaysParseAsBig": true, "useNativeBigInt ### Note on native BigInt support #### Stringifying + Full support out-of-the-box, stringifies BigInts as pure numbers (no quotes, no `n`) #### Limitations + - Roundtrip operations `s === JSONbig.stringify(JSONbig.parse(s))` but -`o !== JSONbig.parse(JSONbig.stringify(o))` +`o !== JSONbig.parse(JSONbig.stringify(o))` -when `o` has a value with something like `123n`. +when `o` has a value with something like `123n`. -`JSONbig` stringify `123n` as `123`, which becomes `number` (aka `123` not `123n`) by default when being reparsed. +`JSONbig` stringify `123n` as `123`, which becomes `number` (aka `123` not `123n`) by default when being reparsed. -There is currently no consistent way to deal with this issue, so we decided to leave it, handling this specific case is then up to users. \ No newline at end of file +There is currently no consistent way to deal with this issue, so we decided to leave it, handling this specific case is then up to users. diff --git a/lib/parse.js b/lib/parse.js index 8229130..bb4e5eb 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,4 +1,12 @@ var BigNumber = null; + +// regexpxs extracted from +// (c) BSD-3-Clause +// https://github.com/fastify/secure-json-parse/graphs/contributors and https://github.com/hapijs/bourne/graphs/contributors + +const suspectProtoRx = /(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/; +const suspectConstructorRx = /(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/; + /* json_parse.js 2012-06-20 @@ -62,332 +70,374 @@ var BigNumber = null; */ var json_parse = function (options) { - "use strict"; - -// This is a function that can parse a JSON text, producing a JavaScript -// data structure. It is a simple, recursive descent parser. It does not use -// eval or regular expressions, so it can be used as a model for implementing -// a JSON parser in other languages. - -// We are defining the function inside of another function to avoid creating -// global variables. - - -// Default options one can override by passing options to the parse() - var _options = { - "strict": false, // not being strict means do not generate syntax errors for "duplicate key" - "storeAsString": false, // toggles whether the values should be stored as BigNumber (default) or a string - "alwaysParseAsBig": false, // toggles whether all numbers should be Big - "useNativeBigInt": false // toggles whether to use native BigInt instead of bignumber.js - }; - + 'use strict'; + + // This is a function that can parse a JSON text, producing a JavaScript + // data structure. It is a simple, recursive descent parser. It does not use + // eval or regular expressions, so it can be used as a model for implementing + // a JSON parser in other languages. + + // We are defining the function inside of another function to avoid creating + // global variables. + + // Default options one can override by passing options to the parse() + var _options = { + strict: false, // not being strict means do not generate syntax errors for "duplicate key" + storeAsString: false, // toggles whether the values should be stored as BigNumber (default) or a string + alwaysParseAsBig: false, // toggles whether all numbers should be Big + useNativeBigInt: false, // toggles whether to use native BigInt instead of bignumber.js + protoAction: 'error', + constructorAction: 'error', + }; + + // If there are options, then use them to override the default _options + if (options !== undefined && options !== null) { + if (options.strict === true) { + _options.strict = true; + } + if (options.storeAsString === true) { + _options.storeAsString = true; + } + _options.alwaysParseAsBig = + options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false; + _options.useNativeBigInt = + options.useNativeBigInt === true ? options.useNativeBigInt : false; + + if (typeof options.constructorAction !== 'undefined') { + if ( + options.constructorAction === 'error' || + options.constructorAction === 'ignore' || + options.constructorAction === 'preserve' + ) { + _options.constructorAction = options.constructorAction; + } else { + throw new Error( + `Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed ${options.constructorAction}` + ); + } + } -// If there are options, then use them to override the default _options - if (options !== undefined && options !== null) { - if (options.strict === true) { - _options.strict = true; + if (typeof options.protoAction !== 'undefined') { + if ( + options.protoAction === 'error' || + options.protoAction === 'ignore' || + options.protoAction === 'preserve' + ) { + _options.protoAction = options.protoAction; + } else { + throw new Error( + `Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed ${options.protoAction}` + ); + } + } + } + + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: '\t', + }, + text, + error = function (m) { + // Call error when something is wrong. + + throw { + name: 'SyntaxError', + message: m, + at: at, + text: text, + }; + }, + next = function (c) { + // If a c parameter is provided, verify that it matches the current character. + + if (c && c !== ch) { + error("Expected '" + c + "' instead of '" + ch + "'"); + } + + // Get the next character. When there are no more characters, + // return the empty string. + + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function () { + // Parse a number value. + + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; } - if (options.storeAsString === true) { - _options.storeAsString = true; + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); } - _options.alwaysParseAsBig = options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false - _options.useNativeBigInt = options.useNativeBigInt === true ? options.useNativeBigInt : false - } - - - var at, // The index of the current character - ch, // The current character - escapee = { - '"': '"', - '\\': '\\', - '/': '/', - b: '\b', - f: '\f', - n: '\n', - r: '\r', - t: '\t' - }, - text, - - error = function (m) { - -// Call error when something is wrong. - - throw { - name: 'SyntaxError', - message: m, - at: at, - text: text - }; - }, - - next = function (c) { - -// If a c parameter is provided, verify that it matches the current character. - - if (c && c !== ch) { - error("Expected '" + c + "' instead of '" + ch + "'"); - } - -// Get the next character. When there are no more characters, -// return the empty string. - - ch = text.charAt(at); - at += 1; - return ch; - }, - - number = function () { -// Parse a number value. - - var number, - string = ''; - - if (ch === '-') { - string = '-'; - next('-'); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - if (ch === '.') { - string += '.'; - while (next() && ch >= '0' && ch <= '9') { - string += ch; - } - } - if (ch === 'e' || ch === 'E') { - string += ch; - next(); - if (ch === '-' || ch === '+') { - string += ch; - next(); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + if (BigNumber == null) BigNumber = require('bignumber.js'); + //if (number > 9007199254740992 || number < -9007199254740992) + // Bignumber has stricter check: everything with length > 15 digits disallowed + if (string.length > 15) + return _options.storeAsString + ? string + : _options.useNativeBigInt + ? BigInt(string) + : new BigNumber(string); + else + return !_options.alwaysParseAsBig + ? number + : _options.useNativeBigInt + ? BigInt(number) + : new BigNumber(number); + } + }, + string = function () { + // Parse a string value. + + var hex, + i, + string = '', + uffff; + + // When parsing for string values, we must look for " and \ characters. + + if (ch === '"') { + var startAt = at; + while (next()) { + if (ch === '"') { + if (at - 1 > startAt) string += text.substring(startAt, at - 1); + next(); + return string; + } + if (ch === '\\') { + if (at - 1 > startAt) string += text.substring(startAt, at - 1); + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; } - } - number = +string; - if (!isFinite(number)) { - error("Bad number"); + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; } else { - if (BigNumber == null) - BigNumber = require('bignumber.js'); - //if (number > 9007199254740992 || number < -9007199254740992) - // Bignumber has stricter check: everything with length > 15 digits disallowed - if (string.length > 15) - return _options.storeAsString ? string : _options.useNativeBigInt ? BigInt(string) : new BigNumber(string); - else - return !_options.alwaysParseAsBig ? number : _options.useNativeBigInt ? BigInt(number) : new BigNumber(number); + break; } - }, - - string = function () { - -// Parse a string value. - - var hex, - i, - string = '', - uffff; - -// When parsing for string values, we must look for " and \ characters. - - if (ch === '"') { - var startAt = at; - while (next()) { - if (ch === '"') { - if (at - 1 > startAt) - string += text.substring(startAt, at - 1); - next(); - return string; - } - if (ch === '\\') { - if (at - 1 > startAt) - string += text.substring(startAt, at - 1); - next(); - if (ch === 'u') { - uffff = 0; - for (i = 0; i < 4; i += 1) { - hex = parseInt(next(), 16); - if (!isFinite(hex)) { - break; - } - uffff = uffff * 16 + hex; - } - string += String.fromCharCode(uffff); - } else if (typeof escapee[ch] === 'string') { - string += escapee[ch]; - } else { - break; - } - startAt = at; - } - } - } - error("Bad string"); - }, - - white = function () { - -// Skip whitespace. - - while (ch && ch <= ' ') { - next(); - } - }, - - word = function () { - -// true, false, or null. - - switch (ch) { - case 't': - next('t'); - next('r'); - next('u'); - next('e'); - return true; - case 'f': - next('f'); - next('a'); - next('l'); - next('s'); - next('e'); - return false; - case 'n': - next('n'); - next('u'); - next('l'); - next('l'); - return null; - } - error("Unexpected '" + ch + "'"); - }, - - value, // Place holder for the value function. - - array = function () { - -// Parse an array value. + startAt = at; + } + } + } + error('Bad string'); + }, + white = function () { + // Skip whitespace. + + while (ch && ch <= ' ') { + next(); + } + }, + word = function () { + // true, false, or null. + + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error("Unexpected '" + ch + "'"); + }, + value, // Place holder for the value function. + array = function () { + // Parse an array value. + + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function () { + // Parse an object value. - var array = []; + var key, + object = Object.create(null); - if (ch === '[') { - next('['); - white(); - if (ch === ']') { - next(']'); - return array; // empty array - } - while (ch) { - array.push(value()); - white(); - if (ch === ']') { - next(']'); - return array; - } - next(','); - white(); - } + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if ( + _options.strict === true && + Object.hasOwnProperty.call(object, key) + ) { + error('Duplicate key "' + key + '"'); + } + + if (suspectProtoRx.test(key) === true) { + if (_options.protoAction === 'error') { + error('Object contains forbidden prototype property'); + } else if (_options.protoAction === 'ignore') { + value(); + } else { + object[key] = value(); } - error("Bad array"); - }, - - object = function () { - -// Parse an object value. - - var key, - object = {}; - - if (ch === '{') { - next('{'); - white(); - if (ch === '}') { - next('}'); - return object; // empty object - } - while (ch) { - key = string(); - white(); - next(':'); - if (_options.strict === true && Object.hasOwnProperty.call(object, key)) { - error('Duplicate key "' + key + '"'); - } - object[key] = value(); - white(); - if (ch === '}') { - next('}'); - return object; - } - next(','); - white(); - } + } else if (suspectConstructorRx.test(key) === true) { + if (_options.constructorAction === 'error') { + error('Object contains forbidden constructor property'); + } else if (_options.constructorAction === 'ignore') { + value(); + } else { + object[key] = value(); } - error("Bad object"); - }; - - value = function () { - -// Parse a JSON value. It could be an object, an array, a string, a number, -// or a word. - - white(); - switch (ch) { - case '{': - return object(); - case '[': - return array(); - case '"': - return string(); - case '-': - return number(); - default: - return ch >= '0' && ch <= '9' ? number() : word(); + } else { + object[key] = value(); + } + + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); } + } + error('Bad object'); }; -// Return the json_parse function. It will have access to all of the above -// functions and variables. + value = function () { + // Parse a JSON value. It could be an object, an array, a string, a number, + // or a word. + + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; - return function (source, reviver) { - var result; + // Return the json_parse function. It will have access to all of the above + // functions and variables. - text = source + ''; - at = 0; - ch = ' '; - result = value(); - white(); - if (ch) { - error("Syntax error"); - } + return function (source, reviver) { + var result; -// If there is a reviver function, we recursively walk the new structure, -// passing each name/value pair to the reviver function for possible -// transformation, starting with a temporary root object that holds the result -// in an empty key. If there is not a reviver function, we simply return the -// result. - - return typeof reviver === 'function' - ? (function walk(holder, key) { - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - Object.keys(value).forEach(function(k) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - }); - } - return reviver.call(holder, key, value); - }({'': result}, '')) - : result; - }; -} + text = source + ''; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + // If there is a reviver function, we recursively walk the new structure, + // passing each name/value pair to the reviver function for possible + // transformation, starting with a temporary root object that holds the result + // in an empty key. If there is not a reviver function, we simply return the + // result. + + return typeof reviver === 'function' + ? (function walk(holder, key) { + var k, + v, + value = holder[key]; + if (value && typeof value === 'object') { + Object.keys(value).forEach(function (k) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + }); + } + return reviver.call(holder, key, value); + })({ '': result }, '') + : result; + }; +}; module.exports = json_parse; diff --git a/test/proto-test.js b/test/proto-test.js new file mode 100644 index 0000000..c61e118 --- /dev/null +++ b/test/proto-test.js @@ -0,0 +1,58 @@ +const makeJSON = require('../index.js'); +const expect = require('chai').expect; +describe('__proto__ and constructor assignment', function () { + it('should set __proto__ property but not a prototype if protoAction is set to preserve', () => { + const JSONbig = makeJSON({ protoAction: 'preserve' }); + const obj1 = JSONbig.parse('{ "__proto__": 1000000000000000 }'); + expect(Object.getPrototypeOf(obj1)).to.equal(null); + const obj2 = JSONbig.parse('{ "__proto__": { "admin": true } }'); + expect(obj2.admin).to.not.equal(true); + }); + + it('should throw an exception if protoAction set to invalid value', () => { + expect(() => { + makeJSON({ protoAction: 'invalid value' }); + }).to.throw( + 'Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed invalid value' + ); + }); + + it('should throw an exception if constructorAction set to invalid value', () => { + expect(() => { + makeJSON({ constructorAction: 'invalid value' }); + }).to.throw( + 'Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed invalid value' + ); + }); + + it('should throw an exception if protoAction set to error and there is __proto__ property', () => { + const JSONbig = makeJSON({ protoAction: 'error' }); + expect(() => + JSONbig.parse('{ "\\u005f_proto__": 1000000000000000 }') + ).to.throw('Object contains forbidden prototype property'); + }); + + it('should throw an exception if constructorAction set to error and there is constructor property', () => { + const JSONbig = makeJSON({ protoAction: 'error' }); + expect(() => JSONbig.parse('{ "constructor": 1000000000000000 }')).to.throw( + 'Object contains forbidden constructor property' + ); + }); + + it('should ignore __proto__ property if protoAction is set to ignore', () => { + const JSONbig = makeJSON({ protoAction: 'ignore' }); + const obj1 = JSONbig.parse( + '{ "__proto__": 1000000000000000, "a" : 42, "nested": { "__proto__": false, "b": 43 } }' + ); + expect(Object.getPrototypeOf(obj1)).to.equal(null); + expect(obj1).to.deep.equal({ a: 42, nested: { b: 43 } }); + }); + + it('should ignore constructor property if constructorAction is set to ignore', () => { + const JSONbig = makeJSON({ constructorAction: 'ignore' }); + const obj1 = JSONbig.parse( + '{ "constructor": 1000000000000000, "a" : 42, "nested": { "constructor": false, "b": 43 } }' + ); + expect(obj1).to.deep.equal({ a: 42, nested: { b: 43 } }); + }); +});