diff --git a/pom.xml b/pom.xml index 315031c..db760d3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.github.package-url packageurl-java - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT jar Package URL @@ -51,6 +51,8 @@ 2.8.5 2.3.2 0.8.2 + 3.1.9 + 3.1.10 @@ -130,6 +132,27 @@ + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.maven.plugin.version} + + + package + + check + + + + + + + com.github.spotbugs + spotbugs + ${com.github.spotbugs.version} + + + org.jacoco jacoco-maven-plugin diff --git a/src/main/java/com/github/packageurl/MalformedPackageURLException.java b/src/main/java/com/github/packageurl/MalformedPackageURLException.java index 4ec900a..47f6f72 100644 --- a/src/main/java/com/github/packageurl/MalformedPackageURLException.java +++ b/src/main/java/com/github/packageurl/MalformedPackageURLException.java @@ -47,4 +47,4 @@ public MalformedPackageURLException(String msg) { super(msg); } -} \ No newline at end of file +} diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index c268296..8f73638 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** *

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 Map validateQualifiers(String encodedString) throws MalformedPackageURLException { - final Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - final String[] pairs = encodedString.split("&"); - for (String pair : pairs) { - if (pair.contains("=")) { - final String[] kvpair = pair.split("="); - if (kvpair.length == 2) { - map.put(validateQualifierKey(kvpair[0]), urldecode(kvpair[1])); - } + private Map validateQualifiers(Map values) throws MalformedPackageURLException { + if (values == null) { + return null; + } + for (Map.Entry entry : values.entrySet()) { + validateKey(entry.getKey()); + String value = entry.getValue(); + if (value == null || value.isEmpty()) { + throw new MalformedPackageURLException("The PackageURL specified contains a qualifier key with an empty or null value"); } } - return map; + return values; } - private Map validateQualifiers(Map qualifiers) throws MalformedPackageURLException { - if (qualifiers == null) { - return null; + private String validateKey(String value) throws MalformedPackageURLException { + if (value == null || value.isEmpty()) { + throw new MalformedPackageURLException("Qualifier key is invalid: " + value); } - for (String key : qualifiers.keySet()) { - validateQualifierKey(key); - if (qualifiers.get(key) == null) { - throw new MalformedPackageURLException("The PackageURL specified contains a qualifier key with a null value"); - } + String retValue = value.toLowerCase(); + if ((value.charAt(0) >= '0' && value.charAt(0) <= '9') + || !value.chars().allMatch(c -> (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_')) { + throw new MalformedPackageURLException("Qualifier key is invalid: " + value); } - return qualifiers; + return retValue; } - private String validateQualifierKey(String key) throws MalformedPackageURLException { - if (key == null || !KEY_PATTERN.matcher(key).matches()) { - throw new MalformedPackageURLException("The PackageURL specified contains a qualifier key name which is invalid"); + private String validatePath(String value, boolean isSubpath) throws MalformedPackageURLException { + if (value == null || value.isEmpty()) { + return null; } - return key; + return validatePath(value.split("/"), isSubpath); } - private String validateSubpath(String subpath) { - if (subpath == null) { + private String validatePath(String[] segments, boolean isSubpath) throws MalformedPackageURLException { + if (segments == null || segments.length == 0) { return null; } - return urldecode(stripLeadingAndTrailingSlash(subpath)); // leading and trailing slashes always need to be removed + try { + return Arrays.stream(segments) + .map(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"); + } + return segment; + }).collect(Collectors.joining("/")); + } catch (ValidationException ex) { + throw new MalformedPackageURLException(ex.getMessage()); + } } /** - * Returns a canonicalized representation of the purl. + * Returns the canonicalized representation of the purl. * - * @return a canonicalized representation of the purl + * @return the canonicalized representation of the purl + * @since 1.1.0 + */ + @Override + public String toString() { + return canonicalize(); + } + + /** + * Returns the canonicalized representation of the purl. + * + * @return the canonicalized representation of the purl * @since 1.0.0 */ public String canonicalize() { + if (canonicalizedForm != null) { + return canonicalizedForm; + } final StringBuilder purl = new StringBuilder(); purl.append(scheme).append(":"); if (type != null) { @@ -403,7 +385,7 @@ public String canonicalize() { } purl.append("/"); if (namespace != null) { - purl.append(urlencode(namespace)); + purl.append(encodePath(namespace)); purl.append("/"); } if (name != null) { @@ -414,37 +396,19 @@ public String canonicalize() { } if (qualifiers != null && qualifiers.size() > 0) { purl.append("?"); - for (Map.Entry entry : qualifiers.entrySet()) { + qualifiers.entrySet().stream().forEachOrdered((entry) -> { purl.append(entry.getKey().toLowerCase()); purl.append("="); purl.append(urlencode(entry.getValue())); - purl.append("&"); - } + purl.append("&"); + }); purl.setLength(purl.length() - 1); } if (subpath != null) { - purl.append("#").append(subpath); - } - return purl.toString(); - } - - /** - * Removes leading and trailing '/' characters from the specified input. - * - * @param input the String to remove leading and trailing '/' from - * @return the processed String - */ - private String stripLeadingAndTrailingSlash(String input) { - if (input == null) { - return null; - } - if (input.startsWith("/")) { - input = input.substring(1); + purl.append("#").append(encodePath(subpath)); } - if (input.endsWith("/")) { - input = input.substring(0, input.length() - 1); - } - return input; + canonicalizedForm = purl.toString(); + return canonicalizedForm; } /** @@ -456,7 +420,7 @@ private String stripLeadingAndTrailingSlash(String input) { private String urlencode(String input) { try { // This SHOULD encoded according to RFC-3986 because URLEncoder alone does not. - return URLEncoder.encode(input, StandardCharsets.UTF_8.name()) + return URLEncoder.encode(input, UTF8) .replace("+", "%20") .replace("%7E", "~"); } catch (UnsupportedEncodingException e) { @@ -476,7 +440,7 @@ private String urldecode(String input) { return null; } try { - final String decoded = URLDecoder.decode(input, StandardCharsets.UTF_8.name()); + final String decoded = URLDecoder.decode(input, UTF8); if (!decoded.equals(input)) { return decoded; } @@ -486,6 +450,125 @@ private String urldecode(String input) { return input; } + /** + * 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 || purl.trim().isEmpty()) { + 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()); + + // subpath is optional - check for existence + if (uri.getRawFragment() != null && !uri.getRawFragment().isEmpty()) { + this.subpath = validatePath(parsePath(uri.getRawFragment(), true), true); + } + // This is the purl (minus the scheme) that needs parsed. + StringBuilder remainder = new StringBuilder(uri.getRawSchemeSpecificPart()); + + // qualifiers are optional - check for existence + int index = remainder.lastIndexOf("?"); + if (index >= 0) { + this.qualifiers = parseQualifiers(remainder.substring(index + 1)); + remainder.setLength(index); + } + + // trim leading and trailing '/' + int end = remainder.length() - 1; + while (end > 0 && '/' == remainder.charAt(end)) { + end--; + } + if (end < remainder.length() - 1) { + remainder.setLength(end + 1); + } + int start = 0; + while (start < remainder.length() && '/' == remainder.charAt(start)) { + start++; + } + //there is no need for the "expensive" delete operation if the start is tracked and used throughout the rest + // of the parsing. + //if (start > 0) { + // remainder.delete(0, start); + //} + + // type + index = remainder.indexOf("/", start); + if (index <= start) { + throw new MalformedPackageURLException("Invalid purl: does not contain both a type and name"); + } + this.type = validateType(remainder.substring(start, index).toLowerCase()); + //remainder.delete(0, index + 1); + start = index + 1; + + // version is optional - check for existence + index = remainder.lastIndexOf("@"); + if (index >= start) { + this.version = validateVersion(urldecode(remainder.substring(index + 1))); + remainder.setLength(index); + } + + // The 'remainder' should now consist of the an optional namespace, and the name + index = remainder.lastIndexOf("/"); + if (index <= start) { + this.name = validateName(urldecode(remainder.substring(start))); + } else { + this.name = validateName(urldecode(remainder.substring(index + 1))); + remainder.setLength(index); + this.namespace = validateNamespace(parsePath(remainder.substring(start), false)); + } + } catch (URISyntaxException e) { + throw new MalformedPackageURLException("Invalid purl: " + e.getMessage()); + } + } + + @SuppressWarnings("StringSplitter")//reason: surprising behavior is okay in this case + private Map parseQualifiers(String encodedString) throws MalformedPackageURLException { + try { + TreeMap results = Arrays.stream(encodedString.split("&")) + .collect(TreeMap::new, + (map, value) -> { + String[] entry = value.split("=", 2); + if (entry.length == 2 && !entry[1].isEmpty()) { + if (map.put(entry[0].toLowerCase(), urldecode(entry[1])) != null) { + throw new ValidationException("Duplicate package qualifier encountere - more then one value was specified for " + entry[0].toLowerCase()); + } + } + }, + TreeMap::putAll); + return validateQualifiers(results); + } catch (ValidationException ex) { + throw new MalformedPackageURLException(ex.getMessage()); + } + } + + @SuppressWarnings("StringSplitter")//reason: surprising behavior is okay in this case + private String[] parsePath(String value, boolean isSubpath) throws MalformedPackageURLException { + if (value == null || value.isEmpty()) { + return null; + } + return PATH_SPLITTER.splitAsStream(value) + .filter(segment -> !segment.isEmpty() && !(isSubpath && (".".equals(segment) || "..".equals(segment)))) + .map(segment -> urldecode(segment)) + .toArray(String[]::new); + } + + private String encodePath(String path) { + return Arrays.stream(path.split("/")).map(segment -> urlencode(segment)).collect(Collectors.joining("/")); + } + /** * Convenience constants that defines common Package-URL 'type's. * diff --git a/src/main/java/com/github/packageurl/PackageURLBuilder.java b/src/main/java/com/github/packageurl/PackageURLBuilder.java index 0f66707..72dd7fc 100644 --- a/src/main/java/com/github/packageurl/PackageURLBuilder.java +++ b/src/main/java/com/github/packageurl/PackageURLBuilder.java @@ -60,19 +60,6 @@ public PackageURLBuilder withType(String type) { return this; } - /** - * Adds the package URL type. - * - * @param type the package type - * @return a reference to the builder - * @see PackageURL#getName() - * @see com.github.packageurl.PackageURL.StandardTypes - */ - public PackageURLBuilder withType(PackageURL.StandardTypes type) { - this.type = type.toString(); - return this; - } - /** * Adds the package namespace. * diff --git a/src/main/java/com/github/packageurl/ValidationException.java b/src/main/java/com/github/packageurl/ValidationException.java new file mode 100644 index 0000000..4cb3ee9 --- /dev/null +++ b/src/main/java/com/github/packageurl/ValidationException.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.github.packageurl; + +/** + * Internal exception class intended to be used within validation contained in lambda expressions. + * + * @author Jeremy Long + * @since 1.1.0 + */ +class ValidationException extends RuntimeException { + + private static final long serialVersionUID = 2045474478691037663L; + + /** + * Constructs a {@code ValidationException}. + * @param msg the error message + */ + ValidationException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/github/packageurl/package-info.java b/src/main/java/com/github/packageurl/package-info.java index 9dc97b7..6a7436c 100644 --- a/src/main/java/com/github/packageurl/package-info.java +++ b/src/main/java/com/github/packageurl/package-info.java @@ -1,6 +1,6 @@ /** - * Java implementation of the Package-URL Specification: - * https://github.com/package-url/purl-spec + *

Java 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 map = null; if (qualifiers != null) { map = qualifiers.toMap().entrySet().stream().collect( - TreeMap::new, - (qmap, entry) -> qmap.put(entry.getKey(), (String)entry.getValue()), - TreeMap::putAll + TreeMap::new, + (qmap, entry) -> qmap.put(entry.getKey(), (String) entry.getValue()), + TreeMap::putAll ); } if (invalid) { try { - new PackageURL(type, namespace, name, version, map, subpath); - Assert.fail(); + PackageURL purl = new PackageURL(type, namespace, name, version, map, subpath); + Assert.fail("Invalid package url components should have caused an exception: " + purl.toString()); } catch (MalformedPackageURLException e) { Assert.assertNotNull(e.getMessage()); } @@ -150,17 +156,109 @@ public void testConstructorParameters() throws MalformedPackageURLException { 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)); - } + }); } } } + @Test + public void testConstructor() throws MalformedPackageURLException { + exception = ExpectedException.none(); + + PackageURL purl = new PackageURL("pkg:generic/namespace/name@1.0.0#"); + Assert.assertEquals("generic", purl.getType()); + Assert.assertNull(purl.getSubpath()); + + purl = new PackageURL("pkg:generic/namespace/name@1.0.0?key=value=="); + Assert.assertEquals("generic", purl.getType()); + Assert.assertEquals(1, purl.getQualifiers().size()); + Assert.assertTrue(purl.getQualifiers().containsValue("value==")); + + purl = new PackageURL("validtype", "name"); + Assert.assertNotNull(purl); + + } + + @Test + public void testConstructorException1() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("", "name"); + Assert.fail("constructor with an empty type should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException2() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("invalid^type", "name"); + Assert.fail("constructor with `invalid^type` should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException3() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("pkg:GOLANG/google.golang.org/genproto@abcdedf#invalid/%2F/subpath"); + Assert.fail("constructor with `invalid/%2F/subpath` should have thrown an error and this line should not be reached"); + } + + + @Test + public void testConstructorException4() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL(null); + Assert.fail("constructor with null purl should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException5() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL(""); + Assert.fail("constructor with empty purl should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException6() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("pkg://generic:8080/name"); + Assert.fail("constructor with port number should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException7() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("pkg://user@generic/name"); + Assert.fail("constructor with username number should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException8() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("invalid url"); + Assert.fail("constructor with invalid url should have thrown an error and this line should not be reached"); + } + + @Test + public void testConstructorException9() throws MalformedPackageURLException { + exception.expect(MalformedPackageURLException.class); + + PackageURL purl = new PackageURL("pkg://generic/name?key=one&key=two"); + Assert.fail("constructor with username number should have thrown an error and this line should not be reached"); + } + @Test public void testStandardTypes() { + exception = ExpectedException.none(); Assert.assertEquals(PackageURL.StandardTypes.BITBUCKET, "bitbucket"); Assert.assertEquals(PackageURL.StandardTypes.COMPOSER, "composer"); Assert.assertEquals(PackageURL.StandardTypes.DEBIAN, "deb"); @@ -175,5 +273,4 @@ public void testStandardTypes() { Assert.assertEquals(PackageURL.StandardTypes.PYPI, "pypi"); Assert.assertEquals(PackageURL.StandardTypes.RPM, "rpm"); } - }