bfj 9.1.1 → 9.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +21 -0
- package/.eslintrc +4 -4
- package/HISTORY.md +25 -0
- package/README.md +7 -2
- package/package.json +6 -6
- package/src/eventify.js +12 -16
- package/src/jsonpath.js +151 -0
- package/src/match.js +2 -2
- package/src/streamify.js +12 -3
- package/src/walk.js +168 -185
- package/test/integration.js +21 -2
- package/test/memory.js +56 -0
- package/test/unit/eventify.js +32 -0
- package/test/unit/jsonpath.js +249 -0
- package/test/unit/streamify.js +69 -0
- package/test/unit/walk.js +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm info:*)",
|
|
5
|
+
"Bash(npm view:*)",
|
|
6
|
+
"Bash(node -e:*)",
|
|
7
|
+
"WebSearch",
|
|
8
|
+
"Bash(npx mocha:*)",
|
|
9
|
+
"Bash(export PATH:*)",
|
|
10
|
+
"Bash(node:*)",
|
|
11
|
+
"Bash(npm install:*)",
|
|
12
|
+
"Bash(npm run unit:*)",
|
|
13
|
+
"Bash(npm run lint:*)",
|
|
14
|
+
"Bash(/usr/bin/tail:*)",
|
|
15
|
+
"Bash(/usr/bin/grep -E '\\(passing|failing\\)')",
|
|
16
|
+
"Bash(npm run integration:*)",
|
|
17
|
+
"Bash(npm test:*)",
|
|
18
|
+
"Bash(npm run memory:*)"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
package/.eslintrc
CHANGED
|
@@ -43,7 +43,7 @@ rules:
|
|
|
43
43
|
keyword-spacing: [ 2, { "before": true, "after": true } ]
|
|
44
44
|
linebreak-style: [ 2, "unix" ]
|
|
45
45
|
lines-around-comment: 0
|
|
46
|
-
max-depth: [ 1,
|
|
46
|
+
max-depth: [ 1, 4 ]
|
|
47
47
|
max-nested-callbacks: [ 1, 2 ]
|
|
48
48
|
max-params: [ 1, 8 ]
|
|
49
49
|
max-statements: 0
|
|
@@ -62,9 +62,9 @@ rules:
|
|
|
62
62
|
no-cond-assign: [ 2, "always" ]
|
|
63
63
|
no-confusing-arrow: 0
|
|
64
64
|
no-console: 1
|
|
65
|
-
no-constant-condition:
|
|
65
|
+
no-constant-condition: 0
|
|
66
66
|
no-const-assign: 2
|
|
67
|
-
no-continue:
|
|
67
|
+
no-continue: 0
|
|
68
68
|
no-control-regex: 2
|
|
69
69
|
no-debugger: 2
|
|
70
70
|
no-delete-var: 2
|
|
@@ -145,7 +145,7 @@ rules:
|
|
|
145
145
|
no-spaced-func: 2
|
|
146
146
|
no-sparse-arrays: 2
|
|
147
147
|
no-sync: 0
|
|
148
|
-
no-ternary:
|
|
148
|
+
no-ternary: 0
|
|
149
149
|
no-this-before-super: 2
|
|
150
150
|
no-throw-literal: 1
|
|
151
151
|
no-trailing-spaces: 2
|
package/HISTORY.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## 9.1.3
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
* memory: eliminate memory leaks in `walk` and `eventify` (8592416af612795d08c5f9b50e8bf611cca303a5)
|
|
8
|
+
* match: remove vulnerable `jsonpath` dependency (d053e0de4cc91c1393b9377f0569eebf91eb84da)
|
|
9
|
+
|
|
10
|
+
### Other changes
|
|
11
|
+
|
|
12
|
+
* repo: tweak lint config (efe9f392127d7ab845bd9c3becb87949cf0a4625)
|
|
13
|
+
* deps: `npm audit fix` (b4d066425e107288f8da3658d29f3e5ec57ad0c9)
|
|
14
|
+
* deps: upgrade `please-release-me` to `3.4.1` (cd65c37f626541599c3eef60ab0011055d304ae0)
|
|
15
|
+
|
|
16
|
+
## 9.1.2
|
|
17
|
+
|
|
18
|
+
### Bug fixes
|
|
19
|
+
|
|
20
|
+
* streamify: fix nonsense race condition mitigation (bcf35db58538b3e4feda31e4d0781ff16ee0d0a7)
|
|
21
|
+
|
|
22
|
+
### Other changes
|
|
23
|
+
|
|
24
|
+
* deps: upgrade `cross-spawn` to `7.0.6` (4a55a0f3e8a0a6eadc3f43bd3c0f33eedb0619a6)
|
|
25
|
+
* eventify: remove redundant `async` (44d90b45f0f84423784ccd835dad20ca8209162b)
|
|
26
|
+
* lint: stop warning on ternary operators (4cb552cd2b69b0432ace5f326b2804f57010e69f)
|
|
27
|
+
|
|
3
28
|
## 9.1.1
|
|
4
29
|
|
|
5
30
|
### Bug fixes
|
package/README.md
CHANGED
|
@@ -276,8 +276,13 @@ the comparison will be made
|
|
|
276
276
|
by calling the [RegExp `test` method][regexp-test]
|
|
277
277
|
with the property key.
|
|
278
278
|
If it is a JSONPath expression,
|
|
279
|
-
it must start with `$.`
|
|
280
|
-
and only
|
|
279
|
+
it must start with `$.`
|
|
280
|
+
and supports only simple child access:
|
|
281
|
+
dot-notation properties (`$.foo.bar`),
|
|
282
|
+
bracket-notation strings (`$["foo"]`),
|
|
283
|
+
numeric indices (`$[0]`),
|
|
284
|
+
and wildcards (`$[*]`).
|
|
285
|
+
Filter, script, and recursive descent expressions are not supported.
|
|
281
286
|
Predicate functions will be called with three arguments:
|
|
282
287
|
`key`, `value` and `depth`.
|
|
283
288
|
If the result of the predicate is a truthy value
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bfj",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.3",
|
|
4
4
|
"description": "Big-friendly JSON. Asynchronous streaming functions for large JSON data sets.",
|
|
5
5
|
"homepage": "https://gitlab.com/philbooth/bfj",
|
|
6
6
|
"bugs": "https://gitlab.com/philbooth/bfj/issues",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"check-types": "^11.2.3",
|
|
33
33
|
"hoopy": "^0.1.4",
|
|
34
|
-
"jsonpath": "^1.1.1",
|
|
35
34
|
"tryer": "^1.0.1"
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
|
@@ -39,16 +38,17 @@
|
|
|
39
38
|
"chai": "^4.5.0",
|
|
40
39
|
"eslint": "^8.57.1",
|
|
41
40
|
"mocha": "^10.7.3",
|
|
42
|
-
"please-release-me": "^
|
|
41
|
+
"please-release-me": "^3.4.1",
|
|
43
42
|
"proxyquire": "^2.1.3",
|
|
44
43
|
"spooks": "^2.0.0"
|
|
45
44
|
},
|
|
46
45
|
"scripts": {
|
|
47
46
|
"lint": "eslint src",
|
|
48
|
-
"test": "npm run unit && npm run integration",
|
|
47
|
+
"test": "npm run unit && npm run integration && npm run memory",
|
|
49
48
|
"unit": "mocha --ui tdd --reporter spec --recursive --colors --slow 120 test/unit",
|
|
50
49
|
"integration": "mocha --ui tdd --reporter spec --colors test/integration",
|
|
51
|
-
"
|
|
52
|
-
"perf
|
|
50
|
+
"memory": "mocha --ui tdd --reporter spec --colors test/memory",
|
|
51
|
+
"perf": "test -f test/mtg.json || curl -o test/mtg.json https://mtgjson.com/api/v5/AllPrices.json && node test/performance mtg",
|
|
52
|
+
"perf-match": "test -f test/mtg.json || curl -o test/mtg.json https://mtgjson.com/api/v5/AllPrices.json && node test/performance mtg currency"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/eventify.js
CHANGED
|
@@ -209,7 +209,7 @@ function eventify (data, options = {}) {
|
|
|
209
209
|
return datum > Number.NEGATIVE_INFINITY && datum < Number.POSITIVE_INFINITY
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
function yieldThenProceed (datum, resolve, reject) {
|
|
213
213
|
setImmediate(async () => {
|
|
214
214
|
try {
|
|
215
215
|
resolve(await afterCoercion(await coerce(datum)))
|
|
@@ -249,7 +249,7 @@ function eventify (data, options = {}) {
|
|
|
249
249
|
|
|
250
250
|
await emit(events[type])
|
|
251
251
|
|
|
252
|
-
await item(obj, arr, type, action, ignoreThisItem
|
|
252
|
+
await item(obj, arr, type, action, ignoreThisItem)
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
async function emit (event, eventData) {
|
|
@@ -265,30 +265,26 @@ function eventify (data, options = {}) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
async function item (obj, arr, type, action, ignoreThisItem
|
|
269
|
-
|
|
270
|
-
if (ignoreThisItem) {
|
|
271
|
-
ignoreItems = false
|
|
272
|
-
}
|
|
273
|
-
|
|
268
|
+
async function item (obj, arr, type, action, ignoreThisItem) {
|
|
269
|
+
for (let index = 0; index < arr.length; index++) {
|
|
274
270
|
if (ignoreItems) {
|
|
275
|
-
|
|
271
|
+
break
|
|
276
272
|
}
|
|
277
273
|
|
|
278
|
-
await
|
|
279
|
-
|
|
280
|
-
references.delete(obj)
|
|
274
|
+
await action(arr[index])
|
|
275
|
+
}
|
|
281
276
|
|
|
282
|
-
|
|
277
|
+
if (ignoreThisItem) {
|
|
278
|
+
ignoreItems = false
|
|
283
279
|
}
|
|
284
280
|
|
|
285
281
|
if (ignoreItems) {
|
|
286
|
-
return
|
|
282
|
+
return
|
|
287
283
|
}
|
|
288
284
|
|
|
289
|
-
await
|
|
285
|
+
await emit(events.endPrefix + events[type])
|
|
290
286
|
|
|
291
|
-
|
|
287
|
+
references.delete(obj)
|
|
292
288
|
}
|
|
293
289
|
|
|
294
290
|
function value (datum, type) {
|
package/src/jsonpath.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = parseJsonPath
|
|
4
|
+
|
|
5
|
+
/* Minimal jsonpath parser,
|
|
6
|
+
* replacing `jsonpath` which depended on `static-eval`
|
|
7
|
+
* (CVE-2026-1615, arbitrary code execution from untrusted input).
|
|
8
|
+
*
|
|
9
|
+
* Only the subset used by bfj is supported:
|
|
10
|
+
* $.foo.bar dot-notation properties
|
|
11
|
+
* $["foo"] bracket-notation strings
|
|
12
|
+
* $[0] numeric indices
|
|
13
|
+
* $[*] wildcards
|
|
14
|
+
*
|
|
15
|
+
* Filter expressions, script expressions, recursive descent, slices, and negative indices
|
|
16
|
+
* are all rejected.
|
|
17
|
+
* No eval, no regex, no dynamic code.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function parseJsonPath (selector) {
|
|
21
|
+
if (typeof selector !== 'string' || selector.length === 0 || selector[0] !== '$') {
|
|
22
|
+
throw new SyntaxError('Invalid jsonpath: must start with $')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = [ { expression: { type: 'root', value: '$' } } ]
|
|
26
|
+
let position = 1
|
|
27
|
+
|
|
28
|
+
if (position >= selector.length) {
|
|
29
|
+
throw new SyntaxError('Invalid jsonpath: must have at least one segment after $')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
while (position < selector.length) {
|
|
33
|
+
const character = selector[position]
|
|
34
|
+
|
|
35
|
+
if (character === '.') {
|
|
36
|
+
position += 1
|
|
37
|
+
if (position >= selector.length || selector[position] === '.') {
|
|
38
|
+
throw new SyntaxError('Invalid jsonpath: unexpected character after dot')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const id = parseIdentifier(selector, position)
|
|
42
|
+
result.push({ expression: { type: 'identifier', value: id.value }, operation: 'member', scope: 'child' })
|
|
43
|
+
position = id.position
|
|
44
|
+
} else if (character === '[') {
|
|
45
|
+
position += 1
|
|
46
|
+
const content = parseBracketContent(selector, position)
|
|
47
|
+
result.push({ expression: content.expression, operation: 'subscript', scope: 'child' })
|
|
48
|
+
|
|
49
|
+
position = content.position
|
|
50
|
+
if (position >= selector.length || selector[position] !== ']') {
|
|
51
|
+
throw new SyntaxError('Invalid jsonpath: unterminated bracket')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
position += 1
|
|
55
|
+
} else {
|
|
56
|
+
throw new SyntaxError(`Invalid jsonpath: unexpected character "${character}"`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseBracketContent (selector, position) {
|
|
64
|
+
if (position >= selector.length) {
|
|
65
|
+
throw new SyntaxError('Invalid jsonpath: unterminated bracket')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const character = selector[position]
|
|
69
|
+
|
|
70
|
+
if (character === '*') {
|
|
71
|
+
return { expression: { type: 'wildcard', value: '*' }, position: position + 1 }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (character === '"' || character === "'") {
|
|
75
|
+
const str = parseStringLiteral(selector, position)
|
|
76
|
+
return { expression: { type: 'string_literal', value: str.value }, position: str.position }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isDigit(character)) {
|
|
80
|
+
const num = parseNumericLiteral(selector, position)
|
|
81
|
+
return { expression: { type: 'numeric_literal', value: num.value }, position: num.position }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new SyntaxError(`Invalid jsonpath: unexpected bracket content "${character}"`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseIdentifier (selector, position) {
|
|
88
|
+
const start = position
|
|
89
|
+
|
|
90
|
+
if (position >= selector.length || !isIdentifierStart(selector[position])) {
|
|
91
|
+
throw new SyntaxError('Invalid jsonpath: expected identifier')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
position += 1
|
|
95
|
+
|
|
96
|
+
while (position < selector.length && isIdentifier(selector[position])) {
|
|
97
|
+
position += 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { value: selector.slice(start, position), position }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseNumericLiteral (selector, position) {
|
|
104
|
+
const start = position
|
|
105
|
+
|
|
106
|
+
while (position < selector.length && isDigit(selector[position])) {
|
|
107
|
+
position += 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { value: parseInt(selector.slice(start, position), 10), position }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseStringLiteral (selector, position) {
|
|
114
|
+
const quote = selector[position]
|
|
115
|
+
position += 1
|
|
116
|
+
const start = position
|
|
117
|
+
|
|
118
|
+
while (position < selector.length && selector[position] !== quote) {
|
|
119
|
+
if (selector[position] === '\\') {
|
|
120
|
+
throw new SyntaxError('Invalid jsonpath: escape sequences in strings are not supported')
|
|
121
|
+
}
|
|
122
|
+
position += 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (position >= selector.length) {
|
|
126
|
+
throw new SyntaxError('Invalid jsonpath: unterminated string')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const value = selector.slice(start, position)
|
|
130
|
+
position += 1
|
|
131
|
+
return { value, position }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isDigit (character) {
|
|
135
|
+
return character >= '0' && character <= '9'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isIdentifier (character) {
|
|
139
|
+
return (character >= 'a' && character <= 'z') ||
|
|
140
|
+
(character >= 'A' && character <= 'Z') ||
|
|
141
|
+
(character >= '0' && character <= '9') ||
|
|
142
|
+
character === '_' ||
|
|
143
|
+
character === '$'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isIdentifierStart (character) {
|
|
147
|
+
return (character >= 'a' && character <= 'z') ||
|
|
148
|
+
(character >= 'A' && character <= 'Z') ||
|
|
149
|
+
character === '_' ||
|
|
150
|
+
character === '$'
|
|
151
|
+
}
|
package/src/match.js
CHANGED
|
@@ -4,7 +4,7 @@ const check = require('check-types')
|
|
|
4
4
|
const DataStream = require('./datastream')
|
|
5
5
|
const events = require('./events')
|
|
6
6
|
const Hoopy = require('hoopy')
|
|
7
|
-
const
|
|
7
|
+
const parseJsonPath = require('./jsonpath')
|
|
8
8
|
const { PassThrough } = require('node:stream')
|
|
9
9
|
const walk = require('./walk')
|
|
10
10
|
|
|
@@ -84,7 +84,7 @@ function match (stream, selector, options = {}) {
|
|
|
84
84
|
check.assert.nonEmptyString(selector)
|
|
85
85
|
|
|
86
86
|
if (selector.startsWith('$.')) {
|
|
87
|
-
selectorPath =
|
|
87
|
+
selectorPath = parseJsonPath(selector)
|
|
88
88
|
check.assert.identical(selectorPath.shift(), {
|
|
89
89
|
expression: {
|
|
90
90
|
type: 'root',
|
package/src/streamify.js
CHANGED
|
@@ -50,6 +50,7 @@ function streamify (data, options = {}) {
|
|
|
50
50
|
streamOptions = { highWaterMark }
|
|
51
51
|
}
|
|
52
52
|
const stream = new JsonStream(read, streamOptions)
|
|
53
|
+
const eventQueue = []
|
|
53
54
|
|
|
54
55
|
let awaitPush = true
|
|
55
56
|
let index = 0
|
|
@@ -58,7 +59,6 @@ function streamify (data, options = {}) {
|
|
|
58
59
|
let isPaused = false
|
|
59
60
|
let isProperty
|
|
60
61
|
let length = 0
|
|
61
|
-
let mutex = Promise.resolve()
|
|
62
62
|
let needsComma
|
|
63
63
|
|
|
64
64
|
emitter.on(events.array, noRacing(array))
|
|
@@ -122,8 +122,17 @@ function streamify (data, options = {}) {
|
|
|
122
122
|
|
|
123
123
|
function noRacing (handler) {
|
|
124
124
|
return async (eventData) => {
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
let resolve
|
|
126
|
+
|
|
127
|
+
eventQueue.push(new Promise(res => resolve = res))
|
|
128
|
+
|
|
129
|
+
if (eventQueue.length > 1) {
|
|
130
|
+
await eventQueue[eventQueue.length - 2]
|
|
131
|
+
eventQueue.shift()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await handler(eventData)
|
|
135
|
+
resolve()
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|