diff --git a/src/packageurl/contrib/purl2url.py b/src/packageurl/contrib/purl2url.py index bbe14e6..2dd0d05 100644 --- a/src/packageurl/contrib/purl2url.py +++ b/src/packageurl/contrib/purl2url.py @@ -225,6 +225,23 @@ def build_pypi_repo_url(purl): return f"https://pypi.org/project/{name}/" +@repo_router.route("pkg:composer/.*") +def build_composer_repo_url(purl): + """ + Return a composer repo URL from the `purl` string. + """ + purl_data = PackageURL.from_string(purl) + + name = purl_data.name + version = purl_data.version + namespace = purl_data.namespace + + if name and version: + return f"https://packagist.org/packages/{namespace}/{name}#{version}" + elif name: + return f"https://packagist.org/packages/{namespace}/{name}" + + @repo_router.route("pkg:nuget/.*") def build_nuget_repo_url(purl): """ diff --git a/src/packageurl/contrib/url2purl.py b/src/packageurl/contrib/url2purl.py index 0693af6..1d30a9d 100644 --- a/src/packageurl/contrib/url2purl.py +++ b/src/packageurl/contrib/url2purl.py @@ -326,6 +326,21 @@ def build_pypi_purl(uri): return purl_from_pattern("pypi", pypi_pattern, last_segment) +# https://packagist.org/packages/webmozart/assert#1.9.1 +@purl_router.route("https?://packagist.org/packages/.*") +def build_composer_purl(uri): + # We use a more general route pattern instead of using `composer_pattern` + # below by itself because we want to capture all packagist download URLs, + # even the ones that are not completely formed. This helps prevent url2purl + # from attempting to create a generic PackageURL from an invalid packagist + # download URL. + + # https://packagist.org/packages/ralouphie/getallheaders + # https://packagist.org/packages/symfony/process#v7.0.0-BETA3 + composer_pattern = r"^https?://packagist\.org/packages/(?P[^/]+)/(?P[^\#]+?)(\#(?P.+))?$" + return purl_from_pattern("composer", composer_pattern, uri) + + # http://nuget.org/packages/EntityFramework/4.2.0.0 # https://www.nuget.org/api/v2/package/Newtonsoft.Json/11.0.1 nuget_www_pattern = r"^https?://.*nuget.org/(api/v2/)?packages?/(?P.+)/(?P.+)$" diff --git a/tests/contrib/data/url2purl.json b/tests/contrib/data/url2purl.json index 3e0cf41..d6bbdc6 100644 --- a/tests/contrib/data/url2purl.json +++ b/tests/contrib/data/url2purl.json @@ -121,6 +121,9 @@ "https://rubygems.org/downloads/unf-0.1.3.gem": "pkg:gem/unf@0.1.3", "https://rubygems.org/downloads/yajl-ruby-1.2.0.gem": "pkg:gem/yajl-ruby@1.2.0", "https://rubygems.org/gems/i18n-js-3.0.11.gem": "pkg:gem/i18n-js@3.0.11", + "https://packagist.org/packages/webmozart/assert":"pkg:composer/webmozart/assert", + "https://packagist.org/packages/guzzlehttp/psr7#2.6.1":"pkg:composer/guzzlehttp/psr7@2.6.1", + "https://packagist.org/packages/symfony/process#v7.0.0-BETA3":"pkg:composer/symfony/process@v7.0.0-BETA3", "https://pypi.org/packages/source/z/zc.recipe.egg/zc.recipe.egg-2.0.0.tar.gz": "pkg:pypi/zc.recipe.egg@2.0.0", "https://pypi.org/project/widgetsnbextension": "pkg:pypi/widgetsnbextension", "https://pypi.org/project/widgetsnbextension/3.0.7/": "pkg:pypi/widgetsnbextension@3.0.7", diff --git a/tests/contrib/test_purl2url.py b/tests/contrib/test_purl2url.py index 94b21f0..c76fa52 100644 --- a/tests/contrib/test_purl2url.py +++ b/tests/contrib/test_purl2url.py @@ -53,6 +53,8 @@ def test_purl2url_get_repo_url(): "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/", + "pkg:composer/psr/log": "https://packagist.org/packages/psr/log", + "pkg:composer/psr/log@1.1.3": "https://packagist.org/packages/psr/log#1.1.3", "pkg:npm/is-npm": "https://www.npmjs.com/package/is-npm", "pkg:npm/is-npm@1.0.0": "https://www.npmjs.com/package/is-npm/v/1.0.0", "pkg:nuget/System.Text.Json": "https://www.nuget.org/packages/System.Text.Json", @@ -95,6 +97,7 @@ def test_purl2url_get_download_url(): "pkg:rubygems/package-name": None, "pkg:bitbucket/birkenfeld": None, "pkg:pypi/sortedcontainers@2.4.0": None, + "pkg:composer/psr/log@1.1.3": None, "pkg:golang/xorm.io/xorm@v0.8.2": None, "pkg:golang/gopkg.in/ldap.v3@v3.1.0": None, } @@ -132,6 +135,7 @@ def test_purl2url_get_inferred_urls(): "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: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:bitbucket/birkenfeld": [], } diff --git a/tests/data/test-suite-data.json b/tests/data/test-suite-data.json index aee685c..571c568 100644 --- a/tests/data/test-suite-data.json +++ b/tests/data/test-suite-data.json @@ -191,6 +191,18 @@ "subpath": null, "is_invalid": false }, + { + "description": "valid packagist purl", + "purl": "pkg:composer/guzzlehttp/promises@2.0.2", + "canonical_purl": "pkg:composer/guzzlehttp/promises@2.0.2", + "type": "composer", + "namespace": "guzzlehttp", + "name": "promises", + "version": "2.0.2", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, { "description": "rpm often use qualifiers", "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25",