Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

0.17.2 (2025-07-29)
-------------------

- Add support for getting download URL for Golang, Hex, Pub and Swift in ``purl2url``.
https://github.com/package-url/packageurl-python/pull/195

0.17.1 (2025-06-06)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = packageurl-python
version = 0.17.1
version = 0.17.2
license = MIT
description = A purl aka. Package URL parser and builder
long_description = file:README.rst
Expand Down
2 changes: 1 addition & 1 deletion src/packageurl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def unquote(s: AnyStr) -> str:
Return a percent-decoded unicode string, given an `s` byte or unicode
string.
"""
unquoted = _percent_unquote(s) # type:ignore[arg-type] # typeshed is incorrect here
unquoted = _percent_unquote(s)
if not isinstance(unquoted, str):
unquoted = unquoted.decode("utf-8")
return unquoted
Expand Down
91 changes: 91 additions & 0 deletions src/packageurl/contrib/purl2url.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,76 @@ def build_repo_download_url(purl):
return get_repo_download_url(purl)


@download_router.route("pkg:hex/.*")
def build_hex_download_url(purl):
"""
Return a hex download URL from the `purl` string.
"""
purl_data = PackageURL.from_string(purl)

name = purl_data.name
version = purl_data.version

if name and version:
return f"https://repo.hex.pm/tarballs/{name}-{version}.tar"


@download_router.route("pkg:golang/.*")
def build_golang_download_url(purl):
"""
Return a golang download URL from the `purl` string.
"""
purl_data = PackageURL.from_string(purl)

namespace = purl_data.namespace
name = purl_data.name
version = purl_data.version

if not name:
return

# TODO: https://github.com/package-url/packageurl-python/issues/197
if namespace:
name = f"{namespace}/{name}"

ename = escape_golang_path(name)
eversion = escape_golang_path(version)

if name and version:
return f"https://proxy.golang.org/{ename}/@v/{eversion}.zip"


@download_router.route("pkg:pub/.*")
def build_pub_download_url(purl):
"""
Return a pub download URL from the `purl` string.
"""
purl_data = PackageURL.from_string(purl)

name = purl_data.name
version = purl_data.version

if name and version:
return f"https://pub.dev/api/archives/{name}-{version}.tar.gz"


@download_router.route("pkg:swift/.*")
def build_swift_download_url(purl):
"""
Return a Swift Package download URL from the `purl` string.
"""
purl_data = PackageURL.from_string(purl)

name = purl_data.name
version = purl_data.version
namespace = purl_data.namespace

if not (namespace or name or version):
return

return f"https://{namespace}/{name}/archive/{version}.zip"


def get_repo_download_url(purl):
"""
Return ``download_url`` if present in ``purl`` qualifiers or
Expand Down Expand Up @@ -470,3 +540,24 @@ def get_repo_download_url(purl):
return get_repo_download_url_by_package_type(
type=type, namespace=namespace, name=name, version=version
)


# TODO: https://github.com/package-url/packageurl-python/issues/196
def escape_golang_path(path: str) -> str:
"""
Return an case-encoded module path or version name.

This is done by replacing every uppercase letter with an exclamation mark followed by the
corresponding lower-case letter, in order to avoid ambiguity when serving from case-insensitive
file systems.

See https://golang.org/ref/mod#goproxy-protocol.
"""
escaped_path = ""
for c in path:
if c >= "A" and c <= "Z":
# replace uppercase with !lowercase
escaped_path += "!" + chr(ord(c) + ord("a") - ord("A"))
else:
escaped_path += c
return escaped_path
9 changes: 7 additions & 2 deletions tests/contrib/test_purl2url.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ def test_purl2url_get_download_url():
"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",
"pkg:maven/org.apache.commons/commons-math3@3.6.1?classifier=sources": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1-sources.jar",
"pkg:hex/plug@1.11.1": "https://repo.hex.pm/tarballs/plug-1.11.1.tar",
"pkg:golang/xorm.io/xorm@v0.8.2": "https://proxy.golang.org/xorm.io/xorm/@v/v0.8.2.zip",
"pkg:golang/gopkg.in/ldap.v3@v3.1.0": "https://proxy.golang.org/gopkg.in/ldap.v3/@v/v3.1.0.zip",
"pkg:golang/example.com/M.v3@v3.1.0": "https://proxy.golang.org/example.com/!m.v3/@v/v3.1.0.zip",
"pkg:pub/http@0.13.3": "https://pub.dev/api/archives/http-0.13.3.tar.gz",
"pkg:swift/github.com/Alamofire/Alamofire@5.4.3": "https://github.com/Alamofire/Alamofire/archive/5.4.3.zip",
"pkg:swift/github.com/RxSwiftCommunity/RxFlow@2.12.4": "https://github.com/RxSwiftCommunity/RxFlow/archive/2.12.4.zip",
# From `download_url` qualifier
"pkg:github/yarnpkg/yarn@1.3.2?download_url=https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz&version_prefix=v": "https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz",
"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",
Expand All @@ -112,8 +119,6 @@ def test_purl2url_get_download_url():
"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,
}

for purl, url in purls_url.items():
Expand Down
Loading