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.
@@ -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, 3 ]
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: 2
65
+ no-constant-condition: 0
66
66
  no-const-assign: 2
67
- no-continue: 1
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: 1
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 `$.` to identify the root node
280
- and only use `child` scope expressions for subsequent nodes.
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.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": "^2.1.6",
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
- "perf": "wget -O test/mtg.json https://mtgjson.com/api/v5/AllPrices.json && node test/performance mtg",
52
- "perf-match": "wget -O test/mtg.json https://mtgjson.com/api/v5/AllPrices.json && node test/performance mtg currency"
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
- async function yieldThenProceed (datum, resolve, reject) {
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, 0)
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, index) {
269
- if (index >= arr.length) {
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
- return
271
+ break
276
272
  }
277
273
 
278
- await emit(events.endPrefix + events[type])
279
-
280
- references.delete(obj)
274
+ await action(arr[index])
275
+ }
281
276
 
282
- return
277
+ if (ignoreThisItem) {
278
+ ignoreItems = false
283
279
  }
284
280
 
285
281
  if (ignoreItems) {
286
- return item(obj, arr, type, action, ignoreThisItem, index + 1)
282
+ return
287
283
  }
288
284
 
289
- await action(arr[index])
285
+ await emit(events.endPrefix + events[type])
290
286
 
291
- await item(obj, arr, type, action, ignoreThisItem, index + 1)
287
+ references.delete(obj)
292
288
  }
293
289
 
294
290
  function value (datum, type) {
@@ -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 jsonpath = require('jsonpath')
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 = jsonpath.parse(selector)
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
- await mutex
126
- mutex = await handler(eventData)
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