Skip to content

Commit

Permalink
fix: add max depth limit and max edges limit (#56)
Browse files Browse the repository at this point in the history
Co-authored-by: Matus Sabo <lamanabie@gmail.com>
  • Loading branch information
simoneb and matus-sabo committed Sep 7, 2021
1 parent 377166e commit ce7db51
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 138 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,3 +2,4 @@
node_modules/
npm-debug.log
yarn.lock
package-lock.json
21 changes: 18 additions & 3 deletions index.d.ts
@@ -1,8 +1,23 @@
declare function stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string;
declare function stringify(
value: any,
replacer?: (key: string, value: any) => any,
space?: string | number,
options?: { depthLimit: number | undefined; edgesLimit: number | undefined }
): string;

declare namespace stringify {
export function stable(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string;
export function stableStringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string;
export function stable(
value: any,
replacer?: (key: string, value: any) => any,
space?: string | number,
options?: { depthLimit: number | undefined; edgesLimit: number | undefined }
): string;
export function stableStringify(
value: any,
replacer?: (key: string, value: any) => any,
space?: string | number,
options?: { depthLimit: number | undefined; edgesLimit: number | undefined }
): string;
}

export default stringify;
129 changes: 91 additions & 38 deletions index.js
Expand Up @@ -3,12 +3,26 @@ stringify.default = stringify
stringify.stable = deterministicStringify
stringify.stableStringify = deterministicStringify

var LIMIT_REPLACE_NODE = '[...]'
var CIRCULAR_REPLACE_NODE = '[Circular]'

var arr = []
var replacerStack = []

function defaultOptions () {
return {
depthLimit: 10,
edgesLimit: 20
}
}

// Regular stringify
function stringify (obj, replacer, spacer) {
decirc(obj, '', [], undefined)
function stringify (obj, replacer, spacer, options) {
if (typeof options === 'undefined') {
options = defaultOptions()
}

decirc(obj, '', 0, [], undefined, 0, options)
var res
try {
if (replacerStack.length === 0) {
Expand All @@ -30,37 +44,60 @@ function stringify (obj, replacer, spacer) {
}
return res
}
function decirc (val, k, stack, parent) {

function setReplace (replace, val, k, parent) {
var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k)
if (propertyDescriptor.get !== undefined) {
if (propertyDescriptor.configurable) {
Object.defineProperty(parent, k, { value: replace })
arr.push([parent, k, val, propertyDescriptor])
} else {
replacerStack.push([val, k, replace])
}
} else {
parent[k] = replace
arr.push([parent, k, val])
}
}

function decirc (val, k, edgeIndex, stack, parent, depth, options) {
depth += 1
var i
if (typeof val === 'object' && val !== null) {
for (i = 0; i < stack.length; i++) {
if (stack[i] === val) {
var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k)
if (propertyDescriptor.get !== undefined) {
if (propertyDescriptor.configurable) {
Object.defineProperty(parent, k, { value: '[Circular]' })
arr.push([parent, k, val, propertyDescriptor])
} else {
replacerStack.push([val, k])
}
} else {
parent[k] = '[Circular]'
arr.push([parent, k, val])
}
setReplace(CIRCULAR_REPLACE_NODE, val, k, parent)
return
}
}

if (
typeof options.depthLimit !== 'undefined' &&
depth > options.depthLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

if (
typeof options.edgesLimit !== 'undefined' &&
edgeIndex + 1 > options.edgesLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

stack.push(val)
// Optimize for Arrays. Big arrays could kill the performance otherwise!
if (Array.isArray(val)) {
for (i = 0; i < val.length; i++) {
decirc(val[i], i, stack, val)
decirc(val[i], i, i, stack, val, depth, options)
}
} else {
var keys = Object.keys(val)
for (i = 0; i < keys.length; i++) {
var key = keys[i]
decirc(val[key], key, stack, val)
decirc(val[key], key, i, stack, val, depth, options)
}
}
stack.pop()
Expand All @@ -78,8 +115,12 @@ function compareFunction (a, b) {
return 0
}

function deterministicStringify (obj, replacer, spacer) {
var tmp = deterministicDecirc(obj, '', [], undefined) || obj
function deterministicStringify (obj, replacer, spacer, options) {
if (typeof options === 'undefined') {
options = defaultOptions()
}

var tmp = deterministicDecirc(obj, '', 0, [], undefined, 0, options) || obj
var res
try {
if (replacerStack.length === 0) {
Expand All @@ -103,23 +144,13 @@ function deterministicStringify (obj, replacer, spacer) {
return res
}

function deterministicDecirc (val, k, stack, parent) {
function deterministicDecirc (val, k, edgeIndex, stack, parent, depth, options) {
depth += 1
var i
if (typeof val === 'object' && val !== null) {
for (i = 0; i < stack.length; i++) {
if (stack[i] === val) {
var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k)
if (propertyDescriptor.get !== undefined) {
if (propertyDescriptor.configurable) {
Object.defineProperty(parent, k, { value: '[Circular]' })
arr.push([parent, k, val, propertyDescriptor])
} else {
replacerStack.push([val, k])
}
} else {
parent[k] = '[Circular]'
arr.push([parent, k, val])
}
setReplace(CIRCULAR_REPLACE_NODE, val, k, parent)
return
}
}
Expand All @@ -130,22 +161,39 @@ function deterministicDecirc (val, k, stack, parent) {
} catch (_) {
return
}

if (
typeof options.depthLimit !== 'undefined' &&
depth > options.depthLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

if (
typeof options.edgesLimit !== 'undefined' &&
edgeIndex + 1 > options.edgesLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

stack.push(val)
// Optimize for Arrays. Big arrays could kill the performance otherwise!
if (Array.isArray(val)) {
for (i = 0; i < val.length; i++) {
deterministicDecirc(val[i], i, stack, val)
deterministicDecirc(val[i], i, i, stack, val, depth, options)
}
} else {
// Create a temporary object in the required way
var tmp = {}
var keys = Object.keys(val).sort(compareFunction)
for (i = 0; i < keys.length; i++) {
var key = keys[i]
deterministicDecirc(val[key], key, stack, val)
deterministicDecirc(val[key], key, i, stack, val, depth, options)
tmp[key] = val[key]
}
if (parent !== undefined) {
if (typeof parent !== 'undefined') {
arr.push([parent, k, val])
parent[k] = tmp
} else {
Expand All @@ -157,15 +205,20 @@ function deterministicDecirc (val, k, stack, parent) {
}

// wraps replacer function to handle values we couldn't replace
// and mark them as [Circular]
// and mark them as replaced value
function replaceGetterValues (replacer) {
replacer = replacer !== undefined ? replacer : function (k, v) { return v }
replacer =
typeof replacer !== 'undefined'
? replacer
: function (k, v) {
return v
}
return function (key, val) {
if (replacerStack.length > 0) {
for (var i = 0; i < replacerStack.length; i++) {
var part = replacerStack[i]
if (part[1] === key && part[0] === val) {
val = '[Circular]'
val = part[2]
replacerStack.splice(i, 1)
break
}
Expand Down
19 changes: 16 additions & 3 deletions readme.md
Expand Up @@ -13,7 +13,7 @@ handle circular structures. See the example below for further information.

The same as [JSON.stringify][].

`stringify(value[, replacer[, space]])`
`stringify(value[, replacer[, space[, options]]])`

```js
const safeStringify = require('fast-safe-stringify')
Expand All @@ -33,7 +33,14 @@ function replacer(key, value) {
}
return value
}
const serialized = safeStringify(o, replacer, 2)

// those are also defaults limits when no options object is passed into safeStringify
const options = {
depthLimit: 10,
edgesLimit: 20,
};

const serialized = safeStringify(o, replacer, 2, options)
// Key: "" Value: {"a":1,"o":"[Circular]"}
// Key: "a" Value: 1
// Key: "o" Value: "[Circular]"
Expand All @@ -43,6 +50,7 @@ console.log(serialized)
// }
```


Using the deterministic version also works the same:

```js
Expand All @@ -62,6 +70,11 @@ A faster and side-effect free implementation is available in the
[safe-stable-stringify][] module. However it is still considered experimental
due to a new and more complex implementation.

### Replace strings constants

- `[Circular]` - when same reference is found
- `[...]` - when some limit from options object is reached

## Differences to JSON.stringify

In general the behavior is identical to [JSON.stringify][]. The [`replacer`][]
Expand Down Expand Up @@ -153,4 +166,4 @@ MIT
[`space`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20space%20argument
[`toJSON`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior
[benchmark]: https://github.com/epoberezkin/fast-json-stable-stringify/blob/67f688f7441010cfef91a6147280cc501701e83b/benchmark
[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

0 comments on commit ce7db51

Please sign in to comment.