Skip to content

fix: discard dot segments from subpath #176

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
48 changes: 27 additions & 21 deletions src/main/java/com/github/packageurl/PackageURL.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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.
*
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 20 additions & 1 deletion src/test/java/com/github/packageurl/PackageURLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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.//[email protected]"));
assertDoesNotThrow(() -> new PackageURL("pkg:maven///HTTPClient///[email protected]"));
assertThrowsExactly(
MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/../HTTPClient/[email protected]"));
assertThrowsExactly(
MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/./HTTPClient/[email protected]"));
assertThrowsExactly(
MalformedPackageURLException.class,
() -> new PackageURL("pkg:maven/%2E%2E/HTTPClient/[email protected]"));
assertThrowsExactly(
MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/%2E/HTTPClient/[email protected]"));
assertThrowsExactly(
MalformedPackageURLException.class, () -> new PackageURL("pkg:maven/%2F/HTTPClient/[email protected]"));
}
}
24 changes: 24 additions & 0 deletions src/test/resources/test-suite-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down