Skip to content

Commit

Permalink
Fix Maximum call stack size exceeded in findSchemaDefinition (#4123)
Browse files Browse the repository at this point in the history
* Add tests for circular and deeply circular refs in definitions

* Fix Maximum call stack size exceeded in findSchemaDefinition

* Update CHANGELOG

* Add tests for findSchemaDefinitionRecursive
  • Loading branch information
MarttiR authored Apr 9, 2024
1 parent b705399 commit 509ef2d
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 12 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ should change the heading of the (upcoming) version to include a major version b
-->

# 5.18.1
# 5.18.2

## @rjsf/core

- Fixed Programmatic submit not working properly in Firefox [#3121](https://github.com/rjsf-team/react-jsonschema-form/issues/3121)

## @rjsf/utils

- [#4116](https://github.com/rjsf-team/react-jsonschema-form/issues/4116) Fix Maximum call stack size exceeded when encountering circular definitions ([Link to PR](https://github.com/rjsf-team/react-jsonschema-form/pull/4123))

# 5.18.0

## @rjsf/antd
Expand Down
54 changes: 43 additions & 11 deletions packages/utils/src/findSchemaDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,69 @@ export function splitKeyElementFromObject(key: string, object: GenericObjectType
return [remaining, value];
}

/** Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the
* path provided by that reference. If `#` is not the first character of the reference, or the path does not exist in
* the schema, then throw an Error. Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema.
/** Given the name of a `$ref` from within a schema, using the `rootSchema`, recursively look up and return the
* sub-schema using the path provided by that reference. If `#` is not the first character of the reference, the path
* does not exist in the schema, or the reference resolves circularly back to itself, then throw an Error.
* Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema.
*
* @param $ref - The ref string for which the schema definition is desired
* @param [rootSchema={}] - The root schema in which to search for the definition
* @param recurseList - List of $refs already resolved to prevent recursion
* @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists
* @throws - Error indicating that no schema for that reference exists
* @throws - Error indicating that no schema for that reference could be resolved
*/
export default function findSchemaDefinition<S extends StrictRJSFSchema = RJSFSchema>(
export function findSchemaDefinitionRecursive<S extends StrictRJSFSchema = RJSFSchema>(
$ref?: string,
rootSchema: S = {} as S
rootSchema: S = {} as S,
recurseList: string[] = []
): S {
let ref = $ref || '';
const ref = $ref || '';
let decodedRef;
if (ref.startsWith('#')) {
// Decode URI fragment representation.
ref = decodeURIComponent(ref.substring(1));
decodedRef = decodeURIComponent(ref.substring(1));
} else {
throw new Error(`Could not find a definition for ${$ref}.`);
}
const current: S = jsonpointer.get(rootSchema, ref);
const current: S = jsonpointer.get(rootSchema, decodedRef);
if (current === undefined) {
throw new Error(`Could not find a definition for ${$ref}.`);
}
if (current[REF_KEY]) {
const nextRef = current[REF_KEY];
if (nextRef) {
// Check for circular references.
if (recurseList.includes(nextRef)) {
if (recurseList.length === 1) {
throw new Error(`Definition for ${$ref} is a circular reference`);
}
const [firstRef, ...restRefs] = recurseList;
const circularPath = [...restRefs, ref, firstRef].join(' -> ');
throw new Error(`Definition for ${firstRef} contains a circular reference through ${circularPath}`);
}
const [remaining, theRef] = splitKeyElementFromObject(REF_KEY, current);
const subSchema = findSchemaDefinition<S>(theRef, rootSchema);
const subSchema = findSchemaDefinitionRecursive<S>(theRef, rootSchema, [...recurseList, ref]);
if (Object.keys(remaining).length > 0) {
return { ...remaining, ...subSchema };
}
return subSchema;
}
return current;
}

/** Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the
* path provided by that reference. If `#` is not the first character of the reference, the path does not exist in
* the schema, or the reference resolves circularly back to itself, then throw an Error. Otherwise return the
* sub-schema. Also deals with nested `$ref`s in the sub-schema.
*
* @param $ref - The ref string for which the schema definition is desired
* @param [rootSchema={}] - The root schema in which to search for the definition
* @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists
* @throws - Error indicating that no schema for that reference could be resolved
*/
export default function findSchemaDefinition<S extends StrictRJSFSchema = RJSFSchema>(
$ref?: string,
rootSchema: S = {} as S
): S {
const recurseList: string[] = [];
return findSchemaDefinitionRecursive($ref, rootSchema, recurseList);
}
61 changes: 61 additions & 0 deletions packages/utils/test/findSchemaDefinition.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RJSFSchema, findSchemaDefinition } from '../src';
import { findSchemaDefinitionRecursive } from '../src/findSchemaDefinition';

const schema: RJSFSchema = {
type: 'object',
Expand All @@ -13,6 +14,21 @@ const schema: RJSFSchema = {
$ref: '#/definitions/stringRef',
title: 'foo',
},
// Reference accidentally pointing to itself.
badCircularNestedRef: {
$ref: '#/definitions/badCircularNestedRef',
},
// Reference accidentally pointing to a chain of references which ultimately
// point back to the original reference.
badCircularDeepNestedRef: {
$ref: '#/definitions/badCircularDeeperNestedRef',
},
badCircularDeeperNestedRef: {
$ref: '#/definitions/badCircularDeepestNestedRef',
},
badCircularDeepestNestedRef: {
$ref: '#/definitions/badCircularDeepNestedRef',
},
},
};

Expand Down Expand Up @@ -41,4 +57,49 @@ describe('findSchemaDefinition()', () => {
it('returns a combined schema made from its nested definition with the extra props', () => {
expect(findSchemaDefinition('#/definitions/extraNestedRef', schema)).toEqual(EXTRA_EXPECTED);
});
it('throws error when ref is a circular reference', () => {
expect(() => findSchemaDefinition('#/definitions/badCircularNestedRef', schema)).toThrowError(
'Definition for #/definitions/badCircularNestedRef is a circular reference'
);
});
it('throws error when ref is a deep circular reference', () => {
expect(() => findSchemaDefinition('#/definitions/badCircularDeepNestedRef', schema)).toThrowError(
'Definition for #/definitions/badCircularDeepNestedRef contains a circular reference through #/definitions/badCircularDeeperNestedRef -> #/definitions/badCircularDeepestNestedRef -> #/definitions/badCircularDeepNestedRef'
);
});
});

describe('findSchemaDefinitionRecursive()', () => {
it('throws error when ref is missing', () => {
expect(() => findSchemaDefinitionRecursive()).toThrowError('Could not find a definition for undefined');
});
it('throws error when ref is malformed', () => {
expect(() => findSchemaDefinitionRecursive('definitions/missing')).toThrowError(
'Could not find a definition for definitions/missing'
);
});
it('throws error when ref does not exist', () => {
expect(() => findSchemaDefinitionRecursive('#/definitions/missing', schema)).toThrowError(
'Could not find a definition for #/definitions/missing'
);
});
it('returns the string ref from its definition', () => {
expect(findSchemaDefinitionRecursive('#/definitions/stringRef', schema)).toBe(schema.definitions!.stringRef);
});
it('returns the string ref from its nested definition', () => {
expect(findSchemaDefinitionRecursive('#/definitions/nestedRef', schema)).toBe(schema.definitions!.stringRef);
});
it('returns a combined schema made from its nested definition with the extra props', () => {
expect(findSchemaDefinitionRecursive('#/definitions/extraNestedRef', schema)).toEqual(EXTRA_EXPECTED);
});
it('throws error when ref is a circular reference', () => {
expect(() => findSchemaDefinitionRecursive('#/definitions/badCircularNestedRef', schema)).toThrowError(
'Definition for #/definitions/badCircularNestedRef is a circular reference'
);
});
it('throws error when ref is a deep circular reference', () => {
expect(() => findSchemaDefinitionRecursive('#/definitions/badCircularDeepNestedRef', schema)).toThrowError(
'Definition for #/definitions/badCircularDeepNestedRef contains a circular reference through #/definitions/badCircularDeeperNestedRef -> #/definitions/badCircularDeepestNestedRef -> #/definitions/badCircularDeepNestedRef'
);
});
});

0 comments on commit 509ef2d

Please sign in to comment.