Package-URL (aka purl) is a "mostly universal" URL to describe a package. A purl is a URL composed of seven components:
@@ -52,8 +53,8 @@ public final class PackageURL implements Serializable { private static final long serialVersionUID = 3243226021636427586L; - private static final Pattern TYPE_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9.+-]+$"); - private static final Pattern KEY_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9.-_]+$"); + private static final String UTF8 = StandardCharsets.UTF_8.name(); + private static final Pattern PATH_SPLITTER = Pattern.compile("/"); /** * Constructs a new PackageURL object by parsing the specified string. @@ -100,7 +101,7 @@ public PackageURL(String type, String namespace, String name, String version, Tr this.name = validateName(name); this.version = validateVersion(version); this.qualifiers = validateQualifiers(qualifiers); - this.subpath = validateSubpath(subpath); + this.subpath = validatePath(subpath, true); } /** @@ -144,6 +145,11 @@ public PackageURL(String type, String namespace, String name, String version, Tr */ private String subpath; + /** + * The cached version of the canonical form. + */ + private String canonicalizedForm = null; + /** * Returns the package url scheme. * @@ -214,93 +220,43 @@ public String getSubpath() { return subpath; } - /** - * Given a specified PackageURL, this method will parse the purl and populate this classes - * instance fields so that the corresponding getters may be called to retrieve the individual - * pieces of the purl. - * - * @param purl the purl string to parse - * @throws MalformedPackageURLException if an exception occurs when parsing - */ - private void parse(String purl) throws MalformedPackageURLException { - if (purl == null || "".equals(purl.trim())) { - throw new MalformedPackageURLException("Invalid purl: Contains an empty or null value"); - } - - try { - URI uri = new URI(purl); - // Check to ensure that none of these parts are parsed. If so, it's an invalid purl. - if (uri.getUserInfo() != null || uri.getPort() != -1) { - throw new MalformedPackageURLException("Invalid purl: Contains parts not supported by the purl spec"); - } - - this.scheme = validateScheme(uri.getScheme()); - - // This is the purl (minus the scheme) that needs parsed. - String remainder = purl.substring(4); - - if (remainder.contains("#")) { // subpath is optional - check for existence - final int index = remainder.lastIndexOf("#"); - this.subpath = validateSubpath(remainder.substring(index + 1)); - remainder = remainder.substring(0, index); - } - - if (remainder.contains("?")) { // qualifiers are optional - check for existence - final int index = remainder.lastIndexOf("?"); - this.qualifiers = validateQualifiers(remainder.substring(index + 1)); - remainder = remainder.substring(0, index); - } - - if (remainder.contains("@")) { // version is optional - check for existence - final int index = remainder.lastIndexOf("@"); - this.version = validateVersion(remainder.substring(index + 1)); - remainder = remainder.substring(0, index); - } - - // The 'remainder' should now consist of the type, an optional namespace, and the name - - // Strip zero or more leading '/' from the beginning ('type') - remainder = remainder.replaceAll("^[/]*", ""); - - String[] firstPartArray = remainder.split("/"); - if (firstPartArray.length < 2) { // The array must contain a 'type' and a 'name' at minimum - throw new MalformedPackageURLException("Invalid purl: Does not contain a minimum of a 'type' and a 'name'"); - } - - this.type = validateType(firstPartArray[0]); - this.name = validateName(firstPartArray[firstPartArray.length - 1]); - - // Test for namespaces - if (firstPartArray.length > 2) { - String[] namespaces = Arrays.copyOfRange(firstPartArray, 1, firstPartArray.length - 1); - String namespace = String.join(",", namespaces); - this.namespace = validateNamespace(namespace); - } - - } catch (URISyntaxException e) { - throw new MalformedPackageURLException("Invalid purl: " + e.getMessage()); + private String validateScheme(String value) throws MalformedPackageURLException { + if ("pkg".equals(value)) { + return "pkg"; } + throw new MalformedPackageURLException("The PackageURL scheme is invalid"); } - private String validateScheme(String scheme) throws MalformedPackageURLException { - if (scheme == null || !scheme.equals("pkg")) { - throw new MalformedPackageURLException("The PackageURL scheme is invalid"); + private String validateType(String value) throws MalformedPackageURLException { + if (value == null || value.isEmpty()) { + throw new MalformedPackageURLException("The PackageURL type cannot be null or empty"); } - return scheme; + if (value.indexOf(0)>='0' && value.indexOf(0)<='9') { + throw new MalformedPackageURLException("The PackageURL type contains start with a number"); + } + String retVal = value.toLowerCase(); + if (retVal.chars().anyMatch(c -> !(c == '.' || c == '+' || c == '-' + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')))) { + throw new MalformedPackageURLException("The PackageURL type contains invalid characters"); + } + return retVal; } - private String validateType(String type) throws MalformedPackageURLException { - if (type == null || !TYPE_PATTERN.matcher(type).matches()) { - throw new MalformedPackageURLException("The PackageURL type specified is invalid"); + private String validateNamespace(String value) throws MalformedPackageURLException { + if (value == null || value.isEmpty()) { + return null; } - return type.toLowerCase(); + return validateNamespace(value.split("/")); } - private String validateNamespace(String value) { - if (value == null) { + private String validateNamespace(String[] values) throws MalformedPackageURLException { + if (values == null || values.length == 0) { return null; } - String temp; + String tempNamespace = validatePath(values, false); + + String retVal; switch (type) { case StandardTypes.BITBUCKET: case StandardTypes.DEBIAN: @@ -308,17 +264,17 @@ private String validateNamespace(String value) { case StandardTypes.GOLANG: case StandardTypes.NPM: case StandardTypes.RPM: - temp = value.toLowerCase(); + retVal = tempNamespace.toLowerCase(); break; default: - temp = value; + retVal = tempNamespace; break; } - return urldecode(temp); + return retVal; } private String validateName(String value) throws MalformedPackageURLException { - if (value == null) { + if (value == null || value.isEmpty()) { throw new MalformedPackageURLException("The PackageURL name specified is invalid"); } String temp; @@ -337,65 +293,91 @@ private String validateName(String value) throws MalformedPackageURLException { temp = value; break; } - return urldecode(temp); + return temp; } - private String validateVersion(String version) { - if (version == null) { + private String validateVersion(String value) { + if (value == null) { return null; } - return urldecode(version); + return value; } - @SuppressWarnings("StringSplitter")//reason: surprising behavior is okay in this case - private MapJava implementation of the Package-URL Specification.
+ *https://github.com/package-url/purl-spec
*/ package com.github.packageurl; diff --git a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java index 6012840..cc4732d 100644 --- a/src/test/java/com/github/packageurl/PackageURLBuilderTest.java +++ b/src/test/java/com/github/packageurl/PackageURLBuilderTest.java @@ -21,20 +21,29 @@ */ package com.github.packageurl; +import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import static org.junit.Assert.*; public class PackageURLBuilderTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + @Test public void testPackageURLBuilder() throws MalformedPackageURLException { + exception = ExpectedException.none(); + PackageURL purl = PackageURLBuilder.aPackageURL() - .withType("type") + .withType("my.type-9+") .withName("name") .build(); - assertEquals("pkg:type/name", purl.canonicalize()); + assertEquals("pkg:my.type-9+/name", purl.toString()); + assertEquals("pkg:my.type-9+/name", purl.canonicalize()); purl = PackageURLBuilder.aPackageURL() .withType("type") @@ -45,18 +54,106 @@ public void testPackageURLBuilder() throws MalformedPackageURLException { .withSubpath("subpath") .build(); - assertEquals("pkg:type/namespace/name@version?key=value#subpath", purl.canonicalize()); + assertEquals("pkg:type/namespace/name@version?key=value#subpath", purl.toString()); purl = PackageURLBuilder.aPackageURL() .withType(PackageURL.StandardTypes.GENERIC) .withNamespace("namespace") .withName("name") .withVersion("version") - .withQualifier("key","value") + .withQualifier("key_1.1-","value") .withSubpath("subpath") .build(); - assertEquals("pkg:generic/namespace/name@version?key=value#subpath", purl.canonicalize()); + assertEquals("pkg:generic/namespace/name@version?key_1.1-=value#subpath", purl.toString()); + + purl = PackageURLBuilder.aPackageURL() + .withType(PackageURL.StandardTypes.GENERIC) + .withNamespace("/////") + .withName("name") + .withVersion("version") + .withQualifier("key","value") + .withSubpath("/////") + .build(); + + assertEquals("pkg:generic/name@version?key=value", purl.toString()); + + purl = PackageURLBuilder.aPackageURL() + .withType(PackageURL.StandardTypes.GENERIC) + .withNamespace("") + .withName("name") + .withVersion("version") + .withQualifier("key","value") + .withQualifier("next","value") + .withSubpath("") + .build(); + + assertEquals("pkg:generic/name@version?key=value&next=value", purl.toString()); + } + + @Test + public void testPackageURLBuilderException1() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("type") + .withName("name") + .withQualifier("key","") + .build(); + Assert.fail("Build should fail due to invalid qualifier (empty value)"); + } + + @Test + public void testPackageURLBuilderException2() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("type") + .withNamespace("invalid//namespace") + .withName("name") + .build(); + Assert.fail("Build should fail due to invalid namespace"); + } + + @Test + public void testPackageURLBuilderException3() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("typ^e") + .withSubpath("invalid/name%2Fspace") + .withName("name") + .build(); + Assert.fail("Build should fail due to invalid subpath"); + } + @Test + public void testPackageURLBuilderException4() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("0_type") + .withName("name") + .build(); + Assert.fail("Build should fail due to invalid type"); + } + + @Test + public void testPackageURLBuilderException5() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("ype") + .withName("name") + .withQualifier("0_key","value") + .build(); + Assert.fail("Build should fail due to invalid qualifier key"); } + + @Test + public void testPackageURLBuilderException6() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + PackageURL purl = PackageURLBuilder.aPackageURL() + .withType("ype") + .withName("name") + .withQualifier("","value") + .build(); + Assert.fail("Build should fail due to invalid qualifier key"); + } + } \ No newline at end of file diff --git a/src/test/java/com/github/packageurl/PackageURLTest.java b/src/test/java/com/github/packageurl/PackageURLTest.java index a6d8341..e47837c 100644 --- a/src/test/java/com/github/packageurl/PackageURLTest.java +++ b/src/test/java/com/github/packageurl/PackageURLTest.java @@ -26,22 +26,26 @@ import org.json.JSONObject; import org.junit.Assert; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; + import java.io.IOException; import java.io.InputStream; -import java.util.Map; import java.util.TreeMap; -import java.util.stream.Collectors; /** * Test cases for PackageURL parsing - * + *
* Original test cases retrieved from: https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json
*
* @author Steve Springett
*/
public class PackageURLTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
private static JSONArray json = new JSONArray();
@BeforeClass
@@ -53,6 +57,7 @@ public static void setup() throws IOException {
@Test
public void testConstructorParsing() throws Exception {
+ exception = ExpectedException.none();
for (int i = 0; i < json.length(); i++) {
JSONObject testDefinition = json.getJSONObject(i);
@@ -71,8 +76,8 @@ public void testConstructorParsing() throws Exception {
if (invalid) {
try {
- new PackageURL(purlString);
- Assert.fail();
+ PackageURL purl = new PackageURL(purlString);
+ Assert.fail("Inavlid purl should have caused an exception: " + purl.toString());
} catch (MalformedPackageURLException e) {
Assert.assertNotNull(e.getMessage());
}
@@ -80,7 +85,6 @@ public void testConstructorParsing() throws Exception {
}
PackageURL purl = new PackageURL(purlString);
- Assert.assertEquals(cpurlString, purl.canonicalize());
Assert.assertEquals("pkg", purl.getScheme());
Assert.assertEquals(type, purl.getType());
@@ -91,18 +95,20 @@ public void testConstructorParsing() throws Exception {
if (qualifiers != null) {
Assert.assertNotNull(purl.getQualifiers());
Assert.assertEquals(qualifiers.length(), purl.getQualifiers().size());
- for (String key: qualifiers.keySet()) {
+ qualifiers.keySet().forEach((key) -> {
String value = qualifiers.getString(key);
Assert.assertTrue(purl.getQualifiers().containsKey(key));
Assert.assertEquals(value, purl.getQualifiers().get(key));
- }
+ });
}
+ Assert.assertEquals(cpurlString, purl.canonicalize());
}
}
@Test
@SuppressWarnings("unchecked")
public void testConstructorParameters() throws MalformedPackageURLException {
+ exception = ExpectedException.none();
for (int i = 0; i < json.length(); i++) {
JSONObject testDefinition = json.getJSONObject(i);
@@ -122,16 +128,16 @@ public void testConstructorParameters() throws MalformedPackageURLException {
TreeMap