diff --git a/docs/source/release-notes/unreleased.rst b/docs/source/release-notes/unreleased.rst index 09b03b4..0326975 100644 --- a/docs/source/release-notes/unreleased.rst +++ b/docs/source/release-notes/unreleased.rst @@ -1,6 +1,10 @@ 2.x.y - 202z-aa-bb ------------------ -- *Add Items here* +- Preserve the leading slash required by RFC 3986 when removing dot segments + from rootless paths, fixing resolution against bases such as + ``scheme:foo/bar``. See `issue #84`_. .. links below here + +.. _issue #84: https://github.com/python-hyper/rfc3986/issues/84 diff --git a/src/rfc3986/normalizers.py b/src/rfc3986/normalizers.py index 79b3585..5c4ca72 100644 --- a/src/rfc3986/normalizers.py +++ b/src/rfc3986/normalizers.py @@ -152,13 +152,12 @@ def remove_dot_segments(s: str) -> str: # element elif output: output.pop() + # Preserve the slash that replaces '/..' when the last segment + # is removed, even if the original path was rootless. + if not output: + output.append("") - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if s.startswith("/") and (not output or output[0]): - output.insert(0, "") - - # If the path starts with '/.' or '/..' ensure we add one more empty + # If the path ends with '/.' or '/..' ensure we add one more empty # string to add a trailing '/' if s.endswith(("/.", "/..")): output.append("") diff --git a/tests/test_normalizers.py b/tests/test_normalizers.py index 87562ac..46690ec 100644 --- a/tests/test_normalizers.py +++ b/tests/test_normalizers.py @@ -41,6 +41,15 @@ def test_normalize_percent_characters(): ("//a/./b/../b/%63/%7Bfoo%7D", "//a/b/%63/%7Bfoo%7D"), ("mid/content=5/../6", "mid/6"), ("/a/b/c/./../../g", "/a/g"), + ("foo/../baz", "/baz"), + ("foo/..", "/"), + ("foo/../", "/"), + ("foo/bar/../../baz", "/baz"), + ("foo/../../baz", "/baz"), + ("foo/..//baz", "//baz"), + ("foo//../baz", "foo/baz"), + ("../", ""), + ("../../baz", "baz"), ] diff --git a/tests/test_uri.py b/tests/test_uri.py index 4ae383c..b03572e 100644 --- a/tests/test_uri.py +++ b/tests/test_uri.py @@ -293,6 +293,22 @@ def test_uris_with_no_authority_with_query_only_are_absolute( class TestURIReferencesResolve: + @pytest.mark.parametrize( + ["relative", "expected"], + [ + ("../baz", "scheme:/baz"), + ("..", "scheme:/"), + ("../../baz", "scheme:/baz"), + ("../baz?query#fragment", "scheme:/baz?query#fragment"), + ("baz", "scheme:foo/baz"), + ], + ) + def test_resolve_with_rootless_base(self, relative, expected): + base = URIReference.from_string("scheme:foo/bar") + reference = URIReference.from_string(relative) + + assert reference.resolve_with(base).unsplit() == expected + def test_with_basic_and_relative_uris(self, basic_uri, relative_uri): R = URIReference.from_string(relative_uri) B = URIReference.from_string(basic_uri)