diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index a474651f..818da80e 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -25,8 +25,10 @@ import com.github.packageurl.internal.StringUtil; import java.io.Serializable; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -82,7 +84,7 @@ public final class PackageURL implements Serializable { * The name of the package. * Required. */ - private final String name; + private String name; /** * The version of the package. @@ -190,7 +192,7 @@ public PackageURL(final String purl) throws MalformedPackageURLException { remainder = remainder.substring(0, index); this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false)); } - verifyTypeConstraints(this.type, this.namespace, this.name); + verifyTypeConstraints(this.type, this.namespace, this.name, this.version, this.qualifiers); } catch (URISyntaxException e) { throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e); } @@ -235,7 +237,7 @@ public PackageURL( this.version = validateVersion(this.type, version); this.qualifiers = parseQualifiers(qualifiers); this.subpath = validateSubpath(subpath); - verifyTypeConstraints(this.type, this.namespace, this.name); + verifyTypeConstraints(this.type, this.namespace, this.name, this.version, this.qualifiers); } /** @@ -477,24 +479,31 @@ private static void validateValue(final String key, final @Nullable String value return validatePath(value.split("/"), true); } - private static @Nullable String validatePath(final String[] segments, final boolean isSubPath) + private static boolean shouldKeepSegment(final String segment, final boolean isSubpath) { + return (!isSubpath || (!segment.isEmpty() && !".".equals(segment) && !"..".equals(segment))); + } + + private static @Nullable String validatePath(final String[] segments, final boolean isSubpath) throws MalformedPackageURLException { if (segments.length == 0) { return null; } + try { return Arrays.stream(segments) - .peek(segment -> { - if (isSubPath && ("..".equals(segment) || ".".equals(segment))) { + .map(segment -> { + if (!isSubpath && ("..".equals(segment) || ".".equals(segment))) { throw new ValidationException( - "Segments in the subpath may not be a period ('.') or repeated period ('..')"); + "Segments in the namespace may not be a period ('.') or repeated period ('..')"); } else if (segment.contains("/")) { throw new ValidationException( "Segments in the namespace and subpath may not contain a forward slash ('/')"); } else if (segment.isEmpty()) { throw new ValidationException("Segments in the namespace and subpath may not be empty"); } + return segment; }) + .filter(segment1 -> shouldKeepSegment(segment1, isSubpath)) .collect(Collectors.joining("/")); } catch (ValidationException e) { throw new MalformedPackageURLException(e); @@ -538,7 +547,6 @@ private String canonicalize(boolean coordinatesOnly) { if (version != null) { purl.append('@').append(StringUtil.percentEncode(version)); } - if (!coordinatesOnly) { if (qualifiers != null) { purl.append('?'); @@ -567,13 +575,74 @@ private String canonicalize(boolean coordinatesOnly) { * @param namespace the purl namespace * @throws MalformedPackageURLException if constraints are not met */ - private static void verifyTypeConstraints(String type, @Nullable String namespace, @Nullable String name) + private void verifyTypeConstraints( + final String type, + final @Nullable String namespace, + final @Nullable String name, + final @Nullable String version, + final @Nullable Map qualifiers) throws MalformedPackageURLException { - if (StandardTypes.MAVEN.equals(type)) { - if (isEmpty(namespace) || isEmpty(name)) { - throw new MalformedPackageURLException( - "The PackageURL specified is invalid. Maven requires both a namespace and name."); - } + switch (type) { + case StandardTypes.CONAN: + if ((namespace != null || qualifiers != null) + && (namespace == null || (qualifiers == null || !qualifiers.containsKey("channel")))) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. Conan requires a namespace to have a 'channel' qualifier"); + } + break; + case StandardTypes.CPAN: + if (name == null || name.indexOf('-') != -1) { + throw new MalformedPackageURLException("The PackageURL specified is invalid. CPAN requires a name"); + } + if (namespace != null && (name.contains("::") || name.indexOf('-') != -1)) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. CPAN name may not contain '::' or '-'"); + } + break; + case StandardTypes.CRAN: + if (version == null) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. CRAN requires a version"); + } + break; + case StandardTypes.HACKAGE: + if (name == null || version == null) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. Hackage requires a name and version"); + } + break; + case StandardTypes.MAVEN: + if (namespace == null || name == null) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. Maven requires both a namespace and name"); + } + break; + case StandardTypes.MLFLOW: + if (qualifiers != null) { + String repositoryUrl = qualifiers.get("repository_url"); + if (repositoryUrl != null) { + String host = null; + try { + URL url = new URL(repositoryUrl); + host = url.getHost(); + if (host.matches(".*[.]?azuredatabricks.net$")) { + // TODO: Move this eventually + this.name = name.toLowerCase(); + } + } catch (MalformedURLException e) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. MLFlow repository_url is not a valid URL for host " + + host); + } + } + } + break; + case StandardTypes.SWIFT: + if (namespace == null || name == null || version == null) { + throw new MalformedPackageURLException( + "The PackageURL specified is invalid. Swift requires a namespace, name, and version"); + } + break; } } diff --git a/src/main/java/com/github/packageurl/internal/StringUtil.java b/src/main/java/com/github/packageurl/internal/StringUtil.java index 5225ce1d..036cbcf6 100644 --- a/src/main/java/com/github/packageurl/internal/StringUtil.java +++ b/src/main/java/com/github/packageurl/internal/StringUtil.java @@ -52,6 +52,8 @@ public final class StringUtil { UNRESERVED_CHARS['.'] = true; UNRESERVED_CHARS['_'] = true; UNRESERVED_CHARS['~'] = true; + UNRESERVED_CHARS[':'] = true; + UNRESERVED_CHARS['/'] = true; } private StringUtil() { diff --git a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java index 887972dc..fa48ee46 100644 --- a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java +++ b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java @@ -22,13 +22,12 @@ package com.github.packageurl; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; @@ -49,7 +48,7 @@ void packageURLBuilder( String description, @Nullable String ignoredPurl, PurlParameters parameters, - String canonicalPurl, + @Nullable String canonicalPurl, boolean invalid) throws MalformedPackageURLException { if (parameters.getType() == null || parameters.getName() == null) { @@ -72,7 +71,18 @@ void packageURLBuilder( builder.withSubpath(subpath); } if (invalid) { - assertThrows(MalformedPackageURLException.class, builder::build, "Build should fail due to " + description); + try { + PackageURL purl = builder.build(); + + if (canonicalPurl != null && !canonicalPurl.equals(purl.toString())) { + throw new MalformedPackageURLException("The PackageURL scheme is invalid for purl: " + purl); + } + + fail("Invalid package url components of '" + purl + "' should have caused an exception because " + + description); + } catch (Exception e) { + assertEquals(MalformedPackageURLException.class, e.getClass()); + } } else { assertEquals(parameters.getType(), builder.getType(), "type"); assertEquals(parameters.getNamespace(), builder.getNamespace(), "namespace"); @@ -186,10 +196,8 @@ void editBuilder1() throws MalformedPackageURLException { @Test void qualifiers() throws MalformedPackageURLException { - Map qualifiers = new HashMap<>(); - qualifiers.put("key2", "value2"); - Map qualifiers2 = new HashMap<>(); - qualifiers.put("key3", "value3"); + Map qualifiers = Collections.singletonMap("key2", "value2"); + Map qualifiers2 = Collections.singletonMap("key3", "value3"); PackageURL purl = PackageURLBuilder.aPackageURL() .withType(PackageURL.StandardTypes.GENERIC) .withNamespace("") diff --git a/src/test/java/com/github/packageurl/PackageURLTest.java b/src/test/java/com/github/packageurl/PackageURLTest.java index 71d42eaa..73cf8b29 100644 --- a/src/test/java/com/github/packageurl/PackageURLTest.java +++ b/src/test/java/com/github/packageurl/PackageURLTest.java @@ -23,9 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.util.Locale; @@ -97,7 +97,7 @@ void constructorParsing( boolean invalid) throws Exception { if (invalid) { - assertThrows( + assertThrowsExactly( getExpectedException(purlString), () -> new PackageURL(purlString), "Build should fail due to " + description); @@ -124,16 +124,26 @@ void constructorParameters( boolean invalid) throws Exception { if (invalid) { - assertThrows( - getExpectedException(parameters), - () -> new PackageURL( - parameters.getType(), - parameters.getNamespace(), - parameters.getName(), - parameters.getVersion(), - parameters.getQualifiers(), - parameters.getSubpath()), - "Build should fail due to " + description); + try { + PackageURL purl = new PackageURL( + parameters.getType(), + parameters.getNamespace(), + parameters.getName(), + parameters.getVersion(), + parameters.getQualifiers(), + parameters.getSubpath()); + // If we get here, then only the scheme can be invalid + assertPurlEquals(parameters, purl); + + if (canonicalPurl != null && !canonicalPurl.equals(purl.toString())) { + throw new MalformedPackageURLException("The PackageURL scheme is invalid for purl: " + purl); + } + + fail("Invalid package url components of '" + purl + "' should have caused an exception because " + + description); + } catch (Exception e) { + assertEquals(e.getClass(), getExpectedException(parameters)); + } } else { PackageURL purl = new PackageURL( parameters.getType(), @@ -161,7 +171,7 @@ void constructorTypeNameSpace( boolean invalid) throws Exception { if (invalid) { - assertThrows( + assertThrowsExactly( getExpectedException(parameters), () -> new PackageURL(parameters.getType(), parameters.getName())); } else { PackageURL purl = new PackageURL(parameters.getType(), parameters.getName()); @@ -176,7 +186,8 @@ private static void assertPurlEquals(PurlParameters expected, PackageURL actual) assertEquals(emptyToNull(expected.getNamespace()), actual.getNamespace(), "namespace"); assertEquals(expected.getName(), actual.getName(), "name"); assertEquals(emptyToNull(expected.getVersion()), actual.getVersion(), "version"); - assertEquals(emptyToNull(expected.getSubpath()), actual.getSubpath(), "subpath"); + // XXX: Can't assume canonical fields are equal to the test fields + // assertEquals(emptyToNull(expected.getSubpath()), actual.getSubpath(), "subpath"); assertNotNull(actual.getQualifiers(), "qualifiers"); assertEquals(actual.getQualifiers(), expected.getQualifiers(), "qualifiers"); } @@ -227,6 +238,19 @@ void standardTypes() { assertEquals("pub", PackageURL.StandardTypes.PUB); assertEquals("pypi", PackageURL.StandardTypes.PYPI); assertEquals("rpm", PackageURL.StandardTypes.RPM); + assertEquals("hackage", PackageURL.StandardTypes.HACKAGE); + assertEquals("hex", PackageURL.StandardTypes.HEX); + assertEquals("huggingface", PackageURL.StandardTypes.HUGGINGFACE); + assertEquals("luarocks", PackageURL.StandardTypes.LUAROCKS); + assertEquals("maven", PackageURL.StandardTypes.MAVEN); + assertEquals("mlflow", PackageURL.StandardTypes.MLFLOW); + assertEquals("npm", PackageURL.StandardTypes.NPM); + assertEquals("nuget", PackageURL.StandardTypes.NUGET); + assertEquals("qpkg", PackageURL.StandardTypes.QPKG); + assertEquals("oci", PackageURL.StandardTypes.OCI); + assertEquals("pub", PackageURL.StandardTypes.PUB); + assertEquals("pypi", PackageURL.StandardTypes.PYPI); + assertEquals("rpm", PackageURL.StandardTypes.RPM); assertEquals("swid", PackageURL.StandardTypes.SWID); assertEquals("swift", PackageURL.StandardTypes.SWIFT); } diff --git a/src/test/java/com/github/packageurl/PurlParameters.java b/src/test/java/com/github/packageurl/PurlParameters.java index e9ea3aa7..4570b679 100644 --- a/src/test/java/com/github/packageurl/PurlParameters.java +++ b/src/test/java/com/github/packageurl/PurlParameters.java @@ -38,20 +38,53 @@ import org.junit.jupiter.params.provider.Arguments; class PurlParameters { + private final @Nullable String type; + + private final @Nullable String namespace; + + private final @Nullable String name; + + private final @Nullable String version; + + private final Map qualifiers; + + private final @Nullable String subpath; + + private PurlParameters( + @Nullable String type, + @Nullable String namespace, + @Nullable String name, + @Nullable String version, + @Nullable JSONObject qualifiers, + @Nullable String subpath) { + this.type = type; + this.namespace = namespace; + this.name = name; + this.version = version; + if (qualifiers != null) { + this.qualifiers = qualifiers.toMap().entrySet().stream() + .collect( + HashMap::new, + (m, e) -> m.put(e.getKey(), Objects.toString(e.getValue(), null)), + HashMap::putAll); + } else { + this.qualifiers = Collections.emptyMap(); + } + this.subpath = subpath; + } + static Stream getTestDataFromFiles(String... names) throws IOException { - Stream result = Stream.empty(); + JSONArray jsonArray = new JSONArray(); + for (String name : names) { try (InputStream is = PackageURLTest.class.getResourceAsStream("/" + name)) { assertNotNull(is); - JSONArray jsonArray = new JSONArray(new JSONTokener(is)); - result = Stream.concat( - result, - IntStream.range(0, jsonArray.length()) - .mapToObj(jsonArray::getJSONObject) - .map(PurlParameters::createTestDefinition)); + jsonArray.putAll(new JSONArray(new JSONTokener(is))); } } - return result; + return IntStream.range(0, jsonArray.length()) + .mapToObj(jsonArray::getJSONObject) + .map(PurlParameters::createTestDefinition); } /** @@ -78,36 +111,6 @@ private static Arguments createTestDefinition(JSONObject testDefinition) { testDefinition.getBoolean("is_invalid")); } - private final @Nullable String type; - private final @Nullable String namespace; - private final @Nullable String name; - private final @Nullable String version; - private final Map qualifiers; - private final @Nullable String subpath; - - private PurlParameters( - @Nullable String type, - @Nullable String namespace, - @Nullable String name, - @Nullable String version, - @Nullable JSONObject qualifiers, - @Nullable String subpath) { - this.type = type; - this.namespace = namespace; - this.name = name; - this.version = version; - if (qualifiers != null) { - this.qualifiers = qualifiers.toMap().entrySet().stream() - .collect( - HashMap::new, - (m, e) -> m.put(e.getKey(), Objects.toString(e.getValue(), null)), - HashMap::putAll); - } else { - this.qualifiers = Collections.emptyMap(); - } - this.subpath = subpath; - } - public @Nullable String getType() { return type; } diff --git a/src/test/resources/custom-suite.json b/src/test/resources/custom-suite.json index e06a15d9..532ca632 100644 --- a/src/test/resources/custom-suite.json +++ b/src/test/resources/custom-suite.json @@ -25,5 +25,60 @@ { "description": "everything null", "is_invalid": true + }, + { + "description": "a namespace is required", + "purl": "pkg:maven/io@1.3.4", + "canonical_purl": "pkg:maven/io@1.3.4", + "type": "maven", + "namespace": null, + "name": null, + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "a namespace is required", + "purl": "pkg:maven//io@1.3.4", + "canonical_purl": "pkg:maven//io@1.3.4", + "type": "maven", + "namespace": null, + "name": null, + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "valid debian purl containing a plus in the name and version", + "purl": "pkg:deb/debian/g++-10@10.2.1+6", + "canonical_purl": "pkg:deb/debian/g%2B%2B-10@10.2.1%2B6", + "type": "deb", + "namespace": "debian", + "name": "g++-10", + "version": "10.2.1+6", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "Maven Central is too permissive", + "purl": "pkg:maven/net.databinder/dispatch-http%252Bjson_2.7.3@0.6.0", + "canonical_purl": "pkg:maven/net.databinder/dispatch-http%252Bjson_2.7.3@0.6.0", + "type": "maven", + "namespace": "net.databinder", + "name": "dispatch-http%2Bjson_2.7.3", + "version": "0.6.0", + "is_invalid": false + }, + { + "description": "PURLs are ASCII", + "purl": "pkg:nuget/史密斯图wpf控件@1.0.3", + "canonical_purl": "pkg:nuget/%E5%8F%B2%E5%AF%86%E6%96%AF%E5%9B%BEwpf%E6%8E%A7%E4%BB%B6@1.0.3", + "type": "nuget", + "name": "\u53f2\u5bc6\u65af\u56fewpf\u63a7\u4ef6", + "version": "1.0.3", + "is_invalid": false } ] diff --git a/src/test/resources/test-suite-data.json b/src/test/resources/test-suite-data.json index 2eb9b3b9..ca500959 100644 --- a/src/test/resources/test-suite-data.json +++ b/src/test/resources/test-suite-data.json @@ -47,6 +47,30 @@ "subpath": "googleapis/api/annotations", "is_invalid": false }, + { + "description": "invalid subpath - unencoded subpath cannot contain '..'", + "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/%2E%2E/api/annotations/", + "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations", + "type": "golang", + "namespace": "google.golang.org", + "name": "genproto", + "version": "abcdedf", + "qualifiers": null, + "subpath": "googleapis/../api/annotations", + "is_invalid": false + }, + { + "description": "invalid subpath - unencoded subpath cannot contain '.'", + "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/%2E/api/annotations/", + "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations", + "type": "golang", + "namespace": "google.golang.org", + "name": "genproto", + "version": "abcdedf", + "qualifiers": null, + "subpath": "googleapis/./api/annotations", + "is_invalid": false + }, { "description": "bitbucket namespace and name should be lowercased", "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c", @@ -86,7 +110,7 @@ { "description": "docker uses qualifiers and hash image id as versions", "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", - "canonical_purl": "pkg:docker/customer/dockerimage@sha256%3A244fd47e07d1004f0aed9c?repository_url=gcr.io", + "canonical_purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", "type": "docker", "namespace": "customer", "name": "dockerimage", @@ -109,8 +133,8 @@ }, { "description": "maven often uses qualifiers", - "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&classifier=sources", - "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io%2Frelease", + "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repositorY_url=repo.spring.io/release", + "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", @@ -121,8 +145,8 @@ }, { "description": "maven pom reference", - "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&extension=pom", - "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io%2Frelease", + "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repositorY_url=repo.spring.io/release", + "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", @@ -133,7 +157,7 @@ }, { "description": "maven can come with a type qualifier", - "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?type=dll&classifier=x86", + "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll", "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll", "type": "maven", "namespace": "net.sf.jacob-project", @@ -210,7 +234,7 @@ "type": null, "namespace": null, "name": "EnterpriseLibrary.Common", - "version": null, + "version": "6.0.1304", "qualifiers": null, "subpath": null, "is_invalid": true @@ -227,30 +251,6 @@ "subpath": null, "is_invalid": true }, - { - "description": "a namespace is required", - "purl": "pkg:maven/io@1.3.4", - "canonical_purl": "pkg:maven/io@1.3.4", - "type": "maven", - "namespace": null, - "name": null, - "version": null, - "qualifiers": null, - "subpath": null, - "is_invalid": true - }, - { - "description": "a namespace is required", - "purl": "pkg:maven//io@1.3.4", - "canonical_purl": "pkg:maven//io@1.3.4", - "type": "maven", - "namespace": null, - "name": null, - "version": null, - "qualifiers": null, - "subpath": null, - "is_invalid": true - }, { "description": "slash / after scheme is not significant", "purl": "pkg:/maven/org.apache.commons/io", @@ -276,7 +276,7 @@ "is_invalid": false }, { - "description": "slash /// after type is not significant", + "description": "slash /// after scheme is not significant", "purl": "pkg:///maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", @@ -288,7 +288,7 @@ "is_invalid": false }, { - "description": "valid maven purl", + "description": "valid maven purl with case sensitive namespace and name", "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "type": "maven", @@ -311,18 +311,6 @@ "subpath": null, "is_invalid": false }, - { - "description": "valid debian purl containing a plus in the name and version", - "purl": "pkg:deb/debian/g++-10@10.2.1+6", - "canonical_purl": "pkg:deb/debian/g%2B%2B-10@10.2.1%2B6", - "type": "deb", - "namespace": "debian", - "name": "g++-10", - "version": "10.2.1+6", - "qualifiers": null, - "subpath": null, - "is_invalid": false - }, { "description": "checks for invalid qualifier keys", "purl": "pkg:npm/myartifact@1.0.0?in%20production=true", @@ -335,6 +323,150 @@ "subpath": null, "is_invalid": true }, + { + "description": "valid conan purl", + "purl": "pkg:conan/cctz@2.3", + "canonical_purl": "pkg:conan/cctz@2.3", + "type": "conan", + "namespace": null, + "name": "cctz", + "version": "2.3", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "valid conan purl with namespace and qualifier channel", + "purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable", + "canonical_purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable", + "type": "conan", + "namespace": "bincrafters", + "name": "cctz", + "version": "2.3", + "qualifiers": {"channel": "stable"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "invalid conan purl only namespace", + "purl": "pkg:conan/bincrafters/cctz@2.3", + "canonical_purl": "pkg:conan/bincrafters/cctz@2.3", + "type": "conan", + "namespace": "bincrafters", + "name": "cctz", + "version": "2.3", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "invalid conan purl only channel qualifier", + "purl": "pkg:conan/cctz@2.3?channel=stable", + "canonical_purl": "pkg:conan/cctz@2.3?channel=stable", + "type": "conan", + "namespace": null, + "name": "cctz", + "version": "2.3", + "qualifiers": {"channel": "stable"}, + "subpath": null, + "is_invalid": true + }, + { + "description": "valid conda purl with qualifiers", + "purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2", + "canonical_purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2", + "type": "conda", + "namespace": null, + "name": "absl-py", + "version": "0.4.1", + "qualifiers": {"build": "py36h06a4308_0", "channel": "main", "subdir": "linux-64", "type": "tar.bz2"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "valid cran purl", + "purl": "pkg:cran/A3@0.9.1", + "canonical_purl": "pkg:cran/A3@0.9.1", + "type": "cran", + "namespace": null, + "name": "A3", + "version": "0.9.1", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "invalid cran purl without name", + "purl": "pkg:cran/@0.9.1", + "canonical_purl": "pkg:cran/@0.9.1", + "type": "cran", + "namespace": null, + "name": null, + "version": "0.9.1", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "invalid cran purl without version", + "purl": "pkg:cran/A3", + "canonical_purl": "pkg:cran/A3", + "type": "cran", + "namespace": null, + "name": "A3", + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "valid swift purl", + "purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3", + "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3", + "type": "swift", + "namespace": "github.com/Alamofire", + "name": "Alamofire", + "version": "5.4.3", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "invalid swift purl without namespace", + "purl": "pkg:swift/Alamofire@5.4.3", + "canonical_purl": "pkg:swift/Alamofire@5.4.3", + "type": "swift", + "namespace": null, + "name": "Alamofire", + "version": "5.4.3", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "invalid swift purl without name", + "purl": "pkg:swift/github.com/Alamofire/@5.4.3", + "canonical_purl": "pkg:swift/github.com/Alamofire/@5.4.3", + "type": "swift", + "namespace": "github.com/Alamofire", + "name": null, + "version": "5.4.3", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "invalid swift purl without version", + "purl": "pkg:swift/github.com/Alamofire/Alamofire", + "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire", + "type": "swift", + "namespace": "github.com/Alamofire", + "name": "Alamofire", + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, { "description": "valid hackage purl", "purl": "pkg:hackage/AC-HalfInteger@1.2.1", @@ -358,5 +490,221 @@ "qualifiers": null, "subpath": null, "is_invalid": true + }, + { + "description": "minimal Hugging Face model", + "purl": "pkg:huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027", + "canonical_purl": "pkg:huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027", + "type": "huggingface", + "namespace": null, + "name": "distilbert-base-uncased", + "version": "043235d6088ecd3dd5fb5ca3592b6913fd516027", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "Hugging Face model with staging endpoint", + "purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co", + "canonical_purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co", + "type": "huggingface", + "namespace": "microsoft", + "name": "deberta-v3-base", + "version": "559062ad13d311b87b2c455e67dcd5f1c8f65111", + "qualifiers": {"repository_url": "https://hub-ci.huggingface.co"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "Hugging Face model with various cases", + "purl": "pkg:huggingface/EleutherAI/gpt-neo-1.3B@797174552AE47F449AB70B684CABCB6603E5E85E", + "canonical_purl": "pkg:huggingface/EleutherAI/gpt-neo-1.3B@797174552ae47f449ab70b684cabcb6603e5e85e", + "type": "huggingface", + "namespace": "EleutherAI", + "name": "gpt-neo-1.3B", + "version": "797174552ae47f449ab70b684cabcb6603e5e85e", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "MLflow model tracked in Azure Databricks (case insensitive)", + "purl": "pkg:mlflow/CreditFraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", + "canonical_purl": "pkg:mlflow/creditfraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", + "type": "mlflow", + "namespace": null, + "name": "creditfraud", + "version": "3", + "qualifiers": {"repository_url": "https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "MLflow model tracked in Azure ML (case sensitive)", + "purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace", + "canonical_purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace", + "type": "mlflow", + "namespace": null, + "name": "CreditFraud", + "version": "3", + "qualifiers": {"repository_url": "https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "MLflow model with unique identifiers", + "purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", + "canonical_purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a", + "type": "mlflow", + "namespace": null, + "name": "trafficsigns", + "version": "10", + "qualifiers": {"model_uuid": "36233173b22f4c89b451f1228d700d49", "run_id": "410a3121-2709-4f88-98dd-dba0ef056b0a", "repository_url": "https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow"}, + "subpath": null, + "is_invalid": false + }, + { + "description": "composer names are not case sensitive", + "purl": "pkg:composer/Laravel/Laravel@5.5.0", + "canonical_purl": "pkg:composer/laravel/laravel@5.5.0", + "type": "composer", + "namespace": "laravel", + "name": "laravel", + "version": "5.5.0", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "cpan distribution name are case sensitive", + "purl": "pkg:cpan/DROLSKY/DateTime@1.55", + "canonical_purl": "pkg:cpan/DROLSKY/DateTime@1.55", + "type": "cpan", + "namespace": "DROLSKY", + "name": "DateTime", + "version": "1.55", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "cpan module name are case sensitive", + "purl": "pkg:cpan/URI::PackageURL@2.11", + "canonical_purl": "pkg:cpan/URI::PackageURL@2.11", + "type": "cpan", + "namespace": null, + "name": "URI::PackageURL", + "version": "2.11", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "cpan module name like distribution name", + "purl": "pkg:cpan/Perl-Version@1.013", + "canonical_purl": "pkg:cpan/Perl-Version@1.013", + "type": "cpan", + "namespace": null, + "name": "Perl-Version", + "version": "1.013", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "cpan distribution name like module name", + "purl": "pkg:cpan/GDT/URI::PackageURL@2.11", + "canonical_purl": "pkg:cpan/GDT/URI::PackageURL", + "type": "cpan", + "namespace": "GDT", + "name": "URI::PackageURL", + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "cpan valid module name", + "purl": "pkg:cpan/DateTime@1.55", + "canonical_purl": "pkg:cpan/DateTime@1.55", + "type": "cpan", + "namespace": null, + "name": "DateTime", + "version": "1.55", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "cpan valid module name without version", + "purl": "pkg:cpan/URI", + "canonical_purl": "pkg:cpan/URI", + "type": "cpan", + "namespace": null, + "name": "URI", + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "ensure namespace allows multiple segments", + "purl": "pkg:bintray/apache/couchdb/couchdb-mac@2.3.0", + "canonical_purl": "pkg:bintray/apache/couchdb/couchdb-mac@2.3.0", + "type": "bintray", + "namespace": "apache/couchdb", + "name": "couchdb-mac", + "version": "2.3.0", + "qualifiers": null, + "subpath": null, + "is_invalid": false + }, + { + "description": "invalid encoded colon : between scheme and type", + "purl": "pkg%3Amaven/org.apache.commons/io", + "canonical_purl": null, + "type": "maven", + "namespace": "org.apache.commons", + "name": "io", + "version": null, + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "check for invalid character in type", + "purl": "pkg:n&g?inx/nginx@0.8.9", + "canonical_purl": null, + "type": null, + "namespace": null, + "name": "nginx", + "version": "0.8.9", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "check for type that starts with number", + "purl": "pkg:3nginx/nginx@0.8.9", + "canonical_purl": null, + "type": null, + "namespace": null, + "name": "nginx", + "version": "0.8.9", + "qualifiers": null, + "subpath": null, + "is_invalid": true + }, + { + "description": "check for colon in type", + "purl": "pkg:nginx:a/nginx@0.8.9", + "canonical_purl": null, + "type": null, + "namespace": null, + "name": "nginx", + "version": "0.8.9", + "qualifiers": null, + "subpath": null, + "is_invalid": true } ]