Skip to content

Commit

Permalink
Merge pull request #39 from flitbit/relative. Fixes #19
Browse files Browse the repository at this point in the history
Support Relative JSON Pointers
  • Loading branch information
Phillip Clark authored May 14, 2021
2 parents 51308ff + f48c5e7 commit 4871821
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ npm install json-ptr
### [nodejs](https://nodejs.org/en/)

```javascript
import { JsonPointer, create } from 'json-ptr';
import { JsonPointer } from 'json-ptr';
```

## API Documentation
Expand Down Expand Up @@ -239,6 +239,8 @@ It is important to recognize in the performance results that _compiled_ options

## Releases

- 2021-05-14 — **2.2.0** _Added Handling for Relative JSON Pointers_
- [Example usage](https://github.com/flitbit/json-ptr/blob/master/examples/relative.ts)
- 2021-05-12 — **2.1.1** _Bug fix for [#36](https://github.com/flitbit/json-ptr/issues/36)_
- @CarolynWebster reported an unintentional behavior change starting at v1.3.0. An operation involving a pointer/path that crossed a null value in the object graph resulted in an exception. In versions prior to v1.3.0 it returned `undefined` as intended. The original behavior has been restored.
- 2021-05-12 — **2.1.0** _Bug fixes for [#28](https://github.com/flitbit/json-ptr/issues/28) and [#30](https://github.com/flitbit/json-ptr/issues/30); **Security Vulnerability Patched**_
Expand Down
115 changes: 115 additions & 0 deletions __tests__/pointer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,4 +474,119 @@ describe('JsonPointer', () => {
expect(p.uriFragmentIdentifier).to.eql('#/foo');
});
});

describe('.relative method', () => {
const doc = {
foo: ['bar', 'baz'],
highly: {
nested: {
objects: true,
},
},
};

it('throws when relative pointer unspecified', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.rel(doc, undefined)).to.throw(
'Invalid type: Relative JSON Pointers are represented as strings.',
);
});
it('throws when relative pointer empty', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.rel(doc, '')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid [0](NaN)', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.rel(doc, 'b/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid 1#/z', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.rel(doc, '1#/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('Spec examples 1', () => {
const p = new JsonPointer('/foo/1');
expect(p.rel(doc, '0')).to.eql('baz');
expect(p.rel(doc, '1/0')).to.eql('bar');
expect(p.rel(doc, '2/highly/nested/objects')).to.eql(true);
expect(p.rel(doc, '0#')).to.eql(1);
expect(p.rel(doc, '1#')).to.eql('foo');
});
it('Spec examples 2', () => {
const p = new JsonPointer('/highly/nested');
expect(p.rel(doc, '0/objects')).to.eql(true);
expect(p.rel(doc, '1/nested/objects')).to.eql(true);
expect(p.rel(doc, '2/foo/0')).to.eql('bar');
expect(p.rel(doc, '0#')).to.eql('nested');
expect(p.rel(doc, '1#')).to.eql('highly');
});
it('returns undefined when relative location cannot exist', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(p.rel(doc, '5/not-here')).to.be.undefined;
});
});

describe('.rel method', () => {
const doc = {
foo: ['bar', 'baz'],
highly: {
nested: {
objects: true,
},
},
};

it('throws when relative pointer unspecified', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative(undefined)).to.throw(
'Invalid type: Relative JSON Pointers are represented as strings.',
);
});
it('throws when relative pointer empty', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative('')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid [0](NaN)', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative('b/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid 1#/z', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative('1#/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer to name (#)', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative('1#')).to.throw(
"We won't compile a pointer that will always return 'nested'. Use JsonPointer.rel(target, ptr) instead.",
);
});
it('throws when relative location cannot exist', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative('5/not-here')).to.throw(
'Relative location does not exist.',
);
});

it('Spec example from 1', () => {
const p = new JsonPointer('/foo/1');
const q = p.relative('2/highly/nested/objects');
expect(q.get(doc)).to.eql(true);
});
it('Spec example from 2', () => {
const p = new JsonPointer('/highly/nested');
const q = p.relative('2/foo/0');
expect(q.get(doc)).to.eql('bar');
});
});
});
14 changes: 12 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2738,7 +2738,7 @@ <h2>Use</h2>
<a href="#nodejs" id="nodejs" style="color: inherit; text-decoration: none;">
<h3><a href="https://nodejs.org/en/">nodejs</a></h3>
</a>
<pre><code class="language-javascript"><span style="color: #AF00DB">import</span><span style="color: #000000"> { </span><span style="color: #001080">JsonPointer</span><span style="color: #000000">, </span><span style="color: #001080">create</span><span style="color: #000000"> } </span><span style="color: #AF00DB">from</span><span style="color: #000000"> </span><span style="color: #A31515">&#039;json-ptr&#039;</span><span style="color: #000000">;</span>
<pre><code class="language-javascript"><span style="color: #AF00DB">import</span><span style="color: #000000"> { </span><span style="color: #001080">JsonPointer</span><span style="color: #000000"> } </span><span style="color: #AF00DB">from</span><span style="color: #000000"> </span><span style="color: #A31515">&#039;json-ptr&#039;</span><span style="color: #000000">;</span>
</code></pre>
<a href="#api-documentation" id="api-documentation" style="color: inherit; text-decoration: none;">
<h2>API Documentation</h2>
Expand Down Expand Up @@ -2991,7 +2991,17 @@ <h2>Performance</h2>
<h2>Releases</h2>
</a>
<ul>
<li><p>2021-05-12 — <strong>2.1.0</strong> <em>Bug fixes for #28 and #30; <strong>Security Vulnerability Patched</strong></em></p>
<li><p>2021-05-14 — <strong>2.2.0</strong> <em>Added Handling for Relative JSON Pointers</em></p>
<ul>
<li><a href="https://github.com/flitbit/json-ptr/blob/master/examples/relative.ts">Example usage</a></li>
</ul>
</li>
<li><p>2021-05-12 — <strong>2.1.1</strong> <em>Bug fix for <a href="https://github.com/flitbit/json-ptr/issues/36">#36</a></em></p>
<ul>
<li>@CarolynWebster reported an unintentional behavior change starting at v1.3.0. An operation involving a pointer/path that crossed a null value in the object graph resulted in an exception. In versions prior to v1.3.0 it returned <code>undefined</code> as intended. The original behavior has been restored.</li>
</ul>
</li>
<li><p>2021-05-12 — <strong>2.1.0</strong> <em>Bug fixes for <a href="https://github.com/flitbit/json-ptr/issues/28">#28</a> and <a href="https://github.com/flitbit/json-ptr/issues/30">#30</a>; <strong>Security Vulnerability Patched</strong></em></p>
<ul>
<li><p>When compiling the accessors for quickly points in an object graph, the <code>.get()</code> method was not properly delimiting single quotes. This error caused the get operation to throw an exception in during normal usage. Worse, in cases where malicious user input was sent directly to <code>json-ptr</code>, the failure to delimit single quotes allowed the execution of arbitrary code (an injection attack). The first of these issues was reported in #28 by @mprast, the second (vulnerability) by @zpbrent. Thanks also to @elimumford for the actual code used for the fix.</p>
</li>
Expand Down
14 changes: 12 additions & 2 deletions docs/modules.html
Original file line number Diff line number Diff line change
Expand Up @@ -2738,7 +2738,7 @@ <h2>Use</h2>
<a href="#nodejs" id="nodejs" style="color: inherit; text-decoration: none;">
<h3><a href="https://nodejs.org/en/">nodejs</a></h3>
</a>
<pre><code class="language-javascript"><span style="color: #AF00DB">import</span><span style="color: #000000"> { </span><span style="color: #001080">JsonPointer</span><span style="color: #000000">, </span><span style="color: #001080">create</span><span style="color: #000000"> } </span><span style="color: #AF00DB">from</span><span style="color: #000000"> </span><span style="color: #A31515">&#039;json-ptr&#039;</span><span style="color: #000000">;</span>
<pre><code class="language-javascript"><span style="color: #AF00DB">import</span><span style="color: #000000"> { </span><span style="color: #001080">JsonPointer</span><span style="color: #000000"> } </span><span style="color: #AF00DB">from</span><span style="color: #000000"> </span><span style="color: #A31515">&#039;json-ptr&#039;</span><span style="color: #000000">;</span>
</code></pre>
<a href="#api-documentation" id="api-documentation" style="color: inherit; text-decoration: none;">
<h2>API Documentation</h2>
Expand Down Expand Up @@ -2991,7 +2991,17 @@ <h2>Performance</h2>
<h2>Releases</h2>
</a>
<ul>
<li><p>2021-05-12 — <strong>2.1.0</strong> <em>Bug fixes for #28 and #30; <strong>Security Vulnerability Patched</strong></em></p>
<li><p>2021-05-14 — <strong>2.2.0</strong> <em>Added Handling for Relative JSON Pointers</em></p>
<ul>
<li><a href="https://github.com/flitbit/json-ptr/blob/master/examples/relative.ts">Example usage</a></li>
</ul>
</li>
<li><p>2021-05-12 — <strong>2.1.1</strong> <em>Bug fix for <a href="https://github.com/flitbit/json-ptr/issues/36">#36</a></em></p>
<ul>
<li>@CarolynWebster reported an unintentional behavior change starting at v1.3.0. An operation involving a pointer/path that crossed a null value in the object graph resulted in an exception. In versions prior to v1.3.0 it returned <code>undefined</code> as intended. The original behavior has been restored.</li>
</ul>
</li>
<li><p>2021-05-12 — <strong>2.1.0</strong> <em>Bug fixes for <a href="https://github.com/flitbit/json-ptr/issues/28">#28</a> and <a href="https://github.com/flitbit/json-ptr/issues/30">#30</a>; <strong>Security Vulnerability Patched</strong></em></p>
<ul>
<li><p>When compiling the accessors for quickly points in an object graph, the <code>.get()</code> method was not properly delimiting single quotes. This error caused the get operation to throw an exception in during normal usage. Worse, in cases where malicious user input was sent directly to <code>json-ptr</code>, the failure to delimit single quotes allowed the execution of arbitrary code (an injection attack). The first of these issues was reported in #28 by @mprast, the second (vulnerability) by @zpbrent. Thanks also to @elimumford for the actual code used for the fix.</p>
</li>
Expand Down
36 changes: 36 additions & 0 deletions examples/relative.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const assert = require('assert');
const { JsonPointer } = require('../dist');

// https://tools.ietf.org/id/draft-handrews-relative-json-pointer-00.html#rfc.section.5.1

const doc = {
foo: ['bar', 'baz'],
highly: {
nested: {
objects: true,
},
},
};

const p = new JsonPointer('/foo/1');
assert(p.rel(doc, '0') == 'baz');
assert(p.rel(doc, '1/0') == 'bar');
assert(p.rel(doc, '2/highly/nested/objects') == true);
assert(p.rel(doc, '0#') == 1);
assert(p.rel(doc, '1#') == 'foo');

const p2 = new JsonPointer('/highly/nested');
assert(p2.rel(doc, '0/objects') == true);
assert(p2.rel(doc, '1/nested/objects') == true);
assert(p2.rel(doc, '2/foo/0') == 'bar');
assert(p2.rel(doc, '0#') == 'nested');
assert(p2.rel(doc, '1#') == 'highly');

// Pre-compile relative pointers to dramatically improve performance in
// scenarios such as loops or when a piece of code will be frequently using
// the same relative location:
const compiled = p2.relative('1/nested/objects');
// ...once compiled, it is just another pointer...
assert(compiled.get(doc, '1/nested/objects') == true);


4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build:watch": "tsc -w --importHelpers -p tsconfig.release.json",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"pretest": "npm run lint",
"pretest": "npm run lint:fix",
"test": "nyc mocha __tests__/**/*.spec.ts",
"test:watch": "chokidar \"*.js\" \"*.json\" \"src/**/*.ts\" \"__tests__/**/*.ts\" --command \"npm run test\" --initial",
"cilint": "eslint . --ext .ts,.tsx --format junit --output-file ./reports/eslint/eslint.xml",
Expand Down Expand Up @@ -91,4 +91,4 @@
"dependencies": {
"tslib": "^2.2.0"
}
}
}
59 changes: 59 additions & 0 deletions src/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
encodeUriFragmentIdentifier,
pickDecoder,
unsetValueAtPath,
decodeRelativePointer,
} from './util';
import {
JsonStringPointer,
UriFragmentIdentifierPointer,
Pointer,
RelativeJsonPointer,
PathSegments,
Encoder,
JsonStringPointerListItem,
Expand Down Expand Up @@ -535,6 +537,63 @@ export class JsonPointer {
return typeof this.get(target) !== 'undefined';
}

/**
* Gets the value in the object graph that is the parent of the pointer location.
* @param target the target of the operation
*/
parent(target: unknown): unknown {
const p = this.path;
if (p.length == 1) return undefined;
const parent = new JsonPointer(p.slice(0, p.length - 1));
return parent.get(target);
}

/**
* Creates a new JsonPointer instance, pointing to the specified relative location in the object graph.
* @param ptr the relative pointer (relative to this)
* @returns A new instance that points to the relative location.
*/
relative(ptr: RelativeJsonPointer): JsonPointer {
const p = this.path;
const decoded = decodeRelativePointer(ptr) as string[];
const n = parseInt(decoded[0]);
if (n > p.length) throw new Error('Relative location does not exist.');
const r = p.slice(0, p.length - n).concat(decoded.slice(1));
if (decoded[0][decoded[0].length - 1] == '#') {
// It references the path segment/name, not the value
const name = r[r.length - 1] as string;
throw new Error(
`We won't compile a pointer that will always return '${name}'. Use JsonPointer.rel(target, ptr) instead.`,
);
}
return new JsonPointer(r);
}

/**
* Resolves the specified relative pointer path against the specified target object, and gets the target object's value at the relative pointer's location.
* @param target the target of the operation
* @param ptr the relative pointer (relative to this)
* @returns the value at the relative pointer's resolved path; otherwise undefined.
*/
rel(target: unknown, ptr: RelativeJsonPointer): unknown {
const p = this.path;
const decoded = decodeRelativePointer(ptr) as string[];
const n = parseInt(decoded[0]);
if (n > p.length) {
// out of bounds
return undefined;
}
const r = p.slice(0, p.length - n).concat(decoded.slice(1));
const other = new JsonPointer(r);
if (decoded[0][decoded[0].length - 1] == '#') {
// It references the path segment/name, not the value
const name = r[r.length - 1] as string;
const parent = other.parent(target);
return Array.isArray(parent) ? parseInt(name, 10) : name;
}
return other.get(target);
}

/**
* Creates a new instance by concatenating the specified pointer's path onto this pointer's path.
* @param ptr the string representation of a pointer, it's decoded path, or an instance of JsonPointer indicating the additional path to concatenate onto the pointer.
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export type PathSegment = string | number;
export type PathSegments = readonly PathSegment[];
export type PathSegments = PathSegment[];

export type JsonStringPointer = string;
export type UriFragmentIdentifierPointer = string;
export type Pointer = JsonStringPointer | UriFragmentIdentifierPointer;
export type RelativeJsonPointer = string;

/**
* List item used when listing pointers and their values in an object graph.
Expand Down
35 changes: 35 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
JsonStringPointer,
UriFragmentIdentifierPointer,
Pointer,
RelativeJsonPointer,
PathSegment,
PathSegments,
Decoder,
Expand Down Expand Up @@ -146,6 +147,40 @@ export function encodeUriFragmentIdentifier(
return '#/'.concat(encodeFragmentSegments(path).join('/'));
}

const InvalidRelativePointerError =
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.';

export function decodeRelativePointer(ptr: RelativeJsonPointer): PathSegments {
if (typeof ptr !== 'string') {
throw new TypeError(
'Invalid type: Relative JSON Pointers are represented as strings.',
);
}
if (ptr.length === 0) {
// https://tools.ietf.org/id/draft-handrews-relative-json-pointer-00.html#rfc.section.3
throw new ReferenceError(InvalidRelativePointerError);
}
const segments = ptr.split('/');
let first = segments[0];
// It is a name reference; strip the hash.
if (first[first.length - 1] == '#') {
if (segments.length > 1) {
throw new ReferenceError(InvalidRelativePointerError);
}
first = first.substr(0, first.length - 1);
}
let i = -1;
const len = first.length;
while (++i < len) {
if (first[i] < '0' || first[i] > '9') {
throw new ReferenceError(InvalidRelativePointerError);
}
}
const path: unknown[] = decodePointerSegments(segments.slice(1));
path.unshift(segments[0]);
return path as PathSegments;
}

export function toArrayIndexReference(
arr: readonly unknown[],
idx: PathSegment,
Expand Down

0 comments on commit 4871821

Please sign in to comment.