Skip to content

fix(utils): reject empty tag components in parse_wheel_filename/parse_tag (#577) - #1234

Merged
henryiii merged 1 commit into
pypa:mainfrom
r266-tech:fix/parse-tag-empty-components
Jun 8, 2026
Merged

henryiii merged 1 commit into
pypa:mainfrom
r266-tech:fix/parse-tag-empty-components

Conversation

@r266-tech

Copy link
Copy Markdown
Contributor

Closes part of #577.

parse_wheel_filename / parse_tag silently build nonsense Tag('', 'none', 'any') instances from filenames with an empty interpreter/ABI/platform component, e.g.:

  • foo-1.0--none-any.whl (empty interpreter)
  • foo-1.0-py3-none-.whl (empty platform)
  • foo-1.0-py3.-none-any.whl (empty member of a compressed tag set)

An empty component can never match a real sys_tags() entry, so accepting it silently masks upstream filename corruption/truncation instead of surfacing it.

This raises a new InvalidTag(ValueError) from parse_tag and wraps it as InvalidWheelFilename, mirroring the UnsortedTagsError handling added in #1150.

Deliberately narrow: it only rejects empty components — the slice that is illegal under every spec reading. It does not add the broader interpreter-format grammar check discussed in #577 (the \w+\d+ / shortname-grammar variants), which was left unresolved feature-vs-bug in the original thread.

@henryiii henryiii left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a potential (hand-written) follow-up to reduce the number of times this is parsed. I also asked Claude for a review after I reviewed it, it was happy with it too.

Dumping followup here so I don't lose the stash

diff --git a/src/packaging/tags.py b/src/packaging/tags.py
index 086d42a..8bd4ec1 100644
--- a/src/packaging/tags.py
+++ b/src/packaging/tags.py
@@ -234,23 +234,24 @@ def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
     .. versionadded:: 26.3
        Raises :class:`InvalidTag` on empty tag components.
     """
-    tags = set()
-    interpreters, abis, platforms = tag.split("-")
-    for component in (interpreters, abis, platforms):
-        if "" in component.split("."):
+    component_parts = [component.split(".") for component in tag.split("-")]
+    for parts in component_parts:
+        if "" in parts:
+            component = ".".join(parts)
             raise InvalidTag(f"Tag {tag!r} has an empty component: {component!r}")
-    if validate_order:
-        for component in (interpreters, abis, platforms):
-            parts = component.split(".")
-            if parts != sorted(parts):
-                raise UnsortedTagsError(
-                    f"Tag component {component!r} is not in sorted order per PEP 425"
-                )
-    for interpreter in interpreters.split("."):
-        for abi in abis.split("."):
-            for platform_ in platforms.split("."):
-                tags.add(Tag(interpreter, abi, platform_))
-    return frozenset(tags)
+        if validate_order and parts != sorted(parts):
+            component = ".".join(parts)
+            raise UnsortedTagsError(
+                f"Tag component {component!r} is not in sorted order per PEP 425"
+            )
+
+    interpreters, abis, platforms = component_parts
+    return frozenset(
+        Tag(interpreter, abi, platform_)
+        for interpreter in interpreters
+        for abi in abis
+        for platform_ in platforms
+    )


 def _get_config_var(name: str, warn: bool = False) -> int | str | None:

…_tag

parse_wheel_filename/parse_tag silently built nonsense Tag('', 'none',
'any') instances from filenames with an empty interpreter/ABI/platform
component. An empty component can never match a real sys_tags() entry, so
accepting it masks upstream filename corruption.

Raise a new InvalidTag(ValueError) from parse_tag and wrap it as
InvalidWheelFilename, mirroring the UnsortedTagsError handling from pypa#1150.
Narrow on purpose: rejects only empty components, not the broader
interpreter-format grammar left unresolved in pypa#577.
@henryiii
henryiii force-pushed the fix/parse-tag-empty-components branch from 0ba3ef2 to bf6494c Compare June 8, 2026 14:06
@henryiii
henryiii merged commit c5b253b into pypa:main Jun 8, 2026
59 checks passed
r266-tech added a commit to r266-tech/packaging that referenced this pull request Jun 19, 2026
parse_wheel_filename gained empty-tag-component rejection in 26.3 (pypa#1234)
but only parse_tag got the matching annotation; add .. versionchanged:: 26.3.
parse_sdist_filename's :raises list omitted the invalid-version case (the
Version() call is wrapped as InvalidSdistFilename); add it. Both from pypa#1239.
henryiii pushed a commit that referenced this pull request Jun 19, 2026
#1273)

parse_wheel_filename gained empty-tag-component rejection in 26.3 (#1234)
but only parse_tag got the matching annotation; add .. versionchanged:: 26.3.
parse_sdist_filename's :raises list omitted the invalid-version case (the
Version() call is wrapped as InvalidSdistFilename); add it. Both from #1239.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants