Skip to content

Commit 7407ce8

Browse files
committed
fix: adjusting dereference caching so it doesn't send itself to infinite loops
1 parent da0fda2 commit 7407ce8

File tree

4 files changed

+2951
-11
lines changed

4 files changed

+2951
-11
lines changed

lib/dereference.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
6969
circular: false,
7070
};
7171

72-
if (options && options.timeoutMs) {
73-
if (Date.now() - startTime > options.timeoutMs) {
74-
throw new TimeoutError(options.timeoutMs);
75-
}
76-
}
72+
checkDereferenceTimeout<S, O>(startTime, options);
73+
7774
const derefOptions = (options.dereference || {}) as DereferenceOptions;
7875
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
7976

@@ -98,6 +95,8 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
9895
result.value = dereferenced.value;
9996
} else {
10097
for (const key of Object.keys(obj)) {
98+
checkDereferenceTimeout<S, O>(startTime, options);
99+
101100
const keyPath = Pointer.join(path, key);
102101
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
103102

@@ -214,7 +213,17 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
214213
const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
215214

216215
const cache = dereferencedCache.get($refPath);
217-
if (cache && !cache.circular) {
216+
217+
if (cache) {
218+
// If the object we found is circular we can immediately return it because it would have been
219+
// cached with everything we need already and we don't need to re-process anything inside it.
220+
//
221+
// If the cached object however is _not_ circular and there are additional keys alongside our
222+
// `$ref` pointer here we should merge them back in and return that.
223+
if (cache.circular) {
224+
return cache;
225+
}
226+
218227
const refKeys = Object.keys($ref);
219228
if (refKeys.length > 1) {
220229
const extraKeys = {};
@@ -294,6 +303,20 @@ function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<
294303
return dereferencedObject;
295304
}
296305

306+
/**
307+
* Check if we've run past our allowed timeout and throw an error if we have.
308+
*
309+
* @param startTime - The time when the dereferencing started.
310+
* @param options
311+
*/
312+
function checkDereferenceTimeout<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(startTime: number, options: O): void {
313+
if (options && options.timeoutMs) {
314+
if (Date.now() - startTime > options.timeoutMs) {
315+
throw new TimeoutError(options.timeoutMs);
316+
}
317+
}
318+
}
319+
297320
/**
298321
* Called when a circular reference is found.
299322
* It sets the {@link $Refs#circular} flag, executes the options.dereference.onCircular callback,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { describe, it } from "vitest";
2+
import $RefParser from "../../../lib/index.js";
3+
import helper from "../../utils/helper.js";
4+
import path from "../../utils/path.js";
5+
6+
import { expect } from "vitest";
7+
8+
describe("Schema with an extensive amount of circular $refs", () => {
9+
it.only("should dereference successfully", async () => {
10+
const circularRefs = new Set<string>();
11+
12+
const parser = new $RefParser<Record<string, any>>();
13+
const schema = await parser.dereference(path.rel("test/specs/circular-extensive/schema.json"), {
14+
dereference: {
15+
onCircular: (ref: string) => circularRefs.add(ref),
16+
},
17+
});
18+
19+
// Ensure that a non-circular $ref was dereferenced.
20+
expect(schema.components?.schemas?.ArrayOfMappedData).toStrictEqual({
21+
type: 'array',
22+
items: {
23+
type: 'object',
24+
properties: {
25+
mappingTypeName: { type: 'string' },
26+
sourceSystemValue: { type: 'string' },
27+
mappedValueID: { type: 'string' },
28+
mappedValue: { type: 'string' }
29+
},
30+
additionalProperties: false
31+
}
32+
});
33+
34+
// Ensure that a circular $ref **was** dereferenced.
35+
expect(circularRefs).toHaveLength(23);
36+
expect(schema.components?.schemas?.Customer?.properties?.customerNode).toStrictEqual({
37+
"type": "array",
38+
"items": {
39+
type: 'object',
40+
properties: {
41+
customerNodeGuid: expect.any(Object),
42+
customerGuid: expect.any(Object),
43+
nodeId: expect.any(Object),
44+
customerGu: expect.any(Object)
45+
},
46+
additionalProperties: false
47+
},
48+
});
49+
});
50+
51+
it("should dereference successfully with `dereference.circular` is `ignore`", async () => {
52+
const circularRefs = new Set<string>();
53+
54+
const parser = new $RefParser<Record<string, any>>();
55+
const schema = await parser.dereference(path.rel("test/specs/circular-extensive/schema.json"), {
56+
dereference: {
57+
onCircular: (ref: string) => circularRefs.add(ref),
58+
circular: 'ignore',
59+
},
60+
});
61+
62+
// Ensure that a non-circular $ref was dereferenced.
63+
expect(schema.components?.schemas?.ArrayOfMappedData).toStrictEqual({
64+
type: 'array',
65+
items: {
66+
type: 'object',
67+
properties: {
68+
mappingTypeName: { type: 'string' },
69+
sourceSystemValue: { type: 'string' },
70+
mappedValueID: { type: 'string' },
71+
mappedValue: { type: 'string' }
72+
},
73+
additionalProperties: false
74+
}
75+
});
76+
77+
// Ensure that a circular $ref was **not** dereferenced.
78+
expect(circularRefs).toHaveLength(23);
79+
expect(schema.components?.schemas?.Customer?.properties?.customerNode).toStrictEqual({
80+
"type": "array",
81+
"items": {
82+
"$ref": "#/components/schemas/CustomerNode",
83+
},
84+
});
85+
});
86+
87+
it('should throw an error if "options.dereference.circular" is false', async () => {
88+
const parser = new $RefParser();
89+
90+
try {
91+
await parser.dereference(path.rel("test/specs/circular-extensive/schema.json"), {
92+
dereference: { circular: false },
93+
});
94+
95+
helper.shouldNotGetCalled();
96+
} catch (err) {
97+
expect(err).to.be.an.instanceOf(ReferenceError);
98+
expect(err.message).to.contain("Circular $ref pointer found at ");
99+
expect(err.message).to.contain("specs/circular-extensive/schema.json#/components/schemas/AssignmentExternalReference/properties/assignment/oneOf/0");
100+
101+
// $Refs.circular should be true
102+
expect(parser.$refs.circular).to.equal(true);
103+
}
104+
});
105+
});

0 commit comments

Comments
 (0)