UNPKG

bfj

Version:

Big-friendly JSON. Asynchronous streaming functions for large JSON data sets.

250 lines (204 loc) 9.47 kB
'use strict' const assert = require('chai').assert const modulePath = '../../src/jsonpath' suite('jsonpath:', () => { let parseJsonPath setup(() => { parseJsonPath = require(modulePath) }) test('require does not throw', () => { assert.doesNotThrow(() => require(modulePath)) }) test('require returns function', () => { assert.isFunction(require(modulePath)) }) suite('valid paths:', () => { test('$.foo returns member identifier', () => { assert.deepEqual(parseJsonPath('$.foo'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' }, ]) }) test('$.foo.bar returns chained members', () => { assert.deepEqual(parseJsonPath('$.foo.bar'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' }, { expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' }, ]) }) test('$[0] returns numeric subscript', () => { assert.deepEqual(parseJsonPath('$[0]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' }, ]) }) test('$[42] returns numeric subscript', () => { assert.deepEqual(parseJsonPath('$[42]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'numeric_literal', value: 42 }, operation: 'subscript', scope: 'child' }, ]) }) test('$["foo"] returns string subscript with double quotes', () => { assert.deepEqual(parseJsonPath('$["foo"]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' }, ]) }) test("$['foo'] returns string subscript with single quotes", () => { assert.deepEqual(parseJsonPath("$['foo']"), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' }, ]) }) test('$[*] returns wildcard subscript', () => { assert.deepEqual(parseJsonPath('$[*]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' }, ]) }) test('$.foo.bar[*] returns mixed path', () => { assert.deepEqual(parseJsonPath('$.foo.bar[*]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' }, { expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' }, { expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' }, ]) }) test('$._private returns identifier with underscore prefix', () => { assert.deepEqual(parseJsonPath('$._private'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: '_private' }, operation: 'member', scope: 'child' }, ]) }) test('$.$ref returns identifier with dollar prefix', () => { assert.deepEqual(parseJsonPath('$.$ref'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: '$ref' }, operation: 'member', scope: 'child' }, ]) }) test('$["foo"].bar returns bracket then dot chaining', () => { assert.deepEqual(parseJsonPath('$["foo"].bar'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'string_literal', value: 'foo' }, operation: 'subscript', scope: 'child' }, { expression: { type: 'identifier', value: 'bar' }, operation: 'member', scope: 'child' }, ]) }) test('$[0][1] returns consecutive brackets', () => { assert.deepEqual(parseJsonPath('$[0][1]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' }, { expression: { type: 'numeric_literal', value: 1 }, operation: 'subscript', scope: 'child' }, ]) }) test('$[0].foo[*] returns bracket-dot-bracket chaining', () => { assert.deepEqual(parseJsonPath('$[0].foo[*]'), [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'numeric_literal', value: 0 }, operation: 'subscript', scope: 'child' }, { expression: { type: 'identifier', value: 'foo' }, operation: 'member', scope: 'child' }, { expression: { type: 'wildcard', value: '*' }, operation: 'subscript', scope: 'child' }, ]) }) }) suite('rejection:', () => { test('empty string throws', () => { assert.throws(() => parseJsonPath(''), /Invalid jsonpath/) }) test('$ alone throws', () => { assert.throws(() => parseJsonPath('$'), /Invalid jsonpath/) }) test('does not start with $ throws', () => { assert.throws(() => parseJsonPath('foo.bar'), /Invalid jsonpath/) }) test('$..foo recursive descent throws', () => { assert.throws(() => parseJsonPath('$..foo'), /Invalid jsonpath/) }) test('$.foo[?(@.bar)] filter expression throws', () => { assert.throws(() => parseJsonPath('$.foo[?(@.bar)]'), /Invalid jsonpath/) }) test('$[(@.length-1)] script expression throws', () => { assert.throws(() => parseJsonPath('$[(@.length-1)]'), /Invalid jsonpath/) }) test('$.foo[-1] negative index throws', () => { assert.throws(() => parseJsonPath('$.foo[-1]'), /Invalid jsonpath/) }) test('$.foo[0:5] slice throws', () => { assert.throws(() => parseJsonPath('$.foo[0:5]'), /Invalid jsonpath/) }) test('unterminated bracket throws', () => { assert.throws(() => parseJsonPath('$.foo[0'), /Invalid jsonpath/) }) test('unterminated string throws', () => { assert.throws(() => parseJsonPath('$.foo["bar'), /Invalid jsonpath/) }) test('$.123 identifier starting with digit throws', () => { assert.throws(() => parseJsonPath('$.123'), /Invalid jsonpath/) }) test('backslash escape in double-quoted string throws', () => { assert.throws(() => parseJsonPath('$["foo\\"bar"]'), /Invalid jsonpath/) }) test('backslash escape in single-quoted string throws', () => { assert.throws(() => parseJsonPath("$['it\\'s']"), /Invalid jsonpath/) }) }) suite('security:', () => { test('prototype traversal RCE parses without code execution', () => { assert.throws(() => parseJsonPath('$[?(@.constructor.constructor("return process")().exit())]')) }) test('$.__proto__ parses to inert AST without code execution', () => { const result = parseJsonPath('$.__proto__') assert.deepEqual(result, [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: '__proto__' }, operation: 'member', scope: 'child' }, ]) }) test('$["__proto__"] parses to inert AST without code execution', () => { const result = parseJsonPath('$["__proto__"]') assert.deepEqual(result, [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'string_literal', value: '__proto__' }, operation: 'subscript', scope: 'child' }, ]) }) test('$.constructor parses to inert AST without code execution', () => { const result = parseJsonPath('$.constructor') assert.deepEqual(result, [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: 'constructor' }, operation: 'member', scope: 'child' }, ]) }) test('$["constructor"] parses to inert AST without code execution', () => { const result = parseJsonPath('$["constructor"]') assert.deepEqual(result, [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'string_literal', value: 'constructor' }, operation: 'subscript', scope: 'child' }, ]) }) test('$.prototype parses to inert AST without code execution', () => { const result = parseJsonPath('$.prototype') assert.deepEqual(result, [ { expression: { type: 'root', value: '$' } }, { expression: { type: 'identifier', value: 'prototype' }, operation: 'member', scope: 'child' }, ]) }) test('eval injection in filter throws', () => { assert.throws(() => parseJsonPath('$[?(@.eval("process.exit()"))]')) }) test('semicolon expression escape throws', () => { assert.throws(() => parseJsonPath('$.foo; process.exit()')) }) test('newline expression escape throws', () => { assert.throws(() => parseJsonPath('$.foo\nprocess.exit()')) }) test('CRLF injection throws', () => { assert.throws(() => parseJsonPath("$.foo\r\nrequire('child_process').exec('rm -rf /')")) }) test('Function constructor in bracket throws', () => { assert.throws(() => parseJsonPath('$[new Function("return process")()]')) }) test('eval in bracket throws', () => { assert.throws(() => parseJsonPath('$[eval("1")]')) }) test('bracket injection attempt throws', () => { assert.throws(() => parseJsonPath('$.foo](malicious)[bar')) }) }) })