From 36e71877ab68fa67306076e1c07549a2f98d2739 Mon Sep 17 00:00:00 2001 From: fhgffy <102001626+fhgffy@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:24:30 +0800 Subject: [PATCH 1/2] Reject negative array indices in JSON pointers --- referencing/_core.py | 6 ++++++ referencing/tests/test_core.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/referencing/_core.py b/referencing/_core.py index db926a2..0ba0b44 100644 --- a/referencing/_core.py +++ b/referencing/_core.py @@ -265,6 +265,12 @@ def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]: for segment in unquote(pointer[1:]).split("/"): if isinstance(contents, Sequence): segment = int(segment) + # 2026-10-08: JSON array indices cannot count from the end. + if segment < 0: + raise exceptions.PointerToNowhere( + ref=pointer, + resource=self, + ) else: segment = segment.replace("~1", "/").replace("~0", "~") try: diff --git a/referencing/tests/test_core.py b/referencing/tests/test_core.py index 3edddbc..9717a04 100644 --- a/referencing/tests/test_core.py +++ b/referencing/tests/test_core.py @@ -762,6 +762,25 @@ def test_lookup_non_existent_pointer_to_array_index(self): resource=resource, ) + @pytest.mark.parametrize("index", [-1, -2, -4]) + def test_lookup_negative_array_index(self, index): + # 2026-10-08: JSON arrays have no negative-index members. + resource = Resource.opaque([1, 2, 4, 8]) + resolver = Registry({"http://example.com/1": resource}).resolver() + pointer = f"/{index}" + with pytest.raises(exceptions.PointerToNowhere) as exc: + resolver.lookup(f"http://example.com/1#{pointer}") + assert exc.value == exceptions.PointerToNowhere( + ref=pointer, + resource=resource, + ) + + def test_lookup_negative_integer_object_key(self): + # 2026-10-08: Object member names remain literal strings. + resource = Resource.opaque({"-1": "value"}) + resolver = Registry().resolver_with_root(resource) + assert resolver.lookup("#/-1").contents == "value" + def test_lookup_pointer_to_empty_string(self): resolver = Registry().resolver_with_root(Resource.opaque({"": {}})) assert resolver.lookup("#/").contents == {} From a572146c5498eea9849b759c6da35486ceb131f2 Mon Sep 17 00:00:00 2001 From: fhgffy <102001626+fhgffy@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:58:21 +0800 Subject: [PATCH 2/2] Validate JSON Pointer array index spelling before conversion --- referencing/_core.py | 10 +++++--- referencing/tests/test_core.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/referencing/_core.py b/referencing/_core.py index 0ba0b44..dba52bd 100644 --- a/referencing/_core.py +++ b/referencing/_core.py @@ -264,13 +264,17 @@ def pointer(self, pointer: str, resolver: Resolver[D]) -> Resolved[D]: segments: list[int | str] = [] for segment in unquote(pointer[1:]).split("/"): if isinstance(contents, Sequence): - segment = int(segment) - # 2026-10-08: JSON array indices cannot count from the end. - if segment < 0: + # 2026-10-11: Validate unsigned ASCII spelling before int(). + if ( + not segment.isascii() + or not segment.isdecimal() + or (len(segment) > 1 and segment.startswith("0")) + ): raise exceptions.PointerToNowhere( ref=pointer, resource=self, ) + segment = int(segment) else: segment = segment.replace("~1", "/").replace("~0", "~") try: diff --git a/referencing/tests/test_core.py b/referencing/tests/test_core.py index 9717a04..ac0e523 100644 --- a/referencing/tests/test_core.py +++ b/referencing/tests/test_core.py @@ -781,6 +781,50 @@ def test_lookup_negative_integer_object_key(self): resolver = Registry().resolver_with_root(resource) assert resolver.lookup("#/-1").contents == "value" + # 2026-10-11: Array tokens retain their spelling until validation. + @pytest.mark.parametrize( + "index", + [ + "-0", + "+1", + "01", + "00", + " 1", + "1 ", + "\u0661", + "\uff11", + "1_0", + "", + "-", + "foo", + "1x", + ], + ) + def test_lookup_invalid_array_index_spelling(self, index): + resource = Resource.opaque([10, 20, 30]) + resolver = Registry().resolver_with_root(resource) + pointer = f"/{index}" + with pytest.raises(exceptions.PointerToNowhere) as exc: + resolver.lookup(f"#{pointer}") + assert exc.value == exceptions.PointerToNowhere( + ref=pointer, + resource=resource, + ) + + @pytest.mark.parametrize("index", ["0", "1", "2"]) + def test_lookup_unsigned_array_index(self, index): + resolver = Registry().resolver_with_root(Resource.opaque([10, 20, 30])) + assert ( + resolver.lookup(f"#/{index}").contents == [10, 20, 30][int(index)] + ) + + @pytest.mark.parametrize("key", ["-0", "+1", "01"]) + def test_lookup_array_index_spelling_as_object_key(self, key): + resolver = Registry().resolver_with_root( + Resource.opaque({key: "value"}), + ) + assert resolver.lookup(f"#/{key}").contents == "value" + def test_lookup_pointer_to_empty_string(self): resolver = Registry().resolver_with_root(Resource.opaque({"": {}})) assert resolver.lookup("#/").contents == {}