diff --git a/lib/modifiers.js b/lib/modifiers.js index e48d5c3..043fa39 100644 --- a/lib/modifiers.js +++ b/lib/modifiers.js @@ -41,28 +41,17 @@ function groupRedact (o, path, censor, isCensorFct, censorFctTakesPath) { return { keys, values, target, flat: true } } -function nestedRestore (arr) { - const length = arr.length - for (var i = 0; i < length; i++) { - const { key, target, value, level } = arr[i] - if (level === 0 || level === 1) { - if (has(target, key)) { - target[key] = value - } - /* istanbul ignore else */ - if (typeof target === 'object') { - const targetKeys = Object.keys(target) - for (var j = 0; j < targetKeys.length; j++) { - const tKey = targetKeys[j] - const subTarget = target[tKey] - if (has(subTarget, key)) { - subTarget[key] = value - } - } - } - } else { - restoreNthLevel(key, target, value, level) +/** + * @param {RestoreInstruction[]} instructions a set of instructions for restoring values to objects + */ +function nestedRestore (instructions) { + for (let i = 0; i < instructions.length; i++) { + const { target, path, value } = instructions[i] + let current = target + for (let i = path.length - 1; i > 0; i--) { + current = current[path[i]] } + current[path[0]] = value } } @@ -73,12 +62,7 @@ function nestedRedact (store, o, path, ns, censor, isCensorFct, censorFctTakesPa const keysLength = keys.length for (var i = 0; i < keysLength; i++) { const key = keys[i] - const { value, parent, exists, level } = - specialSet(target, key, path, ns, censor, isCensorFct, censorFctTakesPath) - - if (exists === true && parent !== null) { - store.push({ key: ns[ns.length - 1], target: parent, value, level }) - } + specialSet(store, target, key, path, ns, censor, isCensorFct, censorFctTakesPath) } return store } @@ -89,7 +73,7 @@ function has (obj, prop) { : false } -function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesPath) { +function specialSet (store, o, k, path, afterPath, censor, isCensorFct, censorFctTakesPath) { const afterPathLen = afterPath.length const lastPathIndex = afterPathLen - 1 const originalKey = k @@ -98,19 +82,21 @@ function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesP var nv var ov var oov = null - var exists = true var wc = null var kIsWc var wcov var consecutive = false var level = 0 + // need to track depth of the `redactPath` tree + var depth = 0 + var redactPathCurrent = tree() ov = n = o[k] - if (typeof n !== 'object') return { value: null, parent: null, exists } + if (typeof n !== 'object') return while (n != null && ++i < afterPathLen) { + depth += 1 k = afterPath[i] oov = ov if (k !== '*' && !wc && !(typeof n === 'object' && k in n)) { - exists = false break } if (k === '*') { @@ -129,8 +115,9 @@ function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesP wcov = n[wck] kIsWc = k === '*' if (consecutive) { + redactPathCurrent = node(redactPathCurrent, wck, depth) level = i - ov = iterateNthLevel(wcov, level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, exists) + ov = iterateNthLevel(wcov, level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, o[originalKey], depth + 1) } else { if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) { if (kIsWc) { @@ -144,12 +131,19 @@ function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesP ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) : censor) if (kIsWc) { + const rv = restoreInstr(node(redactPathCurrent, wck, depth), ov, o[originalKey]) + store.push(rv) n[wck] = nv } else { if (wcov[k] === nv) { - exists = false + // pass + } else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) { + redactPathCurrent = node(redactPathCurrent, wck, depth) } else { - wcov[k] = (nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov) ? wcov[k] : nv + redactPathCurrent = node(redactPathCurrent, wck, depth) + const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, o[originalKey]) + store.push(rv) + wcov[k] = nv } } } @@ -158,22 +152,27 @@ function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesP wc = null } else { ov = n[k] + redactPathCurrent = node(redactPathCurrent, k, depth) nv = (i !== lastPathIndex) ? ov : (isCensorFct ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) : censor) - n[k] = (has(n, k) && nv === ov) || (nv === undefined && censor !== undefined) ? n[k] : nv + if ((has(n, k) && nv === ov) || (nv === undefined && censor !== undefined)) { + // pass + } else { + const rv = restoreInstr(redactPathCurrent, ov, o[originalKey]) + store.push(rv) + n[k] = nv + } n = n[k] } if (typeof n !== 'object') break // prevent circular structure, see https://github.com/pinojs/pino/issues/1513 if (ov === oov || typeof ov === 'undefined') { - exists = false + // pass } } - - return { value: ov, parent: oov, exists, level } } function get (o, p) { @@ -186,7 +185,7 @@ function get (o, p) { return n } -function iterateNthLevel (wcov, level, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, exists) { +function iterateNthLevel (wcov, level, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth) { if (level === 0) { if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) { if (kIsWc) { @@ -200,35 +199,93 @@ function iterateNthLevel (wcov, level, k, path, afterPath, censor, isCensorFct, ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) : censor) if (kIsWc) { + const rv = restoreInstr(redactPathCurrent, ov, parent) + store.push(rv) n[wck] = nv } else { if (wcov[k] === nv) { - exists = false + // pass + } else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) { + // pass } else { - wcov[k] = (nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov) ? wcov[k] : nv + const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, parent) + store.push(rv) + wcov[k] = nv } } } - return ov } for (const key in wcov) { if (typeof wcov[key] === 'object') { - var temp = iterateNthLevel(wcov[key], level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, exists) - return temp + redactPathCurrent = node(redactPathCurrent, key, depth) + iterateNthLevel(wcov[key], level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth + 1) } } } -function restoreNthLevel (key, target, value, level) { - if (level === 0) { - if (has(target, key)) { - target[key] = value - } - return +/** + * @typedef {object} TreeNode + * @prop {TreeNode} [parent] reference to the parent of this node in the tree, or `null` if there is no parent + * @prop {string} key the key that this node represents (key here being part of the path being redacted + * @prop {TreeNode[]} children the child nodes of this node + * @prop {number} depth the depth of this node in the tree + */ + +/** + * instantiate a new, empty tree + * @returns {TreeNode} + */ +function tree () { + return { parent: null, key: null, children: [], depth: 0 } +} + +/** + * creates a new node in the tree, attaching it as a child of the provided parent node + * if the specified depth matches the parent depth, adds the new node as a _sibling_ of the parent instead + * @param {TreeNode} parent the parent node to add a new node to (if the parent depth matches the provided `depth` value, will instead add as a sibling of this + * @param {string} key the key that the new node represents (key here being part of the path being redacted) + * @param {number} depth the depth of the new node in the tree - used to determing whether to add the new node as a child or sibling of the provided `parent` node + * @returns {TreeNode} a reference to the newly created node in the tree + */ +function node (parent, key, depth) { + if (parent.depth === depth) { + return node(parent.parent, key, depth) } - for (const objKey in target) { - if (typeof target[objKey] === 'object') { - restoreNthLevel(key, target[objKey], value, level - 1) - } + + var child = { + parent, + key, + depth, + children: [] } + + parent.children.push(child) + + return child +} + +/** + * @typedef {object} RestoreInstruction + * @prop {string[]} path a reverse-order path that can be used to find the correct insertion point to restore a `value` for the given `parent` object + * @prop {*} value the value to restore + * @prop {object} target the object to restore the `value` in + */ + +/** + * create a restore instruction for the given redactPath node + * generates a path in reverse order by walking up the redactPath tree + * @param {TreeNode} node a tree node that should be at the bottom of the redact path (i.e. have no children) - this will be used to walk up the redact path tree to construct the path needed to restore + * @param {*} value the value to restore + * @param {object} target a reference to the parent object to apply the restore instruction to + * @returns {RestoreInstruction} an instruction used to restore a nested value for a specific object + */ +function restoreInstr (node, value, target) { + let current = node + const path = [] + do { + path.push(current.key) + current = current.parent + } while (current.parent != null) + + return { path, value, target } } diff --git a/test/index.js b/test/index.js index a860866..ac2d391 100644 --- a/test/index.js +++ b/test/index.js @@ -1355,3 +1355,87 @@ test('multi level wildcards at nested level inside object with serialize false', is(result.a.d.x.u.c, 's') end() }) + +test('restores nested wildcard values', ({ end, is }) => { + const o = { a: { b: [{ c: [ + { d: '123' }, + { d: '456' } + ] }] } } + + const censor = 'censor' + const paths = ['a.b[*].c[*].d'] + const redact = fastRedact({ paths, censor, serialize: false }) + + redact(o) + is(o.a.b[0].c[0].d, censor) + is(o.a.b[0].c[1].d, censor) + redact.restore(o) + is(o.a.b[0].c[0].d, '123') + is(o.a.b[0].c[1].d, '456') + end() +}) + +test('restores multi nested wildcard values', ({ end, is }) => { + const o = { + a: { + b1: { + c1: { + d1: { e: '123' }, + d2: { e: '456' } + }, + c2: { + d1: { e: '789' }, + d2: { e: '012' } + } + }, + b2: { + c1: { + d1: { e: '345' }, + d2: { e: '678' } + }, + c2: { + d1: { e: '901' }, + d2: { e: '234' } + } + } + } + } + + const censor = 'censor' + const paths = ['a.*.*.*.e'] + const redact = fastRedact({ paths, censor, serialize: false }) + + redact(o) + is(o.a.b1.c1.d1.e, censor) + is(o.a.b1.c1.d2.e, censor) + is(o.a.b1.c2.d1.e, censor) + is(o.a.b1.c2.d2.e, censor) + is(o.a.b2.c1.d1.e, censor) + is(o.a.b2.c1.d2.e, censor) + is(o.a.b2.c2.d1.e, censor) + is(o.a.b2.c2.d2.e, censor) + redact.restore(o) + is(o.a.b1.c1.d1.e, '123') + is(o.a.b1.c1.d2.e, '456') + is(o.a.b1.c2.d1.e, '789') + is(o.a.b1.c2.d2.e, '012') + is(o.a.b2.c1.d1.e, '345') + is(o.a.b2.c1.d2.e, '678') + is(o.a.b2.c2.d1.e, '901') + is(o.a.b2.c2.d2.e, '234') + end() +}) + +test('redact multi trailing wildcard', ({ end, is }) => { + const o = { a: { b: { c: 'value' } } } + + const censor = 'censor' + const paths = ['a.*.*'] + const redact = fastRedact({ paths, censor, serialize: false }) + + redact(o) + is(o.a.b.c, censor) + redact.restore(o) + is(o.a.b.c, 'value') + end() +})