diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f73e73..630565c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,14 @@ Changelog 0.18.0 (unlreleased) -------------------- +- Add ``PURL_TYPES`` constant sourced from the ``purl-types-index.json`` registry. + Enforce validation of PackageURL types. + Remove ``sourceforge`` and ``gitlab`` support from ``url2purl`` and ``purl2url``. + Remove ``rubygems`` route alias from ``purl2url`` (the registered type is ``gem``). + https://github.com/package-url/packageurl-python/issues/181 + https://github.com/package-url/packageurl-python/issues/155 + https://github.com/package-url/packageurl-python/pull/188 + - Add support for Python 3.14 - Drop support for Python 3.8 diff --git a/src/packageurl/__init__.py b/src/packageurl/__init__.py index 3bfae87..f9d3cb3 100644 --- a/src/packageurl/__init__.py +++ b/src/packageurl/__init__.py @@ -61,6 +61,49 @@ """ +PURL_TYPES: set[str] = { + "alpm", + "apk", + "bitbucket", + "bitnami", + "cargo", + "cocoapods", + "composer", + "conan", + "conda", + "cpan", + "cran", + "deb", + "docker", + "gem", + "generic", + "github", + "golang", + "hackage", + "hex", + "huggingface", + "luarocks", + "maven", + "mlflow", + "npm", + "nuget", + "oci", + "pub", + "pypi", + "qpkg", + "rpm", + "swid", + "swift", +} +"""Registered pURL types. + +Sourced from the `purl-types-index.json +`_ +registry, which is automatically regenerated whenever a new pURL type is +registered. +""" + + class ValidationSeverity(str, Enum): ERROR = "error" WARNING = "warning" @@ -124,8 +167,15 @@ def normalize_type(type: AnyStr | None, encode: bool | None = True) -> str | Non type_str = type if isinstance(type, str) else type.decode("utf-8") quoter = get_quoter(encode) - type_str = quoter(type_str) - return type_str.strip().lower() or None + type_str = quoter(type_str).strip().lower() + if not type_str: + return None + if type_str not in PURL_TYPES: + raise ValueError( + f"Invalid purl type: {type_str!r}. " + f"Must be one of: {', '.join(sorted(PURL_TYPES))}." + ) + return type_str def normalize_namespace( @@ -140,7 +190,6 @@ def normalize_namespace( "bitbucket", "github", "pypi", - "gitlab", "composer", "luarocks", "qpkg", @@ -195,7 +244,6 @@ def normalize_name( "bitbucket", "github", "pypi", - "gitlab", "composer", "luarocks", "oci", diff --git a/src/packageurl/contrib/purl2url.py b/src/packageurl/contrib/purl2url.py index 5806251..55c3f5e 100644 --- a/src/packageurl/contrib/purl2url.py +++ b/src/packageurl/contrib/purl2url.py @@ -44,7 +44,6 @@ def get_repo_download_url_by_package_type( download_url_by_type = { "github": f"https://github.com/{namespace}/{name}/archive/{version}.{archive_extension}", "bitbucket": f"https://bitbucket.org/{namespace}/{name}/get/{version}.{archive_extension}", - "gitlab": f"https://gitlab.com/{namespace}/{name}/-/archive/{version}/{name}-{version}.{archive_extension}", } return download_url_by_type.get(type) @@ -158,21 +157,7 @@ def build_github_repo_url(purl): return repo_url -@repo_router.route("pkg:gitlab/.*") -def build_gitlab_repo_url(purl): - """ - Return a gitlab repo URL from the `purl` string. - """ - purl_data = PackageURL.from_string(purl) - - namespace = purl_data.namespace - name = purl_data.name - - if name and namespace: - return f"https://gitlab.com/{namespace}/{name}" - - -@repo_router.route("pkg:(gem|rubygems)/.*") +@repo_router.route("pkg:gem/.*") def build_rubygems_repo_url(purl): """ Return a rubygems repo URL from the `purl` string. @@ -351,7 +336,7 @@ def build_cargo_download_url(purl): return f"https://crates.io/api/v1/crates/{name}/{version}/download" -@download_router.route("pkg:(gem|rubygems)/.*") +@download_router.route("pkg:gem/.*") def build_rubygems_download_url(purl): """ Return a rubygems download URL from the `purl` string. @@ -435,7 +420,7 @@ def build_nuget_download_url(purl): return f"https://www.nuget.org/api/v2/package/{name}/{version}" -@download_router.route("pkg:gitlab/.*", "pkg:bitbucket/.*", "pkg:github/.*") +@download_router.route("pkg:bitbucket/.*", "pkg:github/.*") def build_repo_download_url(purl): """ Return a gitlab download URL from the `purl` string. diff --git a/src/packageurl/contrib/url2purl.py b/src/packageurl/contrib/url2purl.py index 2353b0b..0656411 100644 --- a/src/packageurl/contrib/url2purl.py +++ b/src/packageurl/contrib/url2purl.py @@ -372,75 +372,6 @@ def build_composer_purl(uri): register_pattern("nuget", nuget_api_pattern) -# https://sourceforge.net/projects/turbovnc/files/3.1/turbovnc-3.1.tar.gz/download -# https://sourceforge.net/projects/scribus/files/scribus/1.6.0/scribus-1.6.0.tar.gz/download -# https://sourceforge.net/projects/ventoy/files/v1.0.96/Ventoy%201.0.96%20release%20source%20code.tar.gz/download -# https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.4/geoserver-2.23.4-war.zip/download -sourceforge_download_pattern = ( - r"^https?://.*sourceforge.net/projects/" - r"(?P.+)/" - r"files/" - r"(?i:(?P=name)/)?" # optional case-insensitive name segment repeated - r"v?(?P[0-9\.]+)/" # version restricted to digits and dots - r"(?i:(?P=name)).*(?P=version).*" # case-insensitive matching for {name}-{version} - r"(/download)$" # ending with "/download" -) - -register_pattern("sourceforge", sourceforge_download_pattern) - - -# https://sourceforge.net/projects/spacesniffer/files/spacesniffer_1_3_0_2.zip/download -sourceforge_download_pattern_bis = ( - r"^https?://.*sourceforge.net/projects/" - r"(?P.+)/" - r"files/" - r"(?i:(?P=name))_*(?P[0-9_]+).*" - r"(/download)$" # ending with "/download" -) - -register_pattern("sourceforge", sourceforge_download_pattern_bis) - - -@purl_router.route("https?://.*sourceforge.net/project/.*") -def build_sourceforge_purl(uri): - # We use a more general route pattern instead of using `sourceforge_pattern` - # below by itself because we want to capture all sourceforge download URLs, - # even the ones that do not fit `sourceforge_pattern`. This helps prevent - # url2purl from attempting to create a generic PackageURL from a sourceforge - # URL that we can't handle. - - # http://master.dl.sourceforge.net/project/libpng/zlib/1.2.3/zlib-1.2.3.tar.bz2 - sourceforge_pattern = ( - r"^https?://.*sourceforge.net/projects?/" - r"(?P([^/]+))/" # do not allow more "/" segments - r"(OldFiles/)?" - r"(?P.+)/" - r"(?P[v0-9\.]+)/" # version restricted to digits and dots - r"(?P=name).*(?P=version).*" # {name}-{version} repeated in the filename - r"[^/]$" # not ending with "/" - ) - - sourceforge_purl = purl_from_pattern("sourceforge", sourceforge_pattern, uri) - - if not sourceforge_purl: - # Get the project name from `uri` and use that as the Package name - # http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip - split_uri = uri.split("/project/") - - # http://master.dl.sourceforge.net, aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip - if len(split_uri) >= 2: - # aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip - remaining_uri_path = split_uri[1] - # aloyscore, aloyscore, 0.1a1%2520stable, 0.1a1_stable_AloysCore.zip - remaining_uri_path_segments = remaining_uri_path.split("/") - if remaining_uri_path_segments: - project_name = remaining_uri_path_segments[0] # aloyscore - sourceforge_purl = PackageURL( - type="sourceforge", name=project_name, qualifiers={"download_url": uri} - ) - return sourceforge_purl - - # https://crates.io/api/v1/crates/rand/0.7.2/download cargo_pattern = r"^https?://crates.io/api/v1/crates/(?P.+)/(?P.+)(\/download)$" @@ -667,74 +598,6 @@ def build_bitbucket_purl(url): ) -@purl_router.route("https?://gitlab\\.com/(?!.*/archive/).*") -def build_gitlab_purl(url): - """ - Return a PackageURL object from Gitlab `url`. - For example: - https://gitlab.com/TG1999/firebase/-/tree/1a122122/views - https://gitlab.com/TG1999/firebase/-/tree - https://gitlab.com/TG1999/firebase/-/master - https://gitlab.com/tg1999/Firebase/-/tree/master - https://gitlab.com/tg1999/Firebase/-/commit/bf04e5f289885cf2f20a92b387bcc6df33e30809 - """ - # https://gitlab.com///-/commit/ - commit_pattern = ( - r"https?://gitlab.com/" - r"(?P[^/]+)/(?P[^/]+)/-/commit/" - r"(?P[0-9a-fA-F]{7,64})/?$" - ) - - commit_matche = re.search(commit_pattern, url) - if commit_matche: - return PackageURL( - type="gitlab", - namespace=commit_matche.group("namespace"), - name=commit_matche.group("name"), - version=commit_matche.group("version"), - qualifiers={}, - subpath="", - ) - - segments = get_path_segments(url) - - if not len(segments) >= 2: - return - namespace = segments[0] - name = segments[1] - version = None - subpath = None - - # https://gitlab.com/TG1999/firebase/master - if (len(segments) >= 3) and segments[2] != "-" and segments[2] != "tree": - version = segments[2] - subpath = "/".join(segments[3:]) - - # https://gitlab.com/TG1999/firebase/-/tree/master - if len(segments) >= 5 and (segments[2] == "-" and segments[3] == "tree"): - version = segments[4] - subpath = "/".join(segments[5:]) - - return PackageURL( - type="gitlab", - namespace=namespace, - name=name, - version=version, - subpath=subpath, - ) - - -# https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz -gitlab_archive_pattern = ( - r"^https?://gitlab.com/" - r"(?P.+)/(?P.+)/-/archive/(?P.+)/" - r"(?P=name)-(?P=version).*" - r"[^/]$" -) - -register_pattern("gitlab", gitlab_archive_pattern) - - # https://hackage.haskell.org/package/cli-extras-0.2.0.0/cli-extras-0.2.0.0.tar.gz hackage_download_pattern = ( r"^https?://hackage.haskell.org/package/" diff --git a/tests/contrib/data/url2purl.json b/tests/contrib/data/url2purl.json index 3a99dd2..3d27071 100644 --- a/tests/contrib/data/url2purl.json +++ b/tests/contrib/data/url2purl.json @@ -155,31 +155,6 @@ "https://www.nuget.org/api/v2/package/MvvmLightLibs/4.1.23": "pkg:nuget/MvvmLightLibs@4.1.23", "https://www.nuget.org/api/v2/package/Twilio/3.4.1": "pkg:nuget/Twilio@3.4.1", "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/10.0.1/newtonsoft.json.10.0.1.nupkg": "pkg:nuget/newtonsoft.json@10.0.1", - "http://master.dl.sourceforge.net/project/zznotes/zznotes/1.1.2/zznotes-1.1.2.tar.gz": "pkg:sourceforge/zznotes/zznotes@1.1.2", - "http://master.dl.sourceforge.net/project/zapping/zvbi/0.2.35/zvbi-0.2.35.tar.bz2": "pkg:sourceforge/zapping/zvbi@0.2.35", - "http://master.dl.sourceforge.net/project/libpng/zlib/1.2.3/zlib-1.2.3.tar.bz2": "pkg:sourceforge/libpng/zlib@1.2.3", - "http://master.dl.sourceforge.net/project/xmlstar/xmlstarlet/1.0.0/xmlstarlet-1.0.0-1.src.rpm": "pkg:sourceforge/xmlstar/xmlstarlet@1.0.0", - "http://master.dl.sourceforge.net/project/wxmozilla/wxMozilla/0.5.5/wxMozilla-0.5.5.exe": "pkg:sourceforge/wxmozilla/wxMozilla@0.5.5", - "http://iweb.dl.sourceforge.net/project/sblim/sblim-cim-client2/2.2.5/sblim-cim-client2-2.2.5-src.zip": "pkg:sourceforge/sblim/sblim-cim-client2@2.2.5", - "http://master.dl.sourceforge.net/project/zinnia/zinnia-win32/0.06/zinnia-win32-0.06.zip": "pkg:sourceforge/zinnia/zinnia-win32@0.06", - "http://iweb.dl.sourceforge.net/project/findbugs/findbugs/1.3.4/findbugs-1.3.4.tar.gz/": "pkg:sourceforge/findbugs?download_url=http://iweb.dl.sourceforge.net/project/findbugs/findbugs/1.3.4/findbugs-1.3.4.tar.gz/", - "http://master.dl.sourceforge.net/project/arestc/net/sf/arestc/arestc/0.1.4/arestc-0.1.4-javadoc.jar": "pkg:sourceforge/arestc?download_url=http://master.dl.sourceforge.net/project/arestc/net/sf/arestc/arestc/0.1.4/arestc-0.1.4-javadoc.jar", - "http://master.dl.sourceforge.net/project/intraperson/OldFiles/intraperson/0.28/intraperson-0.28.tar.gz": "pkg:sourceforge/intraperson/intraperson@0.28", - "http://master.dl.sourceforge.net/project/pwiki/pwiki/0.1.2/0.1.2.zip": "pkg:sourceforge/pwiki?download_url=http://master.dl.sourceforge.net/project/pwiki/pwiki/0.1.2/0.1.2.zip", - "http://master.dl.sourceforge.net/project/iswraid/iswraid/0.1.4.3/2.4.28-pre3-iswraid.patch.gz": "pkg:sourceforge/iswraid?download_url=http://master.dl.sourceforge.net/project/iswraid/iswraid/0.1.4.3/2.4.28-pre3-iswraid.patch.gz", - "http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%20stable/0.1a1_stable_AloysCore.zip": "pkg:sourceforge/aloyscore?download_url=http://master.dl.sourceforge.net/project/aloyscore/aloyscore/0.1a1%2520stable/0.1a1_stable_AloysCore.zip", - "http://master.dl.sourceforge.net/project/myenterprise/OldFiles/1.0.0.2.MyEnterprise.Source.zip": "pkg:sourceforge/myenterprise?download_url=http://master.dl.sourceforge.net/project/myenterprise/OldFiles/1.0.0.2.MyEnterprise.Source.zip", - "http://master.dl.sourceforge.net/project/wxhaskell/wxhaskell/wxhaskell-0.9/wxhaskell-src-0.9.zip": "pkg:sourceforge/wxhaskell?download_url=http://master.dl.sourceforge.net/project/wxhaskell/wxhaskell/wxhaskell-0.9/wxhaskell-src-0.9.zip", - "http://master.dl.sourceforge.net/project/a2freedom/A2/1.2/a2freedom-1.2.zip": "pkg:sourceforge/a2freedom?download_url=http://master.dl.sourceforge.net/project/a2freedom/A2/1.2/a2freedom-1.2.zip", - "http://master.dl.sourceforge.net/project/tinyos/OldFiles/tinyos/1.1.0/tinyos-1.1.0.tar.gz": "pkg:sourceforge/tinyos/tinyos@1.1.0", - "http://master.dl.sourceforge.net/project/urlchecker/lu/ng/urlchecker/urlchecker/1.7/urlchecker-1.7-javadoc.jar": "pkg:sourceforge/urlchecker?download_url=http://master.dl.sourceforge.net/project/urlchecker/lu/ng/urlchecker/urlchecker/1.7/urlchecker-1.7-javadoc.jar", - "http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar": "pkg:sourceforge/zclasspath?download_url=http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar", - "http://master.dl.sourceforge.net/project/googleimagedown/project/v1.1/GoogleImageDownloader-v1.1-src.tar.bz2": "pkg:sourceforge/googleimagedown?download_url=http://master.dl.sourceforge.net/project/googleimagedown/project/v1.1/GoogleImageDownloader-v1.1-src.tar.bz2", - "https://sourceforge.net/projects/scribus/files/scribus/1.6.0/scribus-1.6.0.tar.gz/download": "pkg:sourceforge/scribus@1.6.0", - "https://sourceforge.net/projects/turbovnc/files/3.1/turbovnc-3.1.tar.gz/download": "pkg:sourceforge/turbovnc@3.1", - "https://sourceforge.net/projects/ventoy/files/v1.0.96/Ventoy%201.0.96%20release%20source%20code.tar.gz/download": "pkg:sourceforge/ventoy@1.0.96", - "https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.4/geoserver-2.23.4-war.zip/download": "pkg:sourceforge/geoserver@2.23.4", - "https://sourceforge.net/projects/spacesniffer/files/spacesniffer_1_3_0_2.zip/download": "pkg:sourceforge/spacesniffer@1_3_0_2", "https://crates.io/api/v1/crates/rand/0.7.2/download": "pkg:cargo/rand@0.7.2", "https://crates.io/api/v1/crates/clap/2.33.0/download": "pkg:cargo/clap@2.33.0", "https://crates.io/api/v1/crates/structopt/0.3.11/download": "pkg:cargo/structopt@0.3.11", @@ -253,17 +228,6 @@ "https://bitbucket.org/multicoreware/x265/downloads/x265_2.6.tar.gz": "pkg:bitbucket/multicoreware/x265?download_url=https://bitbucket.org/multicoreware/x265/downloads/x265_2.6.tar.gz", "https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip": "pkg:bitbucket/robeden/trove?download_url=https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip", "https://bitbucket.org/efotinis/deskpins/downloads/DeskPins-1.31-setup.exe": "pkg:bitbucket/efotinis/deskpins?download_url=https://bitbucket.org/efotinis/deskpins/downloads/DeskPins-1.31-setup.exe", - "https://gitlab.com/TG1999/firebase/-/tree/1a122122/views": "pkg:gitlab/tg1999/firebase@1a122122#views", - "https://gitlab.com/tg1999/firebase": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/TG1999/firebase/-/": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/TG1999/firebase/-/tree": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/TG1999/firebase/-/master": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/TG1999/firebase/tree/": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/TG1999/firebase/master": "pkg:gitlab/tg1999/firebase@master", - "https://gitlab.com/TG1999/firebase/-/tree/master": "pkg:gitlab/tg1999/firebase@master", - "https://gitlab.com/tg1999/Firebase/-/tree/master": "pkg:gitlab/tg1999/firebase@master", - "https://gitlab.com/TG1999/FIREBASE": "pkg:gitlab/tg1999/firebase", - "https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz": "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2", "https://hackage.haskell.org/package/a50-0.5/a50-0.5.tar.gz": "pkg:hackage/a50@0.5", "https://hackage.haskell.org/package/AC-HalfInteger-1.2.1/AC-HalfInteger-1.2.1.tar.gz": "pkg:hackage/AC-HalfInteger@1.2.1", "https://hackage.haskell.org/package/3d-graphics-examples-0.0.0.2/3d-graphics-examples-0.0.0.2.tar.gz": "pkg:hackage/3d-graphics-examples@0.0.0.2", @@ -276,6 +240,5 @@ "https://cran.r-project.org/src/contrib/jsonlite_1.8.8.tar.gz": "pkg:cran/jsonlite@1.8.8", "https://packagemanager.rstudio.com/cran/2022-06-23/src/contrib/curl_4.3.2.tar.gz": "pkg:cran/curl@4.3.2?download_url=https://packagemanager.rstudio.com/cran/2022-06-23/src/contrib/curl_4.3.2.tar.gz", "https://github.com/TG1999/first_repo/commit/98e516011d6e096e25247b82fc5f196bbeecff10": "pkg:github/tg1999/first_repo@98e516011d6e096e25247b82fc5f196bbeecff10", - "https://gitlab.com/TG1999/first_repo/-/commit/bf04e5f289885cf2f20a92b387bcc6df33e30809": "pkg:gitlab/tg1999/first_repo@bf04e5f289885cf2f20a92b387bcc6df33e30809", "https://bitbucket.org/TG1999/first_repo/commits/16a60c4a74ef477cd8c16ca82442eaab2fbe8c86": "pkg:bitbucket/tg1999/first_repo@16a60c4a74ef477cd8c16ca82442eaab2fbe8c86" } diff --git a/tests/contrib/test_purl2url.py b/tests/contrib/test_purl2url.py index b34348c..2faa82c 100644 --- a/tests/contrib/test_purl2url.py +++ b/tests/contrib/test_purl2url.py @@ -39,17 +39,11 @@ def test_purl2url_get_repo_url(): "pkg:cargo/rand@0.7.2": "https://crates.io/crates/rand/0.7.2", "pkg:cargo/abc": "https://crates.io/crates/abc", "pkg:gem/bundler@2.3.23": "https://rubygems.org/gems/bundler/versions/2.3.23", - "pkg:rubygems/bundler@2.3.23": "https://rubygems.org/gems/bundler/versions/2.3.23", - "pkg:rubygems/package-name": "https://rubygems.org/gems/package-name", + "pkg:gem/package-name": "https://rubygems.org/gems/package-name", "pkg:bitbucket/birkenfeld/pygments-main": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld/pygments-main@master#views": "https://bitbucket.org/birkenfeld/pygments-main", "pkg:bitbucket/birkenfeld": None, - "pkg:gitlab/tg1999/firebase@master": "https://gitlab.com/tg1999/firebase", - "pkg:gitlab/tg1999/firebase@1a122122#views": "https://gitlab.com/tg1999/firebase", - "pkg:gitlab/tg1999/firebase": "https://gitlab.com/tg1999/firebase", - "pkg:gitlab/tg1999": None, - "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2": "https://gitlab.com/hoppr/hoppr", "pkg:pypi/sortedcontainers": "https://pypi.org/project/sortedcontainers/", "pkg:pypi/sortedcontainers@2.4.0": "https://pypi.org/project/sortedcontainers/2.4.0/", "pkg:pypi/packageurl_python": "https://pypi.org/project/packageurl-python/", @@ -91,9 +85,6 @@ def test_purl2url_get_download_url(): "pkg:github/StonyShi/reactor-netty-jersey@ac525d91ff1724395640531df08e3e4eabef207d": "https://github.com/stonyshi/reactor-netty-jersey/archive/ac525d91ff1724395640531df08e3e4eabef207d.tar.gz", "pkg:bitbucket/robeden/trove@3.0.3": "https://bitbucket.org/robeden/trove/get/3.0.3.tar.gz", "pkg:bitbucket/robeden/trove@3.0.3?version_prefix=v": "https://bitbucket.org/robeden/trove/get/v3.0.3.tar.gz", - "pkg:gitlab/tg1999/firebase@1a122122": "https://gitlab.com/tg1999/firebase/-/archive/1a122122/firebase-1a122122.tar.gz", - "pkg:gitlab/tg1999/firebase@1a122122?version_prefix=v": "https://gitlab.com/tg1999/firebase/-/archive/v1a122122/firebase-v1a122122.tar.gz", - "pkg:gitlab/hoppr/hoppr@v1.11.1-dev.2": "https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz", "pkg:maven/org.apache.commons/commons-io@1.3.2": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar", "pkg:maven/org.apache.commons/commons-io@1.3.2?repository_url=https://repo1.maven.org/maven2": "https://repo1.maven.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar", "pkg:maven/org.apache.commons/commons-io@1.3.2?type=pom": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.pom", @@ -118,12 +109,10 @@ def test_purl2url_get_download_url(): "pkg:generic/lxc-master.tar.gz?download_url=https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz": "https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz", "pkg:generic/code.google.com/android-notifier?download_url=https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm": "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/android-notifier/android-notifier-desktop-0.5.1-1.i386.rpm", "pkg:bitbucket/robeden/trove?download_url=https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip": "https://bitbucket.org/robeden/trove/downloads/trove-3.0.3.zip", - "pkg:sourceforge/zclasspath?download_url=http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar": "http://master.dl.sourceforge.net/project/zclasspath/maven2/org/zclasspath/zclasspath/1.5/zclasspath-1.5.jar", "pkg:pypi/aboutcode-toolkit@3.4.0rc1?download_url=https://files.pythonhosted.org/packages/87/44/0fa8e9d0cccb8eb86fc1b5170208229dc6d6e9fd6e57ea1fe19cbeea68f5/aboutcode_toolkit-3.4.0rc1-py2.py3-none-any.whl": "https://files.pythonhosted.org/packages/87/44/0fa8e9d0cccb8eb86fc1b5170208229dc6d6e9fd6e57ea1fe19cbeea68f5/aboutcode_toolkit-3.4.0rc1-py2.py3-none-any.whl", # Not-supported "pkg:github/tg1999/fetchcode": None, "pkg:cargo/abc": None, - "pkg:rubygems/package-name": None, "pkg:bitbucket/birkenfeld": None, "pkg:pypi/sortedcontainers@2.4.0": None, "pkg:composer/psr/log@1.1.3": None, @@ -161,14 +150,10 @@ def test_purl2url_get_inferred_urls(): ], "pkg:cargo/abc": ["https://crates.io/crates/abc"], "pkg:github/tg1999/fetchcode": ["https://github.com/tg1999/fetchcode"], - "pkg:gitlab/tg1999/firebase@1a122122": [ - "https://gitlab.com/tg1999/firebase", - "https://gitlab.com/tg1999/firebase/-/archive/1a122122/firebase-1a122122.tar.gz", - ], "pkg:pypi/sortedcontainers@2.4.0": ["https://pypi.org/project/sortedcontainers/2.4.0/"], "pkg:cocoapods/AFNetworking@4.0.1": ["https://cocoapods.org/pods/AFNetworking"], "pkg:composer/psr/log@1.1.3": ["https://packagist.org/packages/psr/log#1.1.3"], - "pkg:rubygems/package-name": ["https://rubygems.org/gems/package-name"], + "pkg:gem/package-name": ["https://rubygems.org/gems/package-name"], "pkg:maven/org.apache.commons/commons-io@1.3.2": [ "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2", "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar", @@ -186,7 +171,6 @@ def test_purl2url_get_repo_url_with_invalid_purls(): "pkg:cargo", "pkg:gem", "pkg:bitbucket", - "pkg:gitlab", None, ] diff --git a/tests/contrib/test_utils.py b/tests/contrib/test_utils.py index 9e3b04a..32ec6b4 100644 --- a/tests/contrib/test_utils.py +++ b/tests/contrib/test_utils.py @@ -32,10 +32,11 @@ def test_purl_to_lookups_without_encode(): assert purl_to_lookups( - purl_str="pkg:alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", + purl_str="pkg:apk/alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", encode=False, ) == { - "type": "alpine", + "type": "apk", + "namespace": "alpine", "name": "openssl", "version": "0", "qualifiers": { @@ -48,10 +49,11 @@ def test_purl_to_lookups_without_encode(): def test_purl_to_lookups_with_encode(): assert purl_to_lookups( - purl_str="pkg:alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", + purl_str="pkg:apk/alpine/openssl@0?arch=aarch64&distroversion=edge&reponame=main", encode=True, ) == { - "type": "alpine", + "type": "apk", + "namespace": "alpine", "name": "openssl", "version": "0", "qualifiers": "arch=aarch64&distroversion=edge&reponame=main", @@ -59,14 +61,15 @@ def test_purl_to_lookups_with_encode(): def test_purl_to_lookups_include_empty_fields(): - purl_str = "pkg:alpine/openssl" + purl_str = "pkg:apk/alpine/openssl" assert purl_to_lookups(purl_str) == { - "type": "alpine", + "type": "apk", + "namespace": "alpine", "name": "openssl", } assert purl_to_lookups(purl_str, include_empty_fields=True) == { - "type": "alpine", - "namespace": "", + "type": "apk", + "namespace": "alpine", "name": "openssl", "version": "", "qualifiers": "", diff --git a/tests/test_packageurl.py b/tests/test_packageurl.py index 2c36549..887f568 100644 --- a/tests/test_packageurl.py +++ b/tests/test_packageurl.py @@ -182,7 +182,7 @@ def test_create_PackageURL_from_qualifiers_dict(self): def test_normalize_encode_can_take_unicode_with_non_ascii_with_slash(self): uncd = "núcleo/núcleo" normal = normalize( - type=uncd, + type="generic", namespace=uncd, name=uncd, version=uncd, @@ -191,7 +191,7 @@ def test_normalize_encode_can_take_unicode_with_non_ascii_with_slash(self): encode=True, ) expected = ( - "n%c3%bacleo/n%c3%bacleo", + "generic", "n%C3%BAcleo/n%C3%BAcleo", "n%C3%BAcleo/n%C3%BAcleo", "n%C3%BAcleo/n%C3%BAcleo", @@ -203,7 +203,7 @@ def test_normalize_encode_can_take_unicode_with_non_ascii_with_slash(self): def test_normalize_decode_can_take_unicode_with_non_ascii_with_slash(self): uncd = "núcleo/núcleo" normal = normalize( - type=uncd, + type="generic", namespace=uncd, name=uncd, version=uncd, @@ -212,7 +212,7 @@ def test_normalize_decode_can_take_unicode_with_non_ascii_with_slash(self): encode=False, ) expected = ( - "núcleo/núcleo", + "generic", "núcleo/núcleo", "núcleo/núcleo", "núcleo/núcleo", @@ -224,7 +224,7 @@ def test_normalize_decode_can_take_unicode_with_non_ascii_with_slash(self): def test_normalize_encode_always_reencodes(self): uncd = "n%c3%bacleo/n%c3%bacleo" normal = normalize( - type=uncd, + type="generic", namespace=uncd, name=uncd, version=uncd, @@ -233,7 +233,7 @@ def test_normalize_encode_always_reencodes(self): encode=True, ) expected = ( - "n%25c3%25bacleo/n%25c3%25bacleo", + "generic", "n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", "n%25c3%25bacleo/n%25c3%25bacleo", diff --git a/tests/test_purl_spec.py b/tests/test_purl_spec.py index a78d23d..3d66f3e 100644 --- a/tests/test_purl_spec.py +++ b/tests/test_purl_spec.py @@ -32,7 +32,7 @@ import pytest -from packageurl import PackageURL +from packageurl import PURL_TYPES, PackageURL @dataclass @@ -90,6 +90,10 @@ def load_spec_files(spec_dir: str) -> Dict[str, List[PurlTestCase]]: flattened_cases = [] for filename, cases in spec_dict.items(): + # Only run spec tests for registered pURL types. + type_name = filename.removesuffix("-test.json") + if type_name not in PURL_TYPES: + continue for case in cases: flattened_cases.append((filename, case.description, case)) @@ -185,3 +189,31 @@ def run_test_case(case: PurlTestCase): assert messages == case.expected_output else: assert not messages + + +PURL_TYPES_INDEX = os.path.join(root_dir, "spec", "purl-types-index.json") + + +def test_purl_types_synced_with_spec(): + """Fail if PURL_TYPES drifts from the purl-spec registry. + + This test reads purl-types-index.json from the purl-spec submodule and + compares it with the PURL_TYPES constant. When the submodule is bumped + and new types appear (or old ones are removed), this test will fail with + a message showing exactly what changed. + """ + with open(PURL_TYPES_INDEX, encoding="utf-8") as f: + spec_types = set(json.load(f)) + + added = sorted(spec_types - PURL_TYPES) + removed = sorted(PURL_TYPES - spec_types) + + diff_lines = [] + if added: + diff_lines.append(f"Types in spec but missing from PURL_TYPES: {added}") + if removed: + diff_lines.append(f"Types in PURL_TYPES but removed from spec: {removed}") + + assert not diff_lines, ( + "PURL_TYPES is out of sync with purl-types-index.json.\n" + "\n".join(diff_lines) + )