Skip to content

Commit a1fbe32

Browse files
committed
fix(evaluate): handle realm specific corner cases in realm impls
1 parent 9f8e367 commit a1fbe32

File tree

7 files changed

+160
-133
lines changed

7 files changed

+160
-133
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ evaluate(value, '/bar'); // => throws JSONPointerKeyError
329329
[comment]: <> (SPDX-FileCopyrightText: Copyright &#40;c&#41; 2013 IETF Trust and the persons identified as the document authors. All rights reserved.)
330330
[comment]: <> (SPDX-License-Identifier: BSD-2-Clause)
331331

332-
By default, the evaluation is **strict**, meaning error condition will be raised if it fails to
332+
By default, the evaluation is **strict**, meaning an error condition will be raised if it fails to
333333
resolve a concrete value for any of the JSON pointer's reference tokens. For example, if an array
334334
is referenced with a non-numeric token, an error condition will be raised.
335335

src/evaluate/index.js

Lines changed: 88 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import JSONPointerTypeError from '../errors/JSONPointerTypeError.js';
88
import JSONPointerIndexError from '../errors/JSONPointerIndexError.js';
99
import JSONPointerKeyError from '../errors/JSONPointerKeyError.js';
1010

11-
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER.toString();
12-
1311
const evaluate = (
1412
value,
1513
jsonPointer,
@@ -33,46 +31,35 @@ const evaluate = (
3331
})
3432
: null;
3533

36-
if (!parseResult.success) {
37-
let message = `Invalid JSON Pointer: "${jsonPointer}". Syntax error at position ${parseResult.maxMatched}`;
38-
message += parseTrace ? `, expected ${parseTrace.inferExpectations()}` : '';
39-
40-
tracer?.step({
41-
referenceToken: undefined,
42-
input: value,
43-
success: false,
44-
reason: message,
45-
});
46-
47-
throw new JSONPointerEvaluateError(message, {
48-
jsonPointer,
49-
});
50-
}
51-
5234
try {
5335
let output;
5436

37+
if (!parseResult.success) {
38+
let message = `Invalid JSON Pointer: "${jsonPointer}". Syntax error at position ${parseResult.maxMatched}`;
39+
message += parseTrace ? `, expected ${parseTrace.inferExpectations()}` : '';
40+
41+
throw new JSONPointerEvaluateError(message, {
42+
jsonPointer,
43+
currentValue: value,
44+
realm: realm.name,
45+
});
46+
}
47+
5548
return referenceTokens.reduce((current, referenceToken, referenceTokenPosition) => {
5649
if (realm.isArray(current)) {
5750
if (testArrayDash(referenceToken)) {
5851
if (strictArrays) {
59-
const message = `Invalid array index "-" at position ${referenceTokenPosition} in "${jsonPointer}". The "-" token always refers to a nonexistent element during evaluation`;
60-
61-
tracer?.step({
62-
referenceToken,
63-
input: current,
64-
success: false,
65-
reason: message,
66-
});
67-
68-
throw new JSONPointerIndexError(message, {
69-
jsonPointer,
70-
referenceTokens,
71-
referenceToken,
72-
referenceTokenPosition,
73-
currentValue: current,
74-
realm: realm.name,
75-
});
52+
throw new JSONPointerIndexError(
53+
`Invalid array index "-" at position ${referenceTokenPosition} in "${jsonPointer}". The "-" token always refers to a nonexistent element during evaluation`,
54+
{
55+
jsonPointer,
56+
referenceTokens,
57+
referenceToken,
58+
referenceTokenPosition,
59+
currentValue: current,
60+
realm: realm.name,
61+
},
62+
);
7663
} else {
7764
output = realm.evaluate(current, String(realm.sizeOf(current)));
7865

@@ -86,67 +73,48 @@ const evaluate = (
8673
}
8774
}
8875

89-
if (!testArrayIndex(referenceToken) && strictArrays) {
90-
const message = `Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index MUST be "0", or digits without a leading "0"`;
91-
92-
tracer?.step({
93-
referenceToken,
94-
input: current,
95-
success: false,
96-
reason: message,
97-
});
98-
99-
throw new JSONPointerIndexError(message, {
100-
jsonPointer,
101-
referenceTokens,
102-
referenceToken,
103-
referenceTokenPosition,
104-
currentValue: current,
105-
realm: realm.name,
106-
});
76+
if (strictArrays && !testArrayIndex(referenceToken)) {
77+
throw new JSONPointerIndexError(
78+
`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index MUST be "0", or digits without a leading "0"`,
79+
{
80+
jsonPointer,
81+
referenceTokens,
82+
referenceToken,
83+
referenceTokenPosition,
84+
currentValue: current,
85+
realm: realm.name,
86+
},
87+
);
10788
}
10889

10990
const index = Number(referenceToken);
110-
const indexUint32 = index >>> 0;
111-
112-
if (strictArrays && index !== indexUint32) {
113-
const message = `Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index must be an unsigned 32-bit integer`;
114-
115-
tracer?.step({
116-
referenceToken,
117-
input: current,
118-
success: false,
119-
reason: message,
120-
});
121-
122-
throw new JSONPointerIndexError(message, {
123-
jsonPointer,
124-
referenceTokens,
125-
referenceToken,
126-
referenceTokenPosition,
127-
currentValue: current,
128-
realm: realm.name,
129-
});
91+
92+
if (!Number.isSafeInteger(index)) {
93+
throw new JSONPointerIndexError(
94+
`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": index must be a safe integer`,
95+
{
96+
jsonPointer,
97+
referenceTokens,
98+
referenceToken,
99+
referenceTokenPosition,
100+
currentValue: current,
101+
realm: realm.name,
102+
},
103+
);
130104
}
131105

132-
if (strictArrays && index >= realm.sizeOf(current)) {
133-
const message = `Invalid array index "${index}" at position ${referenceTokenPosition} in "${jsonPointer}": out of bounds`;
134-
135-
tracer?.step({
136-
referenceToken,
137-
input: current,
138-
success: false,
139-
reason: message,
140-
});
141-
142-
throw new JSONPointerIndexError(message, {
143-
jsonPointer,
144-
referenceTokens,
145-
referenceToken,
146-
referenceTokenPosition,
147-
currentValue: current,
148-
realm: realm.name,
149-
});
106+
if (!realm.has(current, referenceToken) && strictArrays) {
107+
throw new JSONPointerIndexError(
108+
`Invalid array index "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": out of bounds`,
109+
{
110+
jsonPointer,
111+
referenceTokens,
112+
referenceToken,
113+
referenceTokenPosition,
114+
currentValue: current,
115+
realm: realm.name,
116+
},
117+
);
150118
}
151119

152120
output = realm.evaluate(current, referenceToken);
@@ -162,23 +130,17 @@ const evaluate = (
162130

163131
if (realm.isObject(current)) {
164132
if (!realm.has(current, referenceToken) && strictObjects) {
165-
const message = `Invalid object key "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": key not found in object`;
166-
167-
tracer?.step({
168-
referenceToken,
169-
input: current,
170-
success: false,
171-
reason: message,
172-
});
173-
174-
throw new JSONPointerKeyError(message, {
175-
jsonPointer,
176-
referenceTokens,
177-
referenceToken,
178-
referenceTokenPosition,
179-
currentValue: current,
180-
realm: realm.name,
181-
});
133+
throw new JSONPointerKeyError(
134+
`Invalid object key "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": key not found in object`,
135+
{
136+
jsonPointer,
137+
referenceTokens,
138+
referenceToken,
139+
referenceTokenPosition,
140+
currentValue: current,
141+
realm: realm.name,
142+
},
143+
);
182144
}
183145

184146
output = realm.evaluate(current, referenceToken);
@@ -192,25 +154,26 @@ const evaluate = (
192154
return output;
193155
}
194156

195-
const message = `Invalid reference token "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": cannot be applied to a non-object/non-array value`;
196-
197-
tracer?.step({
198-
referenceToken,
199-
input: current,
200-
success: false,
201-
reason: message,
202-
});
203-
204-
throw new JSONPointerTypeError(message, {
205-
jsonPointer,
206-
referenceTokens,
207-
referenceToken,
208-
referenceTokenPosition,
209-
currentValue: current,
210-
realm: realm.name,
211-
});
157+
throw new JSONPointerTypeError(
158+
`Invalid reference token "${referenceToken}" at position ${referenceTokenPosition} in "${jsonPointer}": cannot be applied to a non-object/non-array value`,
159+
{
160+
jsonPointer,
161+
referenceTokens,
162+
referenceToken,
163+
referenceTokenPosition,
164+
currentValue: current,
165+
realm: realm.name,
166+
},
167+
);
212168
}, value);
213169
} catch (error) {
170+
tracer?.step({
171+
referenceToken: error.referenceToken,
172+
input: error.currentValue,
173+
success: false,
174+
reason: error.message,
175+
});
176+
214177
if (error instanceof JSONPointerEvaluateError) {
215178
throw error;
216179
}

src/evaluate/realms/apidom/index.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isObjectElement, isArrayElement } from '@swagger-api/apidom-core';
22

33
import EvaluationRealm from '../EvaluationRealm.js';
44
import JSONPointerKeyError from '../../../errors/JSONPointerKeyError.js';
5+
import JSONPointerIndexError from '../../../errors/JSONPointerIndexError.js';
56

67
class ApiDOMEvaluationRealm extends EvaluationRealm {
78
name = 'apidom';
@@ -23,7 +24,21 @@ class ApiDOMEvaluationRealm extends EvaluationRealm {
2324

2425
has(node, referenceToken) {
2526
if (this.isArray(node)) {
26-
return Number(referenceToken) < this.sizeOf(node);
27+
const index = Number(referenceToken);
28+
const indexUint32 = index >>> 0;
29+
30+
if (index !== indexUint32) {
31+
throw new JSONPointerIndexError(
32+
`Invalid array index "${referenceToken}": index must be an unsinged 32-bit integer`,
33+
{
34+
referenceToken,
35+
currentValue: node,
36+
realm: this.name,
37+
},
38+
);
39+
}
40+
41+
return indexUint32 < this.sizeOf(node);
2742
}
2843
if (this.isObject(node)) {
2944
const keys = node.keys();
@@ -33,8 +48,9 @@ class ApiDOMEvaluationRealm extends EvaluationRealm {
3348
throw new JSONPointerKeyError(
3449
`Object key "${referenceToken}" is not unique — JSON Pointer requires unique member names`,
3550
{
36-
currentValue: node,
3751
referenceToken,
52+
currentValue: node,
53+
realm: this.name,
3854
},
3955
);
4056
}

src/evaluate/realms/json/index.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import EvaluationRealm from '../EvaluationRealm.js';
2+
import JSONPointerIndexError from '../../../errors/JSONPointerIndexError.js';
23

34
class JSONEvaluationRealm extends EvaluationRealm {
45
name = 'json';
@@ -23,7 +24,21 @@ class JSONEvaluationRealm extends EvaluationRealm {
2324

2425
has(node, referenceToken) {
2526
if (this.isArray(node)) {
26-
return Number(referenceToken) < this.sizeOf(node);
27+
const index = Number(referenceToken);
28+
const indexUint32 = index >>> 0;
29+
30+
if (index !== indexUint32) {
31+
throw new JSONPointerIndexError(
32+
`Invalid array index "${referenceToken}": index must be an unsinged 32-bit integer`,
33+
{
34+
referenceToken,
35+
currentValue: node,
36+
realm: this.name,
37+
},
38+
);
39+
}
40+
41+
return indexUint32 < this.sizeOf(node);
2742
}
2843
if (this.isObject(node)) {
2944
return Object.prototype.hasOwnProperty.call(node, referenceToken);

src/evaluate/realms/map-set/index.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import EvaluationRealm from '../EvaluationRealm.js';
2+
import JSONPointerIndexError from '../../../errors/JSONPointerIndexError.js';
23

34
class MapSetEvaluationRealm extends EvaluationRealm {
45
name = 'map-set';
@@ -30,7 +31,23 @@ class MapSetEvaluationRealm extends EvaluationRealm {
3031

3132
evaluate(node, referenceToken) {
3233
if (this.isArray(node)) {
33-
return [...node][Number(referenceToken)];
34+
const index = Number(referenceToken);
35+
const iterator = node.values();
36+
37+
for (let i = 0; i < index; i += 1) {
38+
if (iterator.next().done) {
39+
throw new JSONPointerIndexError(
40+
`Invalid array index "${referenceToken}": out of bounds`,
41+
{
42+
referenceToken,
43+
currentValue: node,
44+
realm: this.name,
45+
},
46+
);
47+
}
48+
}
49+
50+
return iterator.next().value;
3451
}
3552
return node.get(referenceToken);
3653
}

0 commit comments

Comments
 (0)