diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e9bb15ada..4a1ab3bc6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,12 @@ Removals: * Drop support for EOL Python 3.9. (:pull:`1263`) +Fixes for requirements and markers: + +* Reject embedded whitespace in the URL of a direct-reference requirement, so a + newline can no longer split one requirement into two when the parsed result is + serialized. (:pull:`1379`) + 26.3 - 2026-08-03 ~~~~~~~~~~~~~~~~~ diff --git a/src/packaging/_tokenizer.py b/src/packaging/_tokenizer.py index 715842114..9249906ec 100644 --- a/src/packaging/_tokenizer.py +++ b/src/packaging/_tokenizer.py @@ -82,7 +82,7 @@ def __str__(self) -> str: re.VERBOSE | re.IGNORECASE, ), "AT": re.compile(r"\@"), - "URL": re.compile(r"[^ \t]+"), + "URL": re.compile(r"\S+"), "IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"), "VERSION_PREFIX_TRAIL": re.compile(r"\.\*"), "VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"), diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 8c0fd8ede..7e2e3643f 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -235,6 +235,7 @@ def test_file_url(self, url: str) -> None: [ "name>=1", 'name; python_version >= "3"', + "name @ https://example.com/name.whl", ], ) def test_error_when_suffixed_with_line_break( @@ -243,6 +244,14 @@ def test_error_when_suffixed_with_line_break( with pytest.raises(InvalidRequirement): Requirement(requirement + line_break) + @pytest.mark.parametrize("line_break", ["\n", "\r", "\r\n"]) + def test_error_when_url_embeds_line_break(self, line_break: str) -> None: + # A line break inside the URL must not let the remainder be + # absorbed into the URL and later serialized as a second + # requirement line. + with pytest.raises(InvalidRequirement): + Requirement(f"name @ https://example.com/name.whl{line_break}evil==1") + @pytest.mark.parametrize("whitespace", [" ", "\t", " \t"]) def test_trailing_horizontal_whitespace(self, whitespace: str) -> None: assert Requirement("name>=1" + whitespace) == Requirement("name>=1")