diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index a474651f..6a94dade 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -27,8 +27,10 @@ import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -477,30 +479,34 @@ 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 @Nullable String validatePath(final String[] segments, final boolean isSubpath) throws MalformedPackageURLException { - if (segments.length == 0) { + int length = segments.length; + + if (length == 0) { return null; } - try { - return Arrays.stream(segments) - .peek(segment -> { - if (isSubPath && ("..".equals(segment) || ".".equals(segment))) { - throw new ValidationException( - "Segments in the subpath 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"); - } - }) - .collect(Collectors.joining("/")); - } catch (ValidationException e) { - throw new MalformedPackageURLException(e); + + List newSegments = new ArrayList<>(length); + + for (String segment : segments) { + if (".".equals(segment) || "..".equals(segment)) { + if (!isSubpath) { + throw new MalformedPackageURLException( + "Segments in the namespace must not be a period ('.') or repeated period ('..'): '" + + segment + "'"); + } + } else if (segment.isEmpty() || segment.contains("/")) { + throw new MalformedPackageURLException( + "Segments in the namespace and subpath must not contain a '/' and must not be empty: '" + + segment + "'"); + } else { + newSegments.add(segment); + } } - } + return String.join("/", newSegments); + } /** * Returns the canonicalized representation of the purl. * @@ -621,8 +627,8 @@ private static void verifyTypeConstraints(String type, @Nullable String namespac } } - private static String[] parsePath(final String path, final boolean isSubpath) { - return Arrays.stream(path.split("/")) + private static String[] parsePath(final String encodedPath, final boolean isSubpath) { + return Arrays.stream(encodedPath.split("/")) .filter(segment -> !segment.isEmpty() && !(isSubpath && (".".equals(segment) || "..".equals(segment)))) .map(StringUtil::percentDecode) .toArray(String[]::new); diff --git a/src/test/java/com/github/packageurl/PackageURLTest.java b/src/test/java/com/github/packageurl/PackageURLTest.java index 71d42eaa..2d7ee3cd 100644 --- a/src/test/java/com/github/packageurl/PackageURLTest.java +++ b/src/test/java/com/github/packageurl/PackageURLTest.java @@ -21,6 +21,7 @@ */ package com.github.packageurl; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -176,7 +177,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 compare canonical fields to components + // assertEquals(emptyToNull(expected.getSubpath()), actual.getSubpath(), "subpath"); assertNotNull(actual.getQualifiers(), "qualifiers"); assertEquals(actual.getQualifiers(), expected.getQualifiers(), "qualifiers"); } @@ -272,4 +274,21 @@ void npmCaseSensitive() throws Exception { assertEquals("Base64", base64Uppercase.getName()); assertEquals("1.0.0", base64Uppercase.getVersion()); } + + @Test + void namespace() { + assertDoesNotThrow(() -> new PackageURL("pkg:maven/..HTTPClient.//HTTPClient@0.3-3")); + assertDoesNotThrow(() -> new PackageURL("pkg:maven///HTTPClient///HTTPClient@0.3-3")); + assertThrowsExactly( + MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/../HTTPClient/HTTPClient@0.3-3")); + assertThrowsExactly( + MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/./HTTPClient/HTTPClient@0.3-3")); + assertThrowsExactly( + MalformedPackageURLException.class, + () -> new PackageURL("pkg:maven/%2E%2E/HTTPClient/HTTPClient@0.3-3")); + assertThrowsExactly( + MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/%2E/HTTPClient/HTTPClient@0.3-3")); + assertThrowsExactly( + MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/%2F/HTTPClient/HTTPClient@0.3-3")); + } } diff --git a/src/test/resources/test-suite-data.json b/src/test/resources/test-suite-data.json index 2eb9b3b9..41daf7b3 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",