diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5240588 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x] + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install + run: | + npm install + + - name: Run tests + run: | + npm run test diff --git a/.gitignore b/.gitignore index 7cddf33..1a49a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .nyc_output package-lock.json -coverage \ No newline at end of file +coverage +.idea diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bfbc621..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: node_js -sudo: false -node_js: - - 6 - - 8 - - 10 - - 12 - - 13 - - 14 -script: - - npm run ci diff --git a/benchmark/index.js b/benchmark/index.js index b599492..2409927 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -26,6 +26,8 @@ const redactIntermediateWildCensorFunction = fastRedact({ paths: ['as.*.c'], cen const redactCensorFunctionWithPath = fastRedact({ paths: ['at.d.b'], censor: censorFn, serialize: false }) const redactWildCensorFunctionWithPath = fastRedact({ paths: ['au.d.*'], censor: censorFnWithPath, serialize: false }) const redactIntermediateWildCensorFunctionWithPath = fastRedact({ paths: ['av.*.c'], censorFnWithPath, serialize: false }) +const redactMultiWild = fastRedact({ paths: ['aw.*.*'] }) +const redactMultiWildCensorFunction = fastRedact({ paths: ['ax.*.*'], censor: censorFn, serialize: false }) const getObj = (outerKey) => ({ [outerKey]: { @@ -201,6 +203,20 @@ var run = bench([ redactIntermediateWildCensorFunctionWithPath(obj) } setImmediate(cb) + }, + function benchFastRedactMultiWild (cb) { + const obj = getObj('aw') + for (var i = 0; i < max; i++) { + redactMultiWild(obj) + } + setImmediate(cb) + }, + function benchFastRedactMultiWildCensorFunction (cb) { + const obj = getObj('ax') + for (var i = 0; i < max; i++) { + redactMultiWildCensorFunction(obj) + } + setImmediate(cb) } ], 500) diff --git a/example/multi-wildcard-array-depth.js b/example/multi-wildcard-array-depth.js new file mode 100644 index 0000000..8a22b5d --- /dev/null +++ b/example/multi-wildcard-array-depth.js @@ -0,0 +1,11 @@ +'use strict' +const fastRedact = require('..') +const redact = fastRedact({ paths: ['a[*].c.d[*].i'] }) +const obj = { + a: [ + { c: { d: [ { i: 'redact me', j: 'not me' } ], e: 'leave me be' } }, + { c: { d: [ { i: 'redact me too', j: 'not me' }, { i: 'redact me too', j: 'not me' } ], f: 'I want to live' } }, + { c: { d: [ { i: 'redact me 3', j: 'not me' } ], g: 'I want to run in a stream' } } + ] +} +console.log(redact(obj)) diff --git a/example/multi-wildcard-array-end.js b/example/multi-wildcard-array-end.js new file mode 100644 index 0000000..eaae35a --- /dev/null +++ b/example/multi-wildcard-array-end.js @@ -0,0 +1,11 @@ +'use strict' +const fastRedact = require('..') +const redact = fastRedact({ paths: ['a[*].c.d[*]'] }) +const obj = { + a: [ + { c: { d: ['hide me', '2'], e: 'leave me be' } }, + { c: { d: ['and me'], f: 'I want to live' } }, + { c: { d: ['and also I'], g: 'I want to run in a stream' } } + ] +} +console.log(redact(obj)) diff --git a/example/multi-wildcard-array.js b/example/multi-wildcard-array.js new file mode 100644 index 0000000..a0152a5 --- /dev/null +++ b/example/multi-wildcard-array.js @@ -0,0 +1,11 @@ +'use strict' +const fastRedact = require('..') +const redact = fastRedact({ paths: ['a[*].c[*].d'] }) +const obj = { + a: [ + { c: [{ d: 'hide me', e: 'leave me be' }, { d: 'hide me too', e: 'leave me be' }, { d: 'hide me 3', e: 'leave me be' }] }, + { c: [{ d: 'and me', f: 'I want to live' }] }, + { c: [{ d: 'and also I', g: 'I want to run in a stream' }] } + ] +} +console.log(redact(obj)) diff --git a/lib/modifiers.js b/lib/modifiers.js index 42c16f7..c94eb85 100644 --- a/lib/modifiers.js +++ b/lib/modifiers.js @@ -45,7 +45,20 @@ function nestedRestore (arr) { const length = arr.length for (var i = 0; i < length; i++) { const { key, target, value } = arr[i] - target[key] = value + 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 + } + } + } } } @@ -67,7 +80,9 @@ function nestedRedact (store, o, path, ns, censor, isCensorFct, censorFctTakesPa } function has (obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop) + return obj !== undefined && obj !== null + ? ('hasOwn' in Object ? Object.hasOwn(obj, prop) : Object.prototype.hasOwnProperty.call(obj, prop)) + : false } function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesPath) { @@ -80,23 +95,57 @@ function specialSet (o, k, path, afterPath, censor, isCensorFct, censorFctTakesP var ov var oov = null var exists = true + var wc = null ov = n = o[k] if (typeof n !== 'object') return { value: null, parent: null, exists } while (n != null && ++i < afterPathLen) { k = afterPath[i] oov = ov - if (!(k in n)) { + if (k !== '*' && !wc && !(typeof n === 'object' && k in n)) { exists = false break } - ov = n[k] - 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 - n = n[k] + if (k === '*') { + wc = k + if (i !== lastPathIndex) { + continue + } + } + if (wc) { + const wcKeys = Object.keys(n) + for (var j = 0; j < wcKeys.length; j++) { + const wck = wcKeys[j] + const wcov = n[wck] + const kIsWc = k === '*' + if (kIsWc || (typeof wcov === 'object' && k in wcov)) { + if (kIsWc) { + ov = wcov + } else { + ov = wcov[k] + } + nv = (i !== lastPathIndex) + ? ov + : (isCensorFct + ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) + : censor) + if (kIsWc) { + n[wck] = nv + } else { + wcov[k] = (nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov) ? wcov[k] : nv + } + } + } + wc = null + } else { + ov = n[k] + 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 + n = n[k] + } if (typeof n !== 'object') break } return { value: ov, parent: oov, exists } diff --git a/lib/parse.js b/lib/parse.js index ccbd980..abdb9e4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -19,7 +19,6 @@ function parse ({ paths }) { const before = path.slice(0, star) const beforeStr = before.join('.') const after = path.slice(star + 1, path.length) - if (after.indexOf('*') > -1) throw Error('fast-redact – Only one wildcard per path is supported') const nested = after.length > 0 wcLen++ wildcards.push({ diff --git a/lib/restorer.js b/lib/restorer.js index d75ffed..a8a06b9 100644 --- a/lib/restorer.js +++ b/lib/restorer.js @@ -8,7 +8,6 @@ function restorer ({ secret, wcLen }) { return function compileRestore () { if (this.restore) return const paths = Object.keys(secret) - .filter((path) => secret[path].precensored === false) const resetters = resetTmpl(secret, paths) const hasWildcards = wcLen > 0 const state = hasWildcards ? { secret, groupRestore, nestedRestore } : { secret } diff --git a/package.json b/package.json index 9a71752..786b6ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fast-redact", - "version": "3.0.1", + "version": "3.0.2", "description": "very fast object redaction", "main": "index.js", "scripts": { diff --git a/test/index.js b/test/index.js index baf2481..c86ff85 100644 --- a/test/index.js +++ b/test/index.js @@ -207,13 +207,6 @@ test('throws when passed illegal paths', ({ end, throws }) => { end() }) -test('throws if more than one wildcard in a path', ({ end, throws }) => { - throws(() => { - fastRedact({ paths: ['a.*.x.*'], serialize: false }) - }, Error('fast-redact – Only one wildcard per path is supported')) - end() -}) - test('throws if a custom serializer is used and remove is true', ({ end, throws }) => { throws(() => { fastRedact({ paths: ['a'], serialize: (o) => o, remove: true }) @@ -289,6 +282,22 @@ test('redact.restore function places original values back in place', ({ end, is end() }) +test('redact.restore function places original values back in place when called twice and the first call is precensored', ({ end, is }) => { + const censor = 'test' + const redact = fastRedact({ paths: ['a'], censor, serialize: false }) + const o1 = { a: censor } + const o2 = { a: 'a' } + redact(o1) + is(o1.a, censor) + redact.restore(o1) + is(o1.a, censor) + redact(o2) + is(o2.a, censor) + redact.restore(o2) + is(o2.a, 'a') + end() +}) + test('masks according to supplied censor function', ({ end, is }) => { const redact = fastRedact({ paths: ['a'], censor: censorFct, serialize: false }) is(redact({ a: '0123456' }).a, 'xxx56') @@ -620,7 +629,7 @@ test('ultimate wildcards – handles circulars', ({ end, is, same }) => { end() }) -test('ultimate wildcards – handles circulars – restore', ({ end, is, same }) => { +test('ultimate wildcards – handles circulars – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['bar.baz.*'], serialize: false }) const bar = { b: 2 } const o = { a: 1, bar } @@ -634,7 +643,21 @@ test('ultimate wildcards – handles circulars – restore', ({ end, is, same } end() }) -test('ultimate wildcards – handles circulars and cross references – restore', ({ end, is, same }) => { +test('ultimate multi wildcards – handles circulars – restore', ({ end, is, same }) => { + const redact = fastRedact({ paths: ['bar.*.baz.*.b'], serialize: false }) + const bar = { b: 2 } + const o = { a: 1, bar } + bar.baz = bar + o.bar.baz = o.bar + is(o.bar.baz, bar) + redact(o) + is(o.bar.baz.b, censor) + redact.restore(o) + same(o.bar.baz, bar) + end() +}) + +test('ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false }) const bar = { b: 2 } const o = { a: 1, bar, cf: { bar } } @@ -742,6 +765,21 @@ test('parent wildcard – two following keys', ({ end, is }) => { end() }) +test('multi object wildcard', ({ end, is }) => { + const redact = fastRedact({ paths: ['a.*.x.*.c'], serialize: false }) + const result = redact({ a: { b: { x: { z: { c: 's' } } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } }) + is(result.a.b.x.z.c, censor) + is(result.a.d.x.u.a, 's') + is(result.a.d.x.u.b, 's') + is(result.a.d.x.u.c, censor) + redact.restore(result) + is(result.a.b.x.z.c, 's') + is(result.a.d.x.u.a, 's') + is(result.a.d.x.u.b, 's') + is(result.a.d.x.u.c, 's') + end() +}) + test('parent wildcard – two following keys – reuse', ({ end, is }) => { const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false }) const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } }) @@ -779,6 +817,106 @@ test('parent wildcard - array', ({ end, is }) => { end() }) +test('multiple wildcards', ({ end, is }) => { + const redact = fastRedact({ paths: ['a[*].c[*].d'], serialize: false }) + const obj = { + a: [ + { c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] }, + { c: [{ d: '1', f: '5' }] }, + { c: [{ d: '2', g: '6' }] } + ] + } + const result = redact(obj) + is(result.a[0].c[0].d, censor) + is(result.a[0].c[0].e, '2') + is(result.a[0].c[1].d, censor) + is(result.a[0].c[1].e, '3') + is(result.a[0].c[2].d, censor) + is(result.a[0].c[2].e, '4') + is(result.a[1].c[0].d, censor) + is(result.a[1].c[0].f, '5') + is(result.a[2].c[0].d, censor) + is(result.a[2].c[0].g, '6') + redact.restore(result) + is(result.a[0].c[0].d, '1') + is(result.a[0].c[0].e, '2') + is(result.a[0].c[1].d, '1') + is(result.a[0].c[1].e, '3') + is(result.a[0].c[2].d, '1') + is(result.a[0].c[2].e, '4') + is(result.a[1].c[0].d, '1') + is(result.a[1].c[0].f, '5') + is(result.a[2].c[0].d, '2') + is(result.a[2].c[0].g, '6') + end() +}) + +test('multiple wildcards - censor function', ({ end, is }) => { + const redact = fastRedact({ paths: ['a[*].c[*].d'], censor: censorFct, serialize: false }) + const obj = { + a: [ + { c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] }, + { c: [{ d: '1', f: '5' }] }, + { c: [{ d: '2', g: '6' }] } + ] + } + const result = redact(obj) + is(result.a[0].c[0].d, 'xxx1') + is(result.a[0].c[0].e, '2') + is(result.a[0].c[1].d, 'xxx1') + is(result.a[0].c[1].e, '3') + is(result.a[0].c[2].d, 'xxx1') + is(result.a[0].c[2].e, '4') + is(result.a[1].c[0].d, 'xxx1') + is(result.a[1].c[0].f, '5') + is(result.a[2].c[0].d, 'xxx2') + is(result.a[2].c[0].g, '6') + end() +}) + +test('multiple wildcards end', ({ end, is, same }) => { + const redact = fastRedact({ paths: ['a[*].c.d[*]'], serialize: false }) + const obj = { + a: [ + { c: { d: [ '1', '2' ], e: '3' } }, + { c: { d: [ '1' ], f: '4' } }, + { c: { d: [ '1' ], g: '5' } } + ] + } + const result = redact(obj) + same(result.a[0].c.d, [censor, censor]) + is(result.a[0].c.e, '3') + same(result.a[1].c.d, [censor]) + is(result.a[1].c.f, '4') + same(result.a[2].c.d, [censor]) + is(result.a[2].c.g, '5') + end() +}) + +test('multiple wildcards depth after n wildcard', ({ end, is }) => { + const redact = fastRedact({ paths: ['a[*].c.d[*].i'], serialize: false }) + const obj = { + a: [ + { c: { d: [ { i: '1', j: '2' } ], e: '3' } }, + { c: { d: [ { i: '1', j: '2' }, { i: '1', j: '3' } ], f: '4' } }, + { c: { d: [ { i: '1', j: '2' } ], g: '5' } } + ] + } + const result = redact(obj) + is(result.a[0].c.d[0].i, censor) + is(result.a[0].c.d[0].j, '2') + is(result.a[0].c.e, '3') + is(result.a[1].c.d[0].i, censor) + is(result.a[1].c.d[0].j, '2') + is(result.a[1].c.d[1].i, censor) + is(result.a[1].c.d[1].j, '3') + is(result.a[1].c.f, '4') + is(result.a[2].c.d[0].i, censor) + is(result.a[2].c.d[0].j, '2') + is(result.a[2].c.g, '5') + end() +}) + test('parent wildcards – array – single index', ({ end, same }) => { const redact = fastRedact({ paths: ['insideArray.like[3].*.foo'], serialize: false }) same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: { foo: censor } }] } }) @@ -810,7 +948,7 @@ test('parent wildcards - gracefully handles primitives that match intermediate k end() }) -test('parent wildcards – handles circulars', ({ end, is, same }) => { +test('parent wildcards – handles circulars', ({ end, same }) => { const redact = fastRedact({ paths: ['x.*.baz'], serialize: false }) const bar = { b: 2 } const o = { x: { a: 1, bar } } @@ -820,7 +958,7 @@ test('parent wildcards – handles circulars', ({ end, is, same }) => { end() }) -test('parent wildcards – handles circulars – restore', ({ end, is, same }) => { +test('parent wildcards – handles circulars – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['x.*.baz'], serialize: false }) const bar = { b: 2 } const o = { x: { a: 1, bar } } @@ -835,7 +973,7 @@ test('parent wildcards – handles circulars – restore', ({ end, is, same }) end() }) -test('parent wildcards – handles circulars and cross references – restore', ({ end, is, same }) => { +test('parent wildcards – handles circulars and cross references – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['x.*.baz', 'x.*.cf.bar'], serialize: false }) const bar = { b: 2 } const o = { x: { a: 1, bar, y: { cf: { bar } } } } @@ -852,14 +990,14 @@ test('parent wildcards – handles circulars and cross references – restore', end() }) -test('parent wildcards – handles missing paths', ({ end, is, same }) => { +test('parent wildcards – handles missing paths', ({ end, is }) => { const redact = fastRedact({ paths: ['z.*.baz'] }) const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } is(redact(o), JSON.stringify(o)) end() }) -test('ultimate wildcards – handles missing paths', ({ end, is, same }) => { +test('ultimate wildcards – handles missing paths', ({ end, is }) => { const redact = fastRedact({ paths: ['z.*'] }) const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } is(redact(o), JSON.stringify(o)) @@ -908,7 +1046,7 @@ test('supports single leading bracket containing non-legal keyword characters', end() }) -test('(leading brackets) ultimate wildcards – handles circulars and cross references – restore', ({ end, is, same }) => { +test('(leading brackets) ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false }) const bar = { b: 2 } const o = { a: 1, bar, cf: { bar } } @@ -925,7 +1063,7 @@ test('(leading brackets) ultimate wildcards – handles circulars and cross refe end() }) -test('(leading brackets) parent wildcards – handles circulars and cross references – restore', ({ end, is, same }) => { +test('(leading brackets) parent wildcards – handles circulars and cross references – restore', ({ end, is }) => { const redact = fastRedact({ paths: ['["x"].*.baz', '["x"].*.cf.bar'], serialize: false }) const bar = { b: 2 } const o = { x: { a: 1, bar, y: { cf: { bar } } } } @@ -942,7 +1080,7 @@ test('(leading brackets) parent wildcards – handles circulars and cross refere end() }) -test('(leading brackets) ultimate wildcards – handles missing paths', ({ end, is, same }) => { +test('(leading brackets) ultimate wildcards – handles missing paths', ({ end, is }) => { const redact = fastRedact({ paths: ['["z"].*'] }) const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } is(redact(o), JSON.stringify(o)) @@ -1061,3 +1199,41 @@ test('handles keys with dots', ({ end, is }) => { is(redactLeading({ 'b.c': 'x', '-1.2': 'x' })['b.c'], censor) end() }) + +test('handles multi wildcards within arrays', ({ end, is }) => { + const redact = fastRedact({ + paths: ['a[*].x.d[*].i.*'] + }) + const o = { + a: [ { x: { d: [ { j: { i: 'R' } }, { i: 'R', j: 'NR' } ] } } ] + } + is(redact(o), '{"a":[{"x":{"d":["[REDACTED]","[REDACTED]"]}}]}') + end() +}) + +test('handles multi wildcards within arrays with a censorFct', ({ end, is }) => { + const redact = fastRedact({ + paths: ['a[*].x.d[*].i.*.i'], + censor: censorWithPath + }) + const o = { + a: [ + { x: { d: [ { i: 'R', j: 'NR' } ] } } + ] + } + is(redact(o), '{"a":[{"x":{"d":[{"i":"a.0.x.d.*.i.*.i xxxR","j":"NR"}]}}]}') + end() +}) + +test('handles multi wildcards within arrays with undefined values', ({ end, is }) => { + const redact = fastRedact({ + paths: ['a[*].x.d[*].i.*.i'] + }) + const o = { + a: [ + { x: { d: [ { i: undefined, j: 'NR' } ] } } + ] + } + is(redact(o), '{"a":[{"x":{"d":[{"i":"[REDACTED]","j":"NR"}]}}]}') + end() +})