Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add max depth limit and max edges limit #56

Merged
merged 1 commit into from Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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