diff --git a/jsonschema/_format.py b/jsonschema/_format.py index 62c0e4ee..548305b0 100644 --- a/jsonschema/_format.py +++ b/jsonschema/_format.py @@ -475,7 +475,6 @@ def is_json_pointer(instance: object) -> bool: @_checks_drafts( draft7="relative-json-pointer", draft201909="relative-json-pointer", - draft202012="relative-json-pointer", raises=jsonpointer.JsonPointerException, ) def is_relative_json_pointer(instance: object) -> bool: @@ -496,6 +495,29 @@ def is_relative_json_pointer(instance: object) -> bool: return False return (rest == "#") or bool(jsonpointer.JsonPointer(rest)) + @_checks_drafts( + draft202012="relative-json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_relative_json_pointer_202012(instance: object) -> bool: + # Definition taken from: + # https://json-schema.org/draft/2020-12/relative-json-pointer + if not isinstance(instance, str): + return True + if not instance: + return False + + prefix = re.match( + r"(?:0|[1-9][0-9]*)(?:[+-](?:0|[1-9][0-9]*))?", + instance, + re.ASCII, + ) + if prefix is None: + return False + + rest = instance[prefix.end():] + return (rest == "#") or bool(jsonpointer.JsonPointer(rest)) + with suppress(ImportError): import uri_template diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py index d829f984..659e55c1 100644 --- a/jsonschema/tests/test_format.py +++ b/jsonschema/tests/test_format.py @@ -6,7 +6,12 @@ from jsonschema import FormatChecker, ValidationError from jsonschema.exceptions import FormatError -from jsonschema.validators import Draft4Validator +from jsonschema.validators import ( + Draft4Validator, + Draft7Validator, + Draft201909Validator, + Draft202012Validator, +) BOOM = ValueError("Boom!") BANG = ZeroDivisionError("Bang!") @@ -89,3 +94,40 @@ def test_repr(self): repr(checker), "", ) + + +class TestRelativeJSONPointer(TestCase): + def _checker(self): + checker = Draft202012Validator.FORMAT_CHECKER + if "relative-json-pointer" not in checker.checkers: + self.skipTest("requires the format extra") + return checker + + def test_draft202012_allows_index_manipulation(self): + checker = self._checker() + + for instance in ("0+1", "0+1/foo", "1-2/bar", "0-0", "0-1#"): + with self.subTest(instance=instance): + checker.check(instance, "relative-json-pointer") + + def test_older_drafts_still_reject_index_manipulation(self): + self._checker() + for validator in (Draft7Validator, Draft201909Validator): + with ( + self.subTest(validator=validator.__name__), + self.assertRaises(FormatError), + ): + validator.FORMAT_CHECKER.check( + "0+1/foo", + "relative-json-pointer", + ) + + def test_draft202012_rejects_malformed_index_manipulation(self): + checker = self._checker() + + for instance in ("0+", "0-01", "0++1", "0+1not-a-pointer"): + with ( + self.subTest(instance=instance), + self.assertRaises(FormatError), + ): + checker.check(instance, "relative-json-pointer")