diff --git a/src/wheel/_commands/tags.py b/src/wheel/_commands/tags.py index 76e798b3..7cbbd6cd 100644 --- a/src/wheel/_commands/tags.py +++ b/src/wheel/_commands/tags.py @@ -19,12 +19,25 @@ def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str return set(original_tags) if new_tags.startswith("+"): - return {*original_tags, *new_tags[1:].split(".")} + parts = new_tags[1:].split(".") + _reject_empty_tag_components(new_tags, parts) + return {*original_tags, *parts} if new_tags.startswith("-"): - return set(original_tags) - set(new_tags[1:].split(".")) + parts = new_tags[1:].split(".") + _reject_empty_tag_components(new_tags, parts) + return set(original_tags) - set(parts) - return set(new_tags.split(".")) + parts = new_tags.split(".") + _reject_empty_tag_components(new_tags, parts) + return set(parts) + + +def _reject_empty_tag_components(new_tags: str, parts: list[str]) -> None: + if not parts or any(part == "" for part in parts): + raise ValueError( + f"Invalid tags value {new_tags!r}: empty tag components are not allowed" + ) def _strip_zip64_extra(extra: bytes) -> bytes: diff --git a/tests/commands/test_tags.py b/tests/commands/test_tags.py index a735a489..2ff1ca71 100644 --- a/tests/commands/test_tags.py +++ b/tests/commands/test_tags.py @@ -287,3 +287,11 @@ def test_retag_does_not_leak_zip64_into_local_headers( ) output_file.unlink() + + +def test_compute_tags_rejects_empty_components() -> None: + from wheel._commands.tags import _compute_tags + + for bad in ("", "py3..py2", "+", "+py2.", "-", "py3."): + with pytest.raises(ValueError, match="empty tag"): + _compute_tags(["py3"], bad)