Skip to content

Modified RFC6901 #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion defaultMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,35 @@ const defaultMethods = {
}
}
},
// Adding this to spec something out, not to merge it.
val: {
method: (args, context, above) => {
let result = context
let start = 0
if (Array.isArray(args[0]) && args[0].length === 1) {
start++
const climb = +Math.abs(args[0][0])
let pos = 0
for (let i = 0; i < climb; i++) {
result = above[pos++]
if (i === above.length - 1 && Array.isArray(result)) {
above = result
result = result[0]
pos = 1
}
}
}

for (let i = start; i < args.length; i++) {
if (args[i] === null) continue
if (result === null || result === undefined) return null
result = result[args[i]]
}
if (typeof result === 'undefined') return null
return result
},
deterministic: false
},
var: (key, context, above, engine) => {
let b
if (Array.isArray(key)) {
Expand Down Expand Up @@ -539,7 +568,7 @@ function createArrayIterativeMethod (name, useTruthy = false) {
}

const method = build(mapper, mapState)
const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null }, context, above]` : buildState.compile`null`
const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null, index: x }, context, above]` : buildState.compile`null`

if (async) {
if (!isSyncDeep(mapper, buildState.engine, buildState)) {
Expand Down
11 changes: 5 additions & 6 deletions general.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ describe('Various Test Cases', () => {
})

it('is able to handle simple path escaping', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, 'b\\.c'] }, { selected: { 'b.c': 2 } }, 2)
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, 'b~2c'] }, { selected: { 'b.c': 2 } }, 2)
})

it('is able to handle simple path escaping in a variable', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, { var: 'key' }] }, { selected: { 'b.c': 2 }, key: 'b\\.c' }, 2)
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { get: [{ var: 'selected' }, { var: 'key' }] }, { selected: { 'b.c': 2 }, key: 'b~2c' }, 2)
})

it('is able to avoid returning functions', async () => {
Expand All @@ -154,24 +154,23 @@ describe('Various Test Cases', () => {
})

it('is able to handle path escaping in a var call', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: 'hello\\.world' }, { 'hello.world': 2 }, 2)
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: 'hello~2world' }, { 'hello.world': 2 }, 2)
})

it('is able to access empty keys', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '.' }, { '': 2 }, 2)
})

it('is able to access dot keys', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\.' }, { '.': 2 }, 2)
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '~2' }, { '.': 2 }, 2)
})

it('is able to access "/" keys from above', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { map: [[1], { '+': [{ var: '' }, { var: '../../..\\/' }] }] }, { '': { '': { '/': 3 } } }, [4])
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { map: [[1], { '+': [{ var: '' }, { var: '../..////~1' }] }] }, { '': { '': { '/': 3 } } }, [4])
})

it('is able to handle path escaping with multiple escapes', async () => {
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\foo' }, { '\\foo': 2 }, 2)
for (const engine of [...normalEngines, ...permissiveEngines]) await testEngine(engine, { var: '\\\\foo' }, { '\\foo': 2 }, 2)
})

it('is able to access the index in the iterators', async () => {
Expand Down
2 changes: 1 addition & 1 deletion suites/scopes.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"rule": {
"map": [
{ "var": "arr" },
{ "+": [{ "var": "../../\\.\\./" }, { "var": "../../..\\/" }]}
{ "+": [{ "var": "../../~2~2~1" }, { "var": "../..////~1" }]}
]
},
"data": { "arr": [1,2,3], "../": 10, "": { "": { "/": 7 }} },
Expand Down
72 changes: 72 additions & 0 deletions suites/val.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[
"Test Specification for val",
{
"description": "Fetches a value from an empty key",
"rule": { "val": "" },
"data": { "" : 1 },
"result": 1
},
{
"description": "Fetches a value from a nested empty key",
"rule": { "val": ["", ""] },
"data": { "" : { "": 2 } },
"result": 2
},
{
"description": "Fetches a value from a doubly nested empty key",
"rule": { "val": ["", "", ""] },
"data": { "" : { "": { "": 3 } } },
"result": 3
},
{
"description": "Fetches a value from a key that is purely a dot",
"rule": { "val": "." },
"data": { "." : 20 },
"result": 20
},
{
"description": "Fetches the entire context",
"rule": { "val": null },
"data": { "": 21 },
"result": { "": 21 }
},
{
"description": "Fetches the entire context for a nested key",
"rule": { "val": "" },
"data": { "": { "": 22 } },
"result": { "": 22 }
},
{
"description": "Using val in a map (remember that null gets the current context, not empty string)",
"rule": { "map": [[1,2,3], { "+": [{ "val": null }, 1] }] },
"data": null,
"result": [2,3,4]
},
{
"description": "Using val in a map (and remember [null] and null are the same)",
"rule": { "map": [[1,2,3], { "+": [{ "val": [null] }, 1] }] },
"data": null,
"result": [2,3,4]
},
"Testing out scopes",
{
"description": "Climb up to get adder",
"rule": { "map": [[1,2,3], { "+": [{ "val": null }, { "val": [[-2], "adder"] }] }] },
"data": { "adder": 10 },
"result": [11,12,13]
},
{
"description": "Climb up to get index",
"rule": { "map": [[1,2,3], { "+": [{ "val": null }, { "val": [[-1], "index"] }] }] },
"data": { "adder": 10 },
"result": [1,3,5]
},
{
"description": "Nested get adder",
"rule": {
"map": [["Test"], { "map": [[1,2,3], { "+": [{"val": null}, {"val": [[-4], "adder"]}] }]} ]
},
"data": { "adder": 10 },
"result": [[11,12,13]]
}
]
66 changes: 48 additions & 18 deletions suites/vars.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,110 @@
"Test Specification for Handling esoteric path traversal",
{
"description": "Fetches a value from an empty key",
"rule": { "var": "." },
"rule": { "var": "/" },
"data": { "" : 1 },
"result": 1
},
{
"description": "Fetches a value from a nested empty key",
"rule": { "var": ".." },
"rule": { "var": "//" },
"data": { "" : { "": 2 } },
"result": 2
},
{
"description": "Fetches a value from a doubly nested empty key",
"rule": { "var": "..." },
"rule": { "var": "///" },
"data": { "" : { "": { "": 3 } } },
"result": 3
},
{
"description": "Fetches a value from a key that is purely a dot",
"rule": { "var": "\\." },
"rule": { "var": "~2" },
"data": { "." : 20 },
"result": 20
},
{
"description": "Fetches a value from a key with a dot in it",
"rule": { "var": "\\.key" },
"rule": { "var": "~2key" },
"data": { ".key" : 4 },
"result": 4
},
{
"description":"Fetches a value from a key with a dot in it (2)",
"rule": { "var": "hello\\.world" },
"rule": { "var": "hello~2world" },
"data": { "hello.world" : 5 },
"result": 5
},
{
"description": "Fetches a value from a key inside an empty key with a dot in it",
"rule": { "var": ".\\.key" },
"rule": { "var": "//~2key" },
"data": { "": { ".key" : 6 } },
"result": 6
},
{
"description": "Going a few levels deep",
"rule": { "var": "..\\.key." },
"rule": { "var": "///~2key/" },
"data": { "": { "": { ".key": { "": 7 }} }},
"result": 7
},
{
"description": "Escape / as well, which is useful for the scope proposal",
"rule": { "var": "\\/" },
"rule": { "var": "~1" },
"data": { "/" : 8 },
"result": 8
},
{
"description": "Though / doesn't inherently need to be escaped",
"rule": { "var": "/" },
"data": { "/" : 9 },
"result": 9
},
{
"description": "Dot then empty key",
"rule": { "var": "\\.." },
"rule": { "var": "~2." },
"data": { "." : { "" : 10 } },
"result": 10
},
{
"description": "Empty key then dot",
"rule": { "var": ".\\." },
"rule": { "var": "//~2" },
"data": { "" : { "." : 11 } },
"result": 11
},
{
"description": "Can use backslack in name, too",
"rule": { "var": "\\\\.Hello" },
"rule": { "var": "\\.Hello" },
"data": { "\\" : { "Hello" : 12 } },
"result": 12
},
{
"description": "Can escape tilde",
"rule": { "var": "~0" },
"data": { "~" : 13 },
"result": 13
},
{
"description": "Fetches a value from an empty key, equivalence",
"rule": { "var": "." },
"data": { "" : 1 },
"result": 1
},
{
"description": "Fetches a value from a nested empty key, equivalence",
"rule": { "var": ".." },
"data": { "" : { "": 2 } },
"result": 2
},
{
"description": "Fetches a value from a doubly nested empty key, equivalence",
"rule": { "var": "..." },
"data": { "" : { "": { "": 3 } } },
"result": 3
},
{
"description": "Old format still works",
"rule": { "var": "hello.world" },
"data": { "hello": { "world": 5 } },
"result": 5
},
{
"description": "New format works too",
"rule": { "var": "hello/world" },
"data": { "hello": { "world": 5 } },
"result": 5
}
]
27 changes: 11 additions & 16 deletions utilities/splitPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,33 @@ export function splitPathMemoized (str) {
return parts
}

const chars = ['~', '/', '.']

/**
* Splits a path string into an array of parts.
*
* @example splitPath('a.b.c') // ['a', 'b', 'c']
* @example splitPath('a\\.b.c') // ['a.b', 'c']
* @example splitPath('a\\\\.b.c') // ['a\\', 'b', 'c']
* @example splitPath('a\\\\\\.b.c') // ['a\\.b', 'c']
* @example splitPath('hello') // ['hello']
* @example splitPath('hello\\') // ['hello\\']
* @example splitPath('hello\\\\') // ['hello\\']
*
* @param {string} str
* @param {string} separator
* @returns {string[]}
*/
export function splitPath (str, separator = '.', escape = '\\', up = '/') {
export function splitPath (str) {
const parts = []
let current = ''

for (let i = 0; i < str.length; i++) {
const char = str[i]
if (char === escape) {
if (str[i + 1] === separator || str[i + 1] === up) {
current += str[i + 1]
if (char === '~') {
if (str[i + 1] === '0' || str[i + 1] === '1' || str[i + 1] === '2') {
current += chars[+str[i + 1]]
i++
} else if (str[i + 1] === escape) {
current += escape
} else if (str[i + 1] === '~') {
current += '~'
i++
// The following else might be something tweaked in a spec.
} else current += escape
} else if (char === separator) {
parts.push(current)
} else throw new Error('Invalid escape sequence')
} else if (char === '.' || char === '/') {
if (i) parts.push(current)
current = ''
} else current += char
}
Expand Down
Loading