bfj 9.1.2 → 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 +3 -3
- package/HISTORY.md +13 -0
- package/README.md +7 -2
- package/package.json +6 -6
- package/src/eventify.js +11 -15
- package/src/jsonpath.js +151 -0
- package/src/match.js +2 -2
- package/src/walk.js +168 -185
- package/test/memory.js +56 -0
- package/test/unit/jsonpath.js +249 -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
|
package/HISTORY.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 9.1.2
|
|
4
17
|
|
|
5
18
|
### 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
|
@@ -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/walk.js
CHANGED
|
@@ -57,10 +57,6 @@ function initialise (stream, options = {}) {
|
|
|
57
57
|
column: 1
|
|
58
58
|
}
|
|
59
59
|
const emitter = new EventEmitter()
|
|
60
|
-
const handlers = {
|
|
61
|
-
arr: value,
|
|
62
|
-
obj: property
|
|
63
|
-
}
|
|
64
60
|
const json = []
|
|
65
61
|
const lengths = []
|
|
66
62
|
const previousPosition = {}
|
|
@@ -116,7 +112,7 @@ function initialise (stream, options = {}) {
|
|
|
116
112
|
resume()
|
|
117
113
|
} else {
|
|
118
114
|
isWalkBegun = true
|
|
119
|
-
|
|
115
|
+
topLevelLoop()
|
|
120
116
|
}
|
|
121
117
|
}
|
|
122
118
|
|
|
@@ -145,36 +141,97 @@ function initialise (stream, options = {}) {
|
|
|
145
141
|
return lengths[chunkCount - 1].aggregate
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
async function topLevelLoop () {
|
|
145
|
+
try {
|
|
146
|
+
if (shouldHandleNdjson) {
|
|
147
|
+
while (true) {
|
|
148
|
+
await awaitNonWhitespace()
|
|
149
|
+
|
|
150
|
+
if (character() === '\n') {
|
|
151
|
+
hasEndedLine = true
|
|
152
|
+
await next()
|
|
153
|
+
await emit(events.endLine)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (! hasEndedLine) {
|
|
158
|
+
await fail(character(), '\n', currentPosition)
|
|
159
|
+
await next()
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
hasEndedLine = false
|
|
164
|
+
await value()
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
await value()
|
|
168
|
+
|
|
169
|
+
while (true) {
|
|
170
|
+
await awaitNonWhitespace()
|
|
171
|
+
await fail(character(), 'EOF', currentPosition)
|
|
172
|
+
await value()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch (_) {
|
|
176
|
+
setImmediate(endWalk)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function value () {
|
|
181
|
+
if (++count % yieldRate === 0) {
|
|
182
|
+
await new Promise(resolve => {
|
|
183
|
+
setImmediate(resolve)
|
|
184
|
+
})
|
|
151
185
|
}
|
|
152
186
|
|
|
153
|
-
|
|
154
|
-
setImmediate(() => consumeValue(resolve))
|
|
155
|
-
})
|
|
187
|
+
await parseOneValue()
|
|
156
188
|
}
|
|
157
189
|
|
|
158
|
-
async function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
async function parseOneValue () {
|
|
191
|
+
await awaitNonWhitespace()
|
|
192
|
+
const char = await next()
|
|
193
|
+
|
|
194
|
+
switch (char) {
|
|
195
|
+
case '[':
|
|
196
|
+
return array()
|
|
197
|
+
case '{':
|
|
198
|
+
return object()
|
|
199
|
+
case '"':
|
|
200
|
+
return string()
|
|
201
|
+
case '0':
|
|
202
|
+
case '1':
|
|
203
|
+
case '2':
|
|
204
|
+
case '3':
|
|
205
|
+
case '4':
|
|
206
|
+
case '5':
|
|
207
|
+
case '6':
|
|
208
|
+
case '7':
|
|
209
|
+
case '8':
|
|
210
|
+
case '9':
|
|
211
|
+
case '-':
|
|
212
|
+
case '.':
|
|
213
|
+
return number(char)
|
|
214
|
+
case 'f':
|
|
215
|
+
return literalFalse()
|
|
216
|
+
case 'n':
|
|
217
|
+
return literalNull()
|
|
218
|
+
case 't':
|
|
219
|
+
return literalTrue()
|
|
220
|
+
default:
|
|
221
|
+
await fail(char, 'value', previousPosition)
|
|
222
|
+
return value()
|
|
169
223
|
}
|
|
170
224
|
}
|
|
171
225
|
|
|
172
226
|
async function awaitNonWhitespace () {
|
|
173
|
-
|
|
227
|
+
while (true) {
|
|
228
|
+
await awaitCharacter()
|
|
229
|
+
|
|
230
|
+
if (! isWhitespace(character())) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
174
233
|
|
|
175
|
-
if (isWhitespace(character())) {
|
|
176
234
|
await next()
|
|
177
|
-
await awaitNonWhitespace()
|
|
178
235
|
}
|
|
179
236
|
}
|
|
180
237
|
|
|
@@ -272,131 +329,62 @@ function initialise (stream, options = {}) {
|
|
|
272
329
|
return result
|
|
273
330
|
}
|
|
274
331
|
|
|
275
|
-
async function handleValue (char) {
|
|
276
|
-
if (shouldHandleNdjson && scopes.length === 0) {
|
|
277
|
-
if (char === '\n') {
|
|
278
|
-
hasEndedLine = true
|
|
279
|
-
await emit(events.endLine)
|
|
280
|
-
return value()
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (! hasEndedLine) {
|
|
284
|
-
await fail(char, '\n', previousPosition)
|
|
285
|
-
return value()
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
hasEndedLine = false
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
switch (char) {
|
|
292
|
-
case '[':
|
|
293
|
-
return array()
|
|
294
|
-
case '{':
|
|
295
|
-
return object()
|
|
296
|
-
case '"':
|
|
297
|
-
return string()
|
|
298
|
-
case '0':
|
|
299
|
-
case '1':
|
|
300
|
-
case '2':
|
|
301
|
-
case '3':
|
|
302
|
-
case '4':
|
|
303
|
-
case '5':
|
|
304
|
-
case '6':
|
|
305
|
-
case '7':
|
|
306
|
-
case '8':
|
|
307
|
-
case '9':
|
|
308
|
-
case '-':
|
|
309
|
-
case '.':
|
|
310
|
-
return number(char)
|
|
311
|
-
case 'f':
|
|
312
|
-
return literalFalse()
|
|
313
|
-
case 'n':
|
|
314
|
-
return literalNull()
|
|
315
|
-
case 't':
|
|
316
|
-
return literalTrue()
|
|
317
|
-
default:
|
|
318
|
-
await fail(char, 'value', previousPosition)
|
|
319
|
-
return value()
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
332
|
function array () {
|
|
324
333
|
return scope(events.array, value)
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
async function scope (event, contentHandler) {
|
|
328
337
|
await emit(event)
|
|
329
|
-
|
|
330
338
|
scopes.push(event)
|
|
331
|
-
await endScope(event)
|
|
332
|
-
|
|
333
|
-
return await contentHandler()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function emit (...args) {
|
|
337
|
-
if (pause) {
|
|
338
|
-
await pause
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
emitter.emit(...args)
|
|
343
|
-
} catch (err) {
|
|
344
|
-
try {
|
|
345
|
-
emitter.emit(events.error, err)
|
|
346
|
-
} catch (_) {
|
|
347
|
-
// When calling user code, anything is possible
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
339
|
|
|
352
|
-
async function endScope (scp) {
|
|
353
340
|
try {
|
|
354
341
|
await awaitNonWhitespace()
|
|
355
342
|
|
|
356
|
-
if (character() === terminators[
|
|
357
|
-
await emit(events.endPrefix +
|
|
358
|
-
|
|
343
|
+
if (character() === terminators[event]) {
|
|
344
|
+
await emit(events.endPrefix + event)
|
|
359
345
|
scopes.pop()
|
|
360
346
|
await next()
|
|
361
|
-
|
|
362
|
-
await endValue()
|
|
347
|
+
return
|
|
363
348
|
}
|
|
364
|
-
} catch (_) {
|
|
365
|
-
setImmediate(endWalk)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
349
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
await awaitNonWhitespace()
|
|
350
|
+
while (true) {
|
|
351
|
+
await contentHandler()
|
|
372
352
|
|
|
373
|
-
|
|
374
|
-
if (shouldHandleNdjson) {
|
|
375
|
-
await value()
|
|
376
|
-
} else {
|
|
377
|
-
await fail(character(), 'EOF', currentPosition)
|
|
378
|
-
await value()
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const scp = scopes[scopes.length - 1]
|
|
383
|
-
const handler = handlers[scp]
|
|
353
|
+
await awaitNonWhitespace()
|
|
384
354
|
|
|
385
|
-
|
|
355
|
+
if (character() === terminators[event]) {
|
|
356
|
+
await emit(events.endPrefix + event)
|
|
357
|
+
scopes.pop()
|
|
358
|
+
await next()
|
|
359
|
+
return
|
|
360
|
+
}
|
|
386
361
|
|
|
387
|
-
if (scopes.length > 0) {
|
|
388
362
|
const isComma = await checkCharacter(character(), ',', currentPosition)
|
|
389
363
|
if (isComma) {
|
|
390
364
|
await next()
|
|
391
365
|
}
|
|
392
366
|
}
|
|
393
|
-
|
|
394
|
-
await handler()
|
|
395
367
|
} catch (_) {
|
|
396
368
|
setImmediate(endWalk)
|
|
397
369
|
}
|
|
398
370
|
}
|
|
399
371
|
|
|
372
|
+
async function emit (...args) {
|
|
373
|
+
if (pause) {
|
|
374
|
+
await pause
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
emitter.emit(...args)
|
|
379
|
+
} catch (err) {
|
|
380
|
+
try {
|
|
381
|
+
emitter.emit(events.error, err)
|
|
382
|
+
} catch (_) {
|
|
383
|
+
// When calling user code, anything is possible
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
400
388
|
function fail (actual, expected, position) {
|
|
401
389
|
return emit(
|
|
402
390
|
events.dataError,
|
|
@@ -434,30 +422,35 @@ function initialise (stream, options = {}) {
|
|
|
434
422
|
|
|
435
423
|
async function walkString (event) {
|
|
436
424
|
isWalkingString = true
|
|
437
|
-
await walkStringContinue(event, [], false)
|
|
438
|
-
}
|
|
439
425
|
|
|
440
|
-
|
|
441
|
-
|
|
426
|
+
const str = []
|
|
427
|
+
let escaping = false
|
|
442
428
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
429
|
+
while (true) {
|
|
430
|
+
const char = await next()
|
|
431
|
+
|
|
432
|
+
if (escaping) {
|
|
433
|
+
str.push(await escape(char))
|
|
434
|
+
if (++stringChunkCount >= stringChunkSize) {
|
|
435
|
+
await walkStringChunk(event, str)
|
|
436
|
+
}
|
|
437
|
+
escaping = false
|
|
438
|
+
continue
|
|
447
439
|
}
|
|
448
|
-
return walkStringContinue(event, str, false)
|
|
449
|
-
}
|
|
450
440
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
441
|
+
if (char === '\\') {
|
|
442
|
+
escaping = true
|
|
443
|
+
continue
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (char === '"') {
|
|
447
|
+
break
|
|
448
|
+
}
|
|
454
449
|
|
|
455
|
-
if (char !== '"') {
|
|
456
450
|
str.push(char)
|
|
457
451
|
if (++stringChunkCount >= stringChunkSize) {
|
|
458
452
|
await walkStringChunk(event, str)
|
|
459
453
|
}
|
|
460
|
-
return walkStringContinue(event, str, isEscaping)
|
|
461
454
|
}
|
|
462
455
|
|
|
463
456
|
isWalkingString = false
|
|
@@ -465,7 +458,7 @@ function initialise (stream, options = {}) {
|
|
|
465
458
|
await walkStringChunk(event, str)
|
|
466
459
|
stringChunkStart = 0
|
|
467
460
|
|
|
468
|
-
|
|
461
|
+
await emit(event, str.join(''))
|
|
469
462
|
}
|
|
470
463
|
|
|
471
464
|
function walkStringChunk (event, str) {
|
|
@@ -483,7 +476,7 @@ function initialise (stream, options = {}) {
|
|
|
483
476
|
}
|
|
484
477
|
|
|
485
478
|
if (char === 'u') {
|
|
486
|
-
return escapeHex(
|
|
479
|
+
return escapeHex()
|
|
487
480
|
}
|
|
488
481
|
|
|
489
482
|
await fail(char, 'escape character', previousPosition)
|
|
@@ -491,30 +484,25 @@ function initialise (stream, options = {}) {
|
|
|
491
484
|
return `\\${char}`
|
|
492
485
|
}
|
|
493
486
|
|
|
494
|
-
async function escapeHex (
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
if (isHexit(char)) {
|
|
498
|
-
hexits.push(char)
|
|
499
|
-
}
|
|
487
|
+
async function escapeHex () {
|
|
488
|
+
const hexits = []
|
|
500
489
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
490
|
+
for (let i = 0; i < 4; i++) {
|
|
491
|
+
const char = await next()
|
|
504
492
|
|
|
505
|
-
|
|
493
|
+
if (! isHexit(char)) {
|
|
494
|
+
await fail(char, 'hex digit', previousPosition)
|
|
495
|
+
return `\\u${hexits.join('')}${char}`
|
|
496
|
+
}
|
|
506
497
|
|
|
507
|
-
|
|
508
|
-
return String.fromCharCode(parseInt(hexits, 16))
|
|
498
|
+
hexits.push(char)
|
|
509
499
|
}
|
|
510
500
|
|
|
511
|
-
|
|
512
|
-
return `\\u${hexits}${char}`
|
|
501
|
+
return String.fromCharCode(parseInt(hexits.join(''), 16))
|
|
513
502
|
}
|
|
514
503
|
|
|
515
504
|
async function string () {
|
|
516
505
|
await walkString(events.string)
|
|
517
|
-
await endValue()
|
|
518
506
|
}
|
|
519
507
|
|
|
520
508
|
async function number (firstCharacter) {
|
|
@@ -553,14 +541,15 @@ function initialise (stream, options = {}) {
|
|
|
553
541
|
|
|
554
542
|
async function walkDigits (digits) {
|
|
555
543
|
try {
|
|
556
|
-
|
|
544
|
+
while (true) {
|
|
545
|
+
await awaitCharacter()
|
|
546
|
+
|
|
547
|
+
if (! isDigit(character())) {
|
|
548
|
+
return false
|
|
549
|
+
}
|
|
557
550
|
|
|
558
|
-
if (isDigit(character())) {
|
|
559
551
|
digits.push(await next())
|
|
560
|
-
return walkDigits(digits)
|
|
561
552
|
}
|
|
562
|
-
|
|
563
|
-
return false
|
|
564
553
|
} catch (_) {
|
|
565
554
|
return true
|
|
566
555
|
}
|
|
@@ -568,7 +557,6 @@ function initialise (stream, options = {}) {
|
|
|
568
557
|
|
|
569
558
|
async function endNumber (digits) {
|
|
570
559
|
await emit(events.number, parseFloat(digits.join('')))
|
|
571
|
-
await endValue()
|
|
572
560
|
}
|
|
573
561
|
|
|
574
562
|
function literalFalse () {
|
|
@@ -576,32 +564,30 @@ function initialise (stream, options = {}) {
|
|
|
576
564
|
}
|
|
577
565
|
|
|
578
566
|
async function literal (expectedCharacters, val) {
|
|
579
|
-
|
|
580
|
-
await awaitCharacter()
|
|
567
|
+
let consumed = 0
|
|
581
568
|
|
|
582
|
-
|
|
583
|
-
|
|
569
|
+
try {
|
|
570
|
+
for (; consumed < expectedCharacters.length; consumed++) {
|
|
571
|
+
await awaitCharacter()
|
|
584
572
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
throw [ actual, expected ]
|
|
588
|
-
}
|
|
573
|
+
const actual = await next()
|
|
574
|
+
const expected = expectedCharacters[consumed]
|
|
589
575
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
576
|
+
if (actual !== expected) {
|
|
577
|
+
if (consumed < expectedCharacters.length - 1) {
|
|
578
|
+
await fail('EOF', expectedCharacters[consumed + 1], currentPosition)
|
|
579
|
+
} else {
|
|
580
|
+
await fail(actual, expected, previousPosition)
|
|
581
|
+
}
|
|
582
|
+
return
|
|
583
|
+
}
|
|
593
584
|
}
|
|
594
585
|
|
|
595
|
-
await literal
|
|
596
|
-
} catch (
|
|
597
|
-
if (expectedCharacters.length
|
|
598
|
-
await fail('EOF', expectedCharacters[
|
|
599
|
-
} else if (Array.isArray(err)) {
|
|
600
|
-
const [ actual, expected ] = err
|
|
601
|
-
await fail(actual, expected, previousPosition)
|
|
586
|
+
await emit(events.literal, val)
|
|
587
|
+
} catch (_) {
|
|
588
|
+
if (consumed < expectedCharacters.length) {
|
|
589
|
+
await fail('EOF', expectedCharacters[consumed], currentPosition)
|
|
602
590
|
}
|
|
603
|
-
|
|
604
|
-
return endValue()
|
|
605
591
|
}
|
|
606
592
|
}
|
|
607
593
|
|
|
@@ -646,12 +632,9 @@ function initialise (stream, options = {}) {
|
|
|
646
632
|
}
|
|
647
633
|
|
|
648
634
|
async function popScopes () {
|
|
649
|
-
|
|
650
|
-
|
|
635
|
+
while (scopes.length > 0) {
|
|
636
|
+
await fail('EOF', terminators[scopes.pop()], currentPosition)
|
|
651
637
|
}
|
|
652
|
-
|
|
653
|
-
await fail('EOF', terminators[scopes.pop()], currentPosition)
|
|
654
|
-
await popScopes()
|
|
655
638
|
}
|
|
656
639
|
}
|
|
657
640
|
|
package/test/memory.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('chai').assert
|
|
4
|
+
const { Readable } = require('stream')
|
|
5
|
+
|
|
6
|
+
const bfj = require('../src')
|
|
7
|
+
|
|
8
|
+
const ELEMENT_COUNT = 1_000_000
|
|
9
|
+
|
|
10
|
+
suite('memory:', () => {
|
|
11
|
+
test('walk does not OOM on large array', function (done) {
|
|
12
|
+
this.timeout(120_000)
|
|
13
|
+
|
|
14
|
+
const json = `[${new Array(ELEMENT_COUNT).fill('1').join(',')}]`
|
|
15
|
+
const stream = new Readable()
|
|
16
|
+
stream._read = () => {}
|
|
17
|
+
stream.push(json)
|
|
18
|
+
stream.push(null)
|
|
19
|
+
|
|
20
|
+
let count = 0
|
|
21
|
+
const baselineRss = process.memoryUsage().rss
|
|
22
|
+
const emitter = bfj.walk(stream)
|
|
23
|
+
|
|
24
|
+
emitter.on('num', () => { count++ })
|
|
25
|
+
emitter.on('err', done)
|
|
26
|
+
emitter.on('err-data', (err) => done(err))
|
|
27
|
+
emitter.on('end', () => {
|
|
28
|
+
const peakRss = process.memoryUsage().rss
|
|
29
|
+
const growth = peakRss - baselineRss
|
|
30
|
+
assert.strictEqual(count, ELEMENT_COUNT)
|
|
31
|
+
assert.isBelow(growth, 512 * 1024 * 1024, `RSS grew by ${(growth / 1024 / 1024).toFixed(0)} MB`)
|
|
32
|
+
done()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('eventify does not OOM on large array', function (done) {
|
|
37
|
+
this.timeout(120_000)
|
|
38
|
+
|
|
39
|
+
const data = new Array(ELEMENT_COUNT).fill(1)
|
|
40
|
+
|
|
41
|
+
let count = 0
|
|
42
|
+
const baselineRss = process.memoryUsage().rss
|
|
43
|
+
const emitter = bfj.eventify(data)
|
|
44
|
+
|
|
45
|
+
emitter.on('num', () => { count++ })
|
|
46
|
+
emitter.on('err', done)
|
|
47
|
+
emitter.on('err-data', (err) => done(err))
|
|
48
|
+
emitter.on('end', () => {
|
|
49
|
+
const peakRss = process.memoryUsage().rss
|
|
50
|
+
const growth = peakRss - baselineRss
|
|
51
|
+
assert.strictEqual(count, ELEMENT_COUNT)
|
|
52
|
+
assert.isBelow(growth, 512 * 1024 * 1024, `RSS grew by ${(growth / 1024 / 1024).toFixed(0)} MB`)
|
|
53
|
+
done()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('chai').assert
|
|
4
|
+
|
|
5
|
+
const modulePath = '../../src/jsonpath'
|
|
6
|
+
|
|
7
|
+
suite('jsonpath:', () => {
|
|
8
|
+
let parseJsonPath
|
|
9
|
+
|
|
10
|
+
setup(() => {
|
|
11
|
+
parseJsonPath = require(modulePath)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('require does not throw', () => {
|
|
15
|
+
assert.doesNotThrow(() => require(modulePath))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('require returns function', () => {
|
|
19
|
+
assert.isFunction(require(modulePath))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
suite('valid paths:', () => {
|
|
23
|
+
test('$.foo returns member identifier', () => {
|
|
24
|
+
assert.deepEqual(parseJsonPath('$.foo'), [
|
|
25
|
+
{ expression: { type: 'root', value: '$' } },
|
|
26
|
+
{ expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' },
|
|
27
|
+
])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('$.foo.bar returns chained members', () => {
|
|
31
|
+
assert.deepEqual(parseJsonPath('$.foo.bar'), [
|
|
32
|
+
{ expression: { type: 'root', value: '$' } },
|
|
33
|
+
{ expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' },
|
|
34
|
+
{ expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' },
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('$[0] returns numeric subscript', () => {
|
|
39
|
+
assert.deepEqual(parseJsonPath('$[0]'), [
|
|
40
|
+
{ expression: { type: 'root', value: '$' } },
|
|
41
|
+
{ expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' },
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('$[42] returns numeric subscript', () => {
|
|
46
|
+
assert.deepEqual(parseJsonPath('$[42]'), [
|
|
47
|
+
{ expression: { type: 'root', value: '$' } },
|
|
48
|
+
{ expression: { type: 'numeric_literal', value: 42 }, operation: 'subscript', scope: 'child' },
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('$["foo"] returns string subscript with double quotes', () => {
|
|
53
|
+
assert.deepEqual(parseJsonPath('$["foo"]'), [
|
|
54
|
+
{ expression: { type: 'root', value: '$' } },
|
|
55
|
+
{ expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' },
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("$['foo'] returns string subscript with single quotes", () => {
|
|
60
|
+
assert.deepEqual(parseJsonPath("$['foo']"), [
|
|
61
|
+
{ expression: { type: 'root', value: '$' } },
|
|
62
|
+
{ expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' },
|
|
63
|
+
])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('$[*] returns wildcard subscript', () => {
|
|
67
|
+
assert.deepEqual(parseJsonPath('$[*]'), [
|
|
68
|
+
{ expression: { type: 'root', value: '$' } },
|
|
69
|
+
{ expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' },
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('$.foo.bar[*] returns mixed path', () => {
|
|
74
|
+
assert.deepEqual(parseJsonPath('$.foo.bar[*]'), [
|
|
75
|
+
{ expression: { type: 'root', value: '$' } },
|
|
76
|
+
{ expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' },
|
|
77
|
+
{ expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' },
|
|
78
|
+
{ expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' },
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('$._private returns identifier with underscore prefix', () => {
|
|
83
|
+
assert.deepEqual(parseJsonPath('$._private'), [
|
|
84
|
+
{ expression: { type: 'root', value: '$' } },
|
|
85
|
+
{ expression: { type: 'identifier', value: '_private' }, operation: 'member', scope: 'child' },
|
|
86
|
+
])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('$.$ref returns identifier with dollar prefix', () => {
|
|
90
|
+
assert.deepEqual(parseJsonPath('$.$ref'), [
|
|
91
|
+
{ expression: { type: 'root', value: '$' } },
|
|
92
|
+
{ expression: { type: 'identifier', value: '$ref' }, operation: 'member', scope: 'child' },
|
|
93
|
+
])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('$["foo"].bar returns bracket then dot chaining', () => {
|
|
97
|
+
assert.deepEqual(parseJsonPath('$["foo"].bar'), [
|
|
98
|
+
{ expression: { type: 'root', value: '$' } },
|
|
99
|
+
{ expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' },
|
|
100
|
+
{ expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' },
|
|
101
|
+
])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('$[0][1] returns consecutive brackets', () => {
|
|
105
|
+
assert.deepEqual(parseJsonPath('$[0][1]'), [
|
|
106
|
+
{ expression: { type: 'root', value: '$' } },
|
|
107
|
+
{ expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' },
|
|
108
|
+
{ expression: { type: 'numeric_literal', value: 1 }, operation: 'subscript', scope: 'child' },
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('$[0].foo[*] returns bracket-dot-bracket chaining', () => {
|
|
113
|
+
assert.deepEqual(parseJsonPath('$[0].foo[*]'), [
|
|
114
|
+
{ expression: { type: 'root', value: '$' } },
|
|
115
|
+
{ expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' },
|
|
116
|
+
{ expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' },
|
|
117
|
+
{ expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' },
|
|
118
|
+
])
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
suite('rejection:', () => {
|
|
123
|
+
test('empty string throws', () => {
|
|
124
|
+
assert.throws(() => parseJsonPath(''), /Invalid jsonpath/)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('$ alone throws', () => {
|
|
128
|
+
assert.throws(() => parseJsonPath('$'), /Invalid jsonpath/)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('does not start with $ throws', () => {
|
|
132
|
+
assert.throws(() => parseJsonPath('foo.bar'), /Invalid jsonpath/)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('$..foo recursive descent throws', () => {
|
|
136
|
+
assert.throws(() => parseJsonPath('$..foo'), /Invalid jsonpath/)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('$.foo[?(@.bar)] filter expression throws', () => {
|
|
140
|
+
assert.throws(() => parseJsonPath('$.foo[?(@.bar)]'), /Invalid jsonpath/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('$[(@.length-1)] script expression throws', () => {
|
|
144
|
+
assert.throws(() => parseJsonPath('$[(@.length-1)]'), /Invalid jsonpath/)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('$.foo[-1] negative index throws', () => {
|
|
148
|
+
assert.throws(() => parseJsonPath('$.foo[-1]'), /Invalid jsonpath/)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('$.foo[0:5] slice throws', () => {
|
|
152
|
+
assert.throws(() => parseJsonPath('$.foo[0:5]'), /Invalid jsonpath/)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('unterminated bracket throws', () => {
|
|
156
|
+
assert.throws(() => parseJsonPath('$.foo[0'), /Invalid jsonpath/)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('unterminated string throws', () => {
|
|
160
|
+
assert.throws(() => parseJsonPath('$.foo["bar'), /Invalid jsonpath/)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('$.123 identifier starting with digit throws', () => {
|
|
164
|
+
assert.throws(() => parseJsonPath('$.123'), /Invalid jsonpath/)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('backslash escape in double-quoted string throws', () => {
|
|
168
|
+
assert.throws(() => parseJsonPath('$["foo\\"bar"]'), /Invalid jsonpath/)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('backslash escape in single-quoted string throws', () => {
|
|
172
|
+
assert.throws(() => parseJsonPath("$['it\\'s']"), /Invalid jsonpath/)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
suite('security:', () => {
|
|
177
|
+
test('prototype traversal RCE parses without code execution', () => {
|
|
178
|
+
assert.throws(() => parseJsonPath('$[?(@.constructor.constructor("return process")().exit())]'))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('$.__proto__ parses to inert AST without code execution', () => {
|
|
182
|
+
const result = parseJsonPath('$.__proto__')
|
|
183
|
+
assert.deepEqual(result, [
|
|
184
|
+
{ expression: { type: 'root', value: '$' } },
|
|
185
|
+
{ expression: { type: 'identifier', value: '__proto__' }, operation: 'member', scope: 'child' },
|
|
186
|
+
])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('$["__proto__"] parses to inert AST without code execution', () => {
|
|
190
|
+
const result = parseJsonPath('$["__proto__"]')
|
|
191
|
+
assert.deepEqual(result, [
|
|
192
|
+
{ expression: { type: 'root', value: '$' } },
|
|
193
|
+
{ expression: { type: 'string_literal', value: '__proto__' }, operation: 'subscript', scope: 'child' },
|
|
194
|
+
])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('$.constructor parses to inert AST without code execution', () => {
|
|
198
|
+
const result = parseJsonPath('$.constructor')
|
|
199
|
+
assert.deepEqual(result, [
|
|
200
|
+
{ expression: { type: 'root', value: '$' } },
|
|
201
|
+
{ expression: { type: 'identifier', value: 'constructor' }, operation: 'member', scope: 'child' },
|
|
202
|
+
])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('$["constructor"] parses to inert AST without code execution', () => {
|
|
206
|
+
const result = parseJsonPath('$["constructor"]')
|
|
207
|
+
assert.deepEqual(result, [
|
|
208
|
+
{ expression: { type: 'root', value: '$' } },
|
|
209
|
+
{ expression: { type: 'string_literal', value: 'constructor' }, operation: 'subscript', scope: 'child' },
|
|
210
|
+
])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('$.prototype parses to inert AST without code execution', () => {
|
|
214
|
+
const result = parseJsonPath('$.prototype')
|
|
215
|
+
assert.deepEqual(result, [
|
|
216
|
+
{ expression: { type: 'root', value: '$' } },
|
|
217
|
+
{ expression: { type: 'identifier', value: 'prototype' }, operation: 'member', scope: 'child' },
|
|
218
|
+
])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('eval injection in filter throws', () => {
|
|
222
|
+
assert.throws(() => parseJsonPath('$[?(@.eval("process.exit()"))]'))
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('semicolon expression escape throws', () => {
|
|
226
|
+
assert.throws(() => parseJsonPath('$.foo; process.exit()'))
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('newline expression escape throws', () => {
|
|
230
|
+
assert.throws(() => parseJsonPath('$.foo\nprocess.exit()'))
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('CRLF injection throws', () => {
|
|
234
|
+
assert.throws(() => parseJsonPath("$.foo\r\nrequire('child_process').exec('rm -rf /')"))
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('Function constructor in bracket throws', () => {
|
|
238
|
+
assert.throws(() => parseJsonPath('$[new Function("return process")()]'))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('eval in bracket throws', () => {
|
|
242
|
+
assert.throws(() => parseJsonPath('$[eval("1")]'))
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('bracket injection attempt throws', () => {
|
|
246
|
+
assert.throws(() => parseJsonPath('$.foo](malicious)[bar'))
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
})
|
package/test/unit/walk.js
CHANGED
|
@@ -1054,6 +1054,50 @@ suite('walk:', () => {
|
|
|
1054
1054
|
})
|
|
1055
1055
|
})
|
|
1056
1056
|
|
|
1057
|
+
suite('string containing early bad unicode escape sequence:', () => {
|
|
1058
|
+
let stream, emitter
|
|
1059
|
+
|
|
1060
|
+
setup(done => {
|
|
1061
|
+
stream = new Readable()
|
|
1062
|
+
stream._read = () => {}
|
|
1063
|
+
|
|
1064
|
+
emitter = walk(stream)
|
|
1065
|
+
|
|
1066
|
+
stream.push('"\\u0Gxx"')
|
|
1067
|
+
stream.push(null)
|
|
1068
|
+
|
|
1069
|
+
Object.entries(events).forEach(([ key, value ]) => {
|
|
1070
|
+
emitter.on(value, spooks.fn({
|
|
1071
|
+
name: key,
|
|
1072
|
+
log: log
|
|
1073
|
+
}))
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
emitter.on(events.end, done)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
test('dataError event occurred once', () => {
|
|
1080
|
+
assert.strictEqual(log.counts.dataError, 1)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
test('dataError event was dispatched correctly', () => {
|
|
1084
|
+
assert.strictEqual(log.args.dataError[0][0].actual, 'G')
|
|
1085
|
+
assert.strictEqual(log.args.dataError[0][0].expected, 'hex digit')
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
test('string event occurred once', () => {
|
|
1089
|
+
assert.strictEqual(log.counts.string, 1)
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
test('string event was dispatched correctly', () => {
|
|
1093
|
+
assert.strictEqual(log.args.string[0][0], '\\u0Gxx')
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
test('end event occurred once', () => {
|
|
1097
|
+
assert.strictEqual(log.counts.end, 1)
|
|
1098
|
+
})
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1057
1101
|
suite('unterminated string:', () => {
|
|
1058
1102
|
let stream, emitter
|
|
1059
1103
|
|