Skip to content

Commit bf2fa27

Browse files
committed
Release some of the safety measures, some of them are incomplete at this
time. Also add a tiny bug fix for try.
1 parent 3b06d51 commit bf2fa27

File tree

8 files changed

+67
-52
lines changed

8 files changed

+67
-52
lines changed

asyncLogic.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ class AsyncLogicEngine {
2323
* - In mainline: empty arrays are falsey; in our implementation, they are truthy.
2424
*
2525
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
26-
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: boolean, permissive?: boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} options
26+
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: boolean, permissive?: boolean, maxDepth?: number, maxArrayLength?: number, maxStringLength?: number }} options
2727
*/
2828
constructor (
2929
methods = defaultMethods,
30-
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, enableObjectAccumulators: false, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
30+
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, maxDepth: 0, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
3131
) {
3232
this.methods = { ...methods }
33-
/** @type {{disableInline?: Boolean, disableInterpretedOptimization?: Boolean, enableObjectAccumulators?: Boolean, maxArrayLength?: number, maxStringLength?: number}} */
34-
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, enableObjectAccumulators: options.enableObjectAccumulators || false, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
33+
/** @type {{disableInline?: Boolean, disableInterpretedOptimization?: Boolean, maxDepth?: number, maxArrayLength?: number, maxStringLength?: number}} */
34+
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, maxDepth: options.maxDepth || 0, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
3535
this.disableInline = options.disableInline
3636
this.disableInterpretedOptimization = options.disableInterpretedOptimization
3737
this.async = true

async_iterators.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22
'use strict'
33

4-
import { assertNotType } from './utilities/downgrade.js'
4+
import { assertAllowedDepth } from './utilities/downgrade.js'
55

66
// Note: Each of these iterators executes synchronously, and will not "run in parallel"
77
// I am supporting filter, reduce, some, every, map
@@ -39,17 +39,17 @@ export async function map (arr, iter) {
3939
return result
4040
}
4141

42-
export async function reduce (arr, iter, defaultValue, skipTypeCheck = false) {
42+
export async function reduce (arr, iter, defaultValue, maxDepth = 0) {
4343
if (arr.length === 0) {
4444
if (typeof defaultValue !== 'undefined') return defaultValue
4545
throw new Error('Array has no elements.')
4646
}
4747

4848
const start = typeof defaultValue === 'undefined' ? 1 : 0
49-
let data = assertNotType(start ? arr[0] : defaultValue, skipTypeCheck ? '' : 'object')
49+
let data = assertAllowedDepth(start ? arr[0] : defaultValue, maxDepth)
5050

5151
for (let i = start; i < arr.length; i++) {
52-
data = assertNotType(await iter(data, arr[i]), skipTypeCheck ? '' : 'object')
52+
data = assertAllowedDepth(await iter(data, arr[i]), maxDepth)
5353
}
5454

5555
return data

compiler.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import asyncIterators from './async_iterators.js'
1212
import { coerceArray } from './utilities/coerceArray.js'
1313
import { countArguments } from './utilities/countArguments.js'
14-
import { precoerceNumber, assertSize, compareCheck, assertNotType } from './utilities/downgrade.js'
14+
import { precoerceNumber, assertSize, compareCheck, assertAllowedDepth } from './utilities/downgrade.js'
1515

1616
/**
1717
* Provides a simple way to compile logic into a function that can be run.
@@ -99,7 +99,7 @@ export function isDeterministic (method, engine, buildState) {
9999
return typeof engine.methods[func].deterministic === 'function'
100100
? engine.methods[func].deterministic(lower, buildState)
101101
: engine.methods[func].deterministic &&
102-
isDeterministic(lower, engine, buildState)
102+
isDeterministic(lower, engine, buildState)
103103
}
104104

105105
return true
@@ -319,12 +319,12 @@ function processBuiltString (method, str, buildState) {
319319
str = str.replace(`__%%%${x}%%%__`, item)
320320
})
321321

322-
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertNotType) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
322+
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertAllowedDepth) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
323323
// console.log(str)
324324
// console.log(final)
325325
// eslint-disable-next-line no-eval
326326
return Object.assign(
327-
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertNotType), {
327+
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertAllowedDepth), {
328328
[Sync]: !buildState.asyncDetected,
329329
deterministic: !str.includes('('),
330330
aboveDetected: typeof str === 'string' && str.includes(', above')

defaultMethods.js

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import declareSync from './utilities/declareSync.js'
77
import { build, buildString } from './compiler.js'
88
import chainingSupported from './utilities/chainingSupported.js'
99
import legacyMethods from './legacy.js'
10-
import { precoerceNumber, assertNotType } from './utilities/downgrade.js'
10+
import { precoerceNumber, assertAllowedDepth } from './utilities/downgrade.js'
1111

1212
const INVALID_ARGUMENTS = { type: 'Invalid Arguments' }
1313

@@ -402,7 +402,7 @@ const defaultMethods = {
402402
}
403403

404404
res = buildState.compile`${res} } })(context, above)`
405-
if (res[Compiled].includes('await')) res[Compiled] = res[Compiled].replace('((context', '(async (context')
405+
if (res[Compiled].includes('await')) res[Compiled] = res[Compiled].replace('((context', 'await (async (context')
406406

407407
return res
408408
}
@@ -653,16 +653,16 @@ const defaultMethods = {
653653
}
654654
mapper = build(mapper, mapState)
655655
const aboveArray = mapper.aboveDetected ? '[null, context, above]' : 'null'
656-
const verifyAccumulator = buildState.engine.options.enableObjectAccumulators ? '' : 'assertNotType'
656+
const verifyAccumulator = buildState.engine.options.maxDepth === Infinity ? '' : 'assertAllowedDepth'
657657
buildState.methods.push(mapper)
658658

659659
if (async) {
660660
if (!isSync(mapper) || selector.includes('await')) {
661661
buildState.asyncDetected = true
662662
if (typeof defaultValue !== 'undefined') {
663-
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue}, ${buildState.engine.options.enableObjectAccumulators})`
663+
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue}, ${buildState.engine.options.maxDepth})`
664664
}
665-
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), undefined, ${buildState.engine.options.enableObjectAccumulators})`
665+
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), undefined, ${buildState.engine.options.maxDepth})`
666666
}
667667
}
668668

@@ -675,15 +675,13 @@ const defaultMethods = {
675675
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
676676
let [selector, mapper, defaultValue] = input
677677

678-
const verifyAccumulator = engine.options.enableObjectAccumulators ? a => a : assertNotType
679-
680-
defaultValue = verifyAccumulator(runOptimizedOrFallback(defaultValue, engine, context, above))
678+
defaultValue = assertAllowedDepth(runOptimizedOrFallback(defaultValue, engine, context, above), engine.options.maxDepth)
681679
selector = runOptimizedOrFallback(selector, engine, context, above) || []
682-
let func = (accumulator, current) => verifyAccumulator(engine.run(mapper, { accumulator, current }, { above: [selector, context, above] }), 'object')
680+
let func = (accumulator, current) => assertAllowedDepth(engine.run(mapper, { accumulator, current }, { above: [selector, context, above] }), engine.options.maxDepth)
683681

684682
if (engine.optimizedMap.has(mapper) && typeof engine.optimizedMap.get(mapper) === 'function') {
685683
const optimized = engine.optimizedMap.get(mapper)
686-
func = (accumulator, current) => verifyAccumulator(optimized({ accumulator, current }, [selector, context, above]))
684+
func = (accumulator, current) => assertAllowedDepth(optimized({ accumulator, current }, [selector, context, above]), engine.options.maxDepth)
687685
}
688686

689687
if (typeof defaultValue === 'undefined') return selector.reduce(func)
@@ -694,9 +692,8 @@ const defaultMethods = {
694692
asyncMethod: async (input, context, above, engine) => {
695693
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
696694
let [selector, mapper, defaultValue] = input
697-
const verifyAccumulator = engine.options.enableObjectAccumulators ? a => a : assertNotType
698695

699-
defaultValue = verifyAccumulator(await engine.run(defaultValue, context, { above }))
696+
defaultValue = assertAllowedDepth(await engine.run(defaultValue, context, { above }), engine.options.maxDepth)
700697
selector = (await engine.run(selector, context, { above })) || []
701698
return asyncIterators.reduce(
702699
selector,
@@ -713,7 +710,7 @@ const defaultMethods = {
713710
)
714711
},
715712
defaultValue,
716-
engine.enableObjectAccumulators
713+
engine.options.maxDepth
717714
)
718715
},
719716
lazy: true
@@ -823,7 +820,7 @@ const defaultMethods = {
823820
return accumulator
824821
},
825822
{},
826-
true
823+
Infinity
827824
)
828825
return result
829826
}

logic.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class LogicEngine {
1818
* Creates a new instance of the Logic Engine.
1919
*
2020
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
21-
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, permissive?: boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} options
21+
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, permissive?: boolean, maxDepth?: number, maxArrayLength?: number, maxStringLength?: number }} options
2222
*/
2323
constructor (
2424
methods = defaultMethods,
25-
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, enableObjectAccumulators: false, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
25+
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, maxDepth: 0, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
2626
) {
2727
this.disableInline = options.disableInline
2828
this.disableInterpretedOptimization = options.disableInterpretedOptimization
@@ -31,8 +31,8 @@ class LogicEngine {
3131
this.optimizedMap = new WeakMap()
3232
this.missesSinceSeen = 0
3333

34-
/** @type {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} */
35-
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, enableObjectAccumulators: options.enableObjectAccumulators || false, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
34+
/** @type {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, maxDepth?: number, maxArrayLength?: number, maxStringLength?: number }} */
35+
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, maxDepth: options.maxDepth || 0, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
3636
if (!this.isData) {
3737
if (!options.permissive) this.isData = () => false
3838
else this.isData = (data, key) => !(key in this.methods)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-logic-engine",
3-
"version": "5.0.2",
3+
"version": "5.0.3",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

suites/additional.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,27 @@
7878
"result": 1
7979
},
8080
{
81-
"description": "Throws when you try to use reduce with an array as the accumulator (Default)",
82-
"rule": { "reduce": [[1,2,3], 0, []] },
83-
"error": { "type": "Invalid Return" },
81+
"description": "Throws when you try to use reduce with an array as the accumulator beyond allowed depth (Default)",
82+
"rule": { "reduce": [[1,2,3], 0, [[]]] },
83+
"error": { "type": "Exceeded Allowed Depth" },
8484
"data": null
8585
},
8686
{
87-
"description": "Throws when you try to use reduce with an array as the accumulator (Return)",
88-
"rule": { "reduce": [[1,2,3], []] },
89-
"error": { "type": "Invalid Return" },
87+
"description": "Throws when you try to use reduce with an array as the accumulator beyond allowed depth (Return)",
88+
"rule": { "reduce": [[1,2,3], [[]]] },
89+
"error": { "type": "Exceeded Allowed Depth" },
9090
"data": null
9191
},
9292
{
93-
"description": "Throws when you try to use reduce with an object as the accumulator (Default)",
94-
"rule": { "reduce": [[1,2,3], 0, {}] },
95-
"error": { "type": "Invalid Return" },
93+
"description": "Throws when you try to use reduce with an array as the accumulator beyond allowed depth (Return 2)",
94+
"rule": { "reduce": [[1,2,3], [{}]] },
95+
"error": { "type": "Exceeded Allowed Depth" },
9696
"data": null
9797
},
9898
{
99-
"description": "Throws when you try to use reduce with an object as the accumulator (Return)",
100-
"rule": { "reduce": [[1,2,3], {}] },
101-
"error": { "type": "Invalid Return" },
99+
"description": "Throws when you try to use reduce with an array as the accumulator beyond allowed depth (Return 3)",
100+
"rule": { "reduce": [[1,2,3], { "preserve": { "a": [] } }] },
101+
"error": { "type": "Exceeded Allowed Depth" },
102102
"data": null
103103
}
104104
]

utilities/downgrade.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,34 @@ export function assertSize (arr, size) {
2121
}
2222

2323
/**
24-
* Asserts that an item is not of a certain type.
25-
* Treats null as its own type, "null".
26-
* The main use-case for this is to prevent reduce from returning objects.
27-
* @param {*} item
28-
* @param {string} type
24+
* Asserts that an object has a certain allowed depth.
2925
*/
30-
export function assertNotType (item, type = 'object', error = 'Invalid Return') {
31-
const typeOfItem = item === null ? 'null' : typeof item
32-
// eslint-disable-next-line no-throw-literal
33-
if (typeOfItem === type) throw { type: error }
26+
export function assertAllowedDepth (item, depthAllowed = 0) {
27+
if (!item) return item
28+
if (depthAllowed === Infinity) return item
29+
if (typeof item !== 'object') return item
30+
31+
// checks for depthAllowed levels of depth being objects
32+
if (Array.isArray(item)) {
33+
for (let i = 0; i < item.length; i++) {
34+
if (typeof item[i] === 'object' && item[i]) {
35+
// eslint-disable-next-line no-throw-literal
36+
if (depthAllowed === 0) throw { type: 'Exceeded Allowed Depth' }
37+
assertAllowedDepth(item[i], depthAllowed - 1)
38+
}
39+
}
40+
} else {
41+
const keys = Object.keys(item)
42+
for (let i = 0; i < keys.length; i++) {
43+
const val = item[keys[i]]
44+
if (typeof val === 'object' && val) {
45+
// eslint-disable-next-line no-throw-literal
46+
if (depthAllowed === 0) throw { type: 'Exceeded Allowed Depth' }
47+
assertAllowedDepth(val, depthAllowed - 1)
48+
}
49+
}
50+
}
51+
3452
return item
3553
}
3654

0 commit comments

Comments
 (0)