Skip to content

Commit

Permalink
fix: handle incoming invalid dynamic metadata
Browse files Browse the repository at this point in the history
Not currently validated by `packaging` library.
Provides the user a better error message than a 500 response.

Fixes: pypi#17389

Signed-off-by: Mike Fiedler <[email protected]>
  • Loading branch information
miketheman committed Jan 10, 2025
1 parent e951ec1 commit 4e93989
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 4 deletions.
14 changes: 14 additions & 0 deletions tests/unit/forklift/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ def test_invalid_dists(self, field_name):
metadata.parse(None, form_data=data)
_assert_invalid_metadata(excinfo.value, field_name.replace("_", "-"))

def test_valid_dynamic(self):
data = MultiDict(metadata_version="2.2", name="spam", version="2.0")
data.add("dynamic", "keywords")
data.add("dynamic", "author")
meta = metadata.parse(None, form_data=data)
assert meta.dynamic == ["keywords", "author"]

def test_invalid_dynamic(self):
data = MultiDict(metadata_version="2.2", name="spam", version="2.0")
data.add("dynamic", "requires")
with pytest.raises(ExceptionGroup) as excinfo:
metadata.parse(None, form_data=data)
_assert_invalid_metadata(excinfo.value, "dynamic")


class TestFromFormData:
def test_valid(self):
Expand Down
22 changes: 18 additions & 4 deletions warehouse/forklift/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from trove_classifiers import all_classifiers, deprecated_classifiers
from webob.multidict import MultiDict

from warehouse.packaging.models import DynamicFieldsEnum
from warehouse.utils import http

SUPPORTED_METADATA_VERSIONS = {"1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"}
Expand Down Expand Up @@ -141,7 +142,7 @@ def _validate_metadata(metadata: Metadata, *, backfill: bool = False):
InvalidMetadata("classifier", f"{classifier!r} is not a valid classifier.")
)

# Validate that no deprecated classifers are being used.
# Validate that no deprecated classifiers are being used.
# NOTE: We only check this is we're not doing a backfill, because backfill
# operations may legitimately use deprecated classifiers.
if not backfill:
Expand Down Expand Up @@ -235,6 +236,19 @@ def _validate_metadata(metadata: Metadata, *, backfill: bool = False):
)
)

# Validate that any `dynamic` fields passed are in the allowed list
# TODO: This probably should be lifted up to packaging.metadata
for field in {"dynamic"}:
if (value := getattr(metadata, field)) is not None:
for key in value:
if key not in map(str.lower, DynamicFieldsEnum.enums):
errors.append(
InvalidMetadata(
_RAW_TO_EMAIL_MAPPING.get(field, field),
f"Dynamic field {key!r} is not a valid dynamic field.",
)
)

# Ensure that License and License-Expression are mutually exclusive
# See https://peps.python.org/pep-0639/#deprecate-license-field
if metadata.license and metadata.license_expression:
Expand Down Expand Up @@ -263,12 +277,12 @@ def _validate_metadata(metadata: Metadata, *, backfill: bool = False):


def parse_form_metadata(data: MultiDict) -> Metadata:
# We construct a RawMetdata using the form data, which we will later pass
# We construct a RawMetadata using the form data, which we will later pass
# to Metadata to get a validated metadata.
#
# NOTE: Form data is very similiar to the email format where the only difference
# NOTE: Form data is very similar to the email format where the only difference
# between a list and a single value is whether or not the same key is used
# multiple times. Thus we will handle things in a similiar way, always
# multiple times. Thus, we will handle things in a similar way, always
# fetching things as a list and then determining what to do based on the
# field type and how many values we found.
#
Expand Down

0 comments on commit 4e93989

Please sign in to comment.