diff --git a/model/src/main/kotlin/utils/PurlUtils.kt b/model/src/main/kotlin/utils/PurlUtils.kt index 1e0531c1b6fc5..19c70880a2a1b 100644 --- a/model/src/main/kotlin/utils/PurlUtils.kt +++ b/model/src/main/kotlin/utils/PurlUtils.kt @@ -21,6 +21,7 @@ package org.ossreviewtoolkit.model.utils +import org.ossreviewtoolkit.model.HashAlgorithm import org.ossreviewtoolkit.utils.common.percentEncode /** @@ -92,28 +93,44 @@ internal fun createPurl( ): String = buildString { append("pkg:") - append(type) + append(type.lowercase()) + append('/') if (namespace.isNotEmpty()) { + append(namespace.trim('/').split('/').joinToString("/") { it.percentEncode() }) append('/') - append(namespace.percentEncode()) + append(name.trim('/').percentEncode()) + } else { + append(name.percentEncode()) } - append('/') - append(name.percentEncode()) + if (version.isNotEmpty()) { + append('@') - append('@') - append(version.percentEncode()) + // See https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#character-encoding which + // says "the '#', '?', '@' and ':' characters must NOT be encoded when used as separators". + val isChecksum = HashAlgorithm.VERIFIABLE.any { version.startsWith("${it.name.lowercase()}:") } + if (isChecksum) append(version) else append(version.percentEncode()) + } - qualifiers.onEachIndexed { index, entry -> + qualifiers.filterValues { it.isNotEmpty() }.toSortedMap().onEachIndexed { index, entry -> if (index == 0) append("?") else append("&") - append(entry.key.percentEncode()) + + val key = entry.key.lowercase() + append(key) + append("=") - append(entry.value.percentEncode()) + + // See https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs. + if (key in KNOWN_KEYS) append(entry.value) else append(entry.value.percentEncode()) } if (subpath.isNotEmpty()) { - val value = subpath.split('/').joinToString("/", prefix = "#") { it.percentEncode() } + val value = subpath.trim('/').split('/') + .filter { it.isNotEmpty() && it != "." && it != ".." } + .joinToString("/", prefix = "#") { it.percentEncode() } append(value) } } + +private val KNOWN_KEYS = setOf("repository_url", "download_url", "vcs_url", "file_name", "checksum") diff --git a/model/src/test/kotlin/utils/PurlExtensionsTest.kt b/model/src/test/kotlin/utils/PurlExtensionsTest.kt index 01eb198bcabcb..64960e2d61e5a 100644 --- a/model/src/test/kotlin/utils/PurlExtensionsTest.kt +++ b/model/src/test/kotlin/utils/PurlExtensionsTest.kt @@ -68,10 +68,10 @@ class PurlExtensionsTest : WordSpec({ purl shouldBe "pkg:generic/name@version" } - "percent-encode namespaces with segments" { - val purl = Identifier("generic", "name/space", "name", "version").toPurl() + "percent-encode namespace segments" { + val purl = Identifier("generic", "name space/with spaces", "name", "version").toPurl() - purl shouldBe "pkg:generic/name%2Fspace/name@version" + purl shouldBe "pkg:generic/name%20space/with%20spaces/name@version" } "percent-encode the name" { @@ -104,8 +104,8 @@ class PurlExtensionsTest : WordSpec({ val purl = id.toPurl(extras.qualifiers, extras.subpath) purl shouldBe "pkg:maven/com.example/sources@1.2.3?" + - "download_url=https%3A%2F%2Fexample.com%2Fsources.zip&" + - "checksum=md5%3Addce269a1e3d054cae349621c198dd52" + "checksum=md5%3Addce269a1e3d054cae349621c198dd52&" + + "download_url=https%3A%2F%2Fexample.com%2Fsources.zip" purl.toProvenance() shouldBe provenance } @@ -125,10 +125,10 @@ class PurlExtensionsTest : WordSpec({ val purl = id.toPurl(extras.qualifiers, extras.subpath) purl shouldBe "pkg:maven/com.example/sources@1.2.3?" + - "vcs_type=Git&" + - "vcs_url=https%3A%2F%2Fgithub.com%2Fapache%2Fcommons-text.git&" + + "resolved_revision=7643b12421100d29fd2b78053e77bcb04a251b2e&" + "vcs_revision=7643b12421100d29fd2b78053e77bcb04a251b2e&" + - "resolved_revision=7643b12421100d29fd2b78053e77bcb04a251b2e" + + "vcs_type=Git&" + + "vcs_url=https%3A%2F%2Fgithub.com%2Fapache%2Fcommons-text.git" + "#subpath" purl.toProvenance() shouldBe provenance } diff --git a/plugins/package-managers/swiftpm/src/funTest/assets/projects/synthetic/expected-output-only-lockfile-v1.yml b/plugins/package-managers/swiftpm/src/funTest/assets/projects/synthetic/expected-output-only-lockfile-v1.yml index 320581788ab07..9ebd3e148325a 100644 --- a/plugins/package-managers/swiftpm/src/funTest/assets/projects/synthetic/expected-output-only-lockfile-v1.yml +++ b/plugins/package-managers/swiftpm/src/funTest/assets/projects/synthetic/expected-output-only-lockfile-v1.yml @@ -51,7 +51,7 @@ packages: revision: "eb51f949cdd0c9d88abba9ce79d37eb7ea1231d0" path: "" - id: "Swift::github.com/apple/swift-crypto:" - purl: "pkg:swift/github.com%2Fapple%2Fswift-crypto@" + purl: "pkg:swift/github.com%2Fapple%2Fswift-crypto" declared_licenses: [] declared_licenses_processed: {} description: ""