packageurl-python icon indicating copy to clipboard operation
packageurl-python copied to clipboard

Support private npm packages that contain `/` in the name

Open eredwine opened this issue 1 year ago • 0 comments

While working with PackageURL.from_string, I came across a purl string that looked like this: pkg:npm/@stencil/core/[email protected]. PackageURL.from_string("pkg:npm/@stencil/core/[email protected]") failed with the following error:

Traceback (most recent call last):
  File "/repos/sw-factory/hoppr/hoppr/./test.py", line 7, in <module>
    purl = PackageURL.from_string(unquote("pkg:npm/@stencil/core/[email protected]"))
  File "/repos/sw-factory/hoppr/hoppr/venv/lib/python3.10/site-packages/packageurl/__init__.py", line 538, in from_string
    raise ValueError(f"purl is missing the required name component: {repr(purl)}")
ValueError: purl is missing the required name component: 'pkg:npm/@stencil/core/[email protected]'

I know that the purl should be pkg:npm/%40stencil/core%[email protected] according to the spec. But, I also noticed that PackageURL.from_string already has special logic to handle npm purl types whose namespace begins with @. But, the special logic breaks down in the case where the name field contains a /.

In the if block there are two cases where name could end up being empty. The first is if remainder is an empty string after the version logic. The second is if namespace is set and len(ns_name_parts) is greater than 1. I would expect the former to be an actual error while the latter is a weird corner case when dealing with invalid purls like pkg:npm/@stencil/core/[email protected].

So, I was hoping to fix the weird corner case by treating ns_name as the name field if namespace has already been set via the if type == "npm" and path.startswith("@") step.

        name = ""
        if not namespace and len(ns_name_parts) > 1:
            name = ns_name_parts[-1]
            ns = ns_name_parts[0:-1]
            namespace = "/".join(ns)
        # If a namespace has already been set then the remainder is the name
        elif namespace:
            name = ns_name
        elif len(ns_name_parts) == 1:
            name = ns_name_parts[0]

eredwine avatar Aug 07 '24 16:08 eredwine