Skip to content
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

Fix Maximum call stack size exceeded in findSchemaDefinition #4123

Merged
Merged
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
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
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
* @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,
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
rootSchema: S = {} as S
rootSchema: S = {} as S,
recurseList: string[] = []
): S {
let ref = $ref || '';
const ref = $ref || '';
let decodedRef;
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
if (ref.startsWith('#')) {
// Decode URI fragment representation.
ref = decodeURIComponent(ref.substring(1));
decodedRef = decodeURIComponent(ref.substring(1));
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
} 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);
heath-freenome marked this conversation as resolved.
Show resolved Hide resolved
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'
);
});
});