Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/source/release-notes/unreleased.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
2.x.y - 202z-aa-bb
------------------

- *Add Items here*
- Reject colons in the first path segment when validating a relative
reference without a scheme.

.. links below here
4 changes: 3 additions & 1 deletion src/rfc3986/_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ def path_is_valid(self, require: bool = False) -> bool:
"This method will be eventually removed.",
DeprecationWarning,
)
return validators.path_is_valid(self.path, require)
return validators._path_is_valid_for_scheme(
self.path, self.scheme, require
)

def query_is_valid(self, require: bool = False) -> bool:
"""Determine if the query component is valid.
Expand Down
19 changes: 17 additions & 2 deletions src/rfc3986/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,17 @@ def path_is_valid(path: t.Optional[str], require: bool = False) -> bool:
return is_valid(path, misc.PATH_MATCHER, require)


def _path_is_valid_for_scheme(
path: t.Optional[str],
scheme: t.Optional[str],
require: bool = False,
) -> bool:
"""Apply the path-noscheme restriction to relative references."""
if not scheme and path and ":" in path.split("/", 1)[0]:
return False
return path_is_valid(path, require)


def query_is_valid(query: t.Optional[str], require: bool = False) -> bool:
"""Determine if the query component is valid.

Expand Down Expand Up @@ -469,8 +480,12 @@ def ensure_components_are_valid(
# https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered
continue # nocov: Python 2.7, 3.3, 3.4

validator = _COMPONENT_VALIDATORS[component]
if not validator(getattr(uri, component)):
if component == "path":
valid = _path_is_valid_for_scheme(uri.path, uri.scheme)
else:
validator = _COMPONENT_VALIDATORS[component]
valid = validator(getattr(uri, component))
if not valid:
invalid_components.add(component)

if invalid_components:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_relative_path_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

from rfc3986 import exceptions
from rfc3986 import uri_reference
from rfc3986 import validators


@pytest.mark.parametrize("text", [".://", "1:a", ":a", "a_b:c", "a%20b:c"])
def test_relative_first_segment_cannot_contain_colon(text):
reference = uri_reference(text)
assert reference.scheme is None
with pytest.raises(exceptions.InvalidComponentsError):
validators.Validator().check_validity_of("path").validate(reference)
assert not reference.path_is_valid()
assert not reference.is_valid()


@pytest.mark.parametrize(
"text",
["./a:b", "/a:b", "a/b:c", "a%3Ab", "a:b", "urn:a:b", "", "?q=a:b"],
)
def test_colons_remain_valid_outside_relative_first_segment(text):
reference = uri_reference(text)
validators.Validator().check_validity_of("path").validate(reference)
assert reference.path_is_valid()
assert reference.is_valid()


def test_component_validation_does_not_infer_uri_context():
assert validators.path_is_valid("a:b")


def test_path_validation_remains_opt_in():
validators.Validator().validate(uri_reference(".://"))


def test_required_path_still_rejects_missing_path():
assert not uri_reference("https://example.com").path_is_valid(require=True)