Skip to content

Commit b2f26f8

Browse files
authored
fix: allow non-lowercase qualifier keys (#169)
1 parent dd8309b commit b2f26f8

File tree

3 files changed

+96
-26
lines changed

3 files changed

+96
-26
lines changed

src/main/java/com/github/packageurl/PackageURL.java

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.Map;
3232
import java.util.Objects;
3333
import java.util.TreeMap;
34+
import java.util.function.IntPredicate;
3435
import java.util.regex.Pattern;
3536
import java.util.stream.Collectors;
3637

@@ -98,7 +99,7 @@ public PackageURL(final String type, final String namespace, final String name,
9899
this.namespace = validateNamespace(namespace);
99100
this.name = validateName(name);
100101
this.version = validateVersion(version);
101-
this.qualifiers = validateQualifiers(qualifiers);
102+
this.qualifiers = parseQualifiers(qualifiers);
102103
this.subpath = validatePath(subpath, true);
103104
verifyTypeConstraints(this.type, this.namespace, this.name);
104105
}
@@ -223,7 +224,7 @@ public String getVersion() {
223224
* @since 1.0.0
224225
*/
225226
public Map<String, String> getQualifiers() {
226-
return (qualifiers != null)? Collections.unmodifiableMap(qualifiers) : null;
227+
return (qualifiers != null) ? Collections.unmodifiableMap(qualifiers) : null;
227228
}
228229

229230
/**
@@ -247,18 +248,31 @@ private String validateType(final String value) throws MalformedPackageURLExcept
247248
throw new MalformedPackageURLException("The PackageURL type cannot be null or empty");
248249
}
249250

250-
if (isDigit(value.charAt(0))) {
251-
throw new MalformedPackageURLException("The PackageURL type cannot start with a number");
252-
}
251+
validateChars(value, PackageURL::isValidCharForType, "type");
252+
253+
return value;
254+
}
255+
256+
private static boolean isValidCharForType(int c) {
257+
return (isAlphaNumeric(c) || c == '.' || c == '+' || c == '-');
258+
}
253259

254-
if (!value.chars().allMatch(c -> (c == '.' || c == '+' || c == '-'
255-
|| isUpperCase(c)
256-
|| isLowerCase(c)
257-
|| isDigit(c)))) {
258-
throw new MalformedPackageURLException("The PackageURL type contains invalid characters");
260+
private static boolean isValidCharForKey(int c) {
261+
return (isAlphaNumeric(c) || c == '.' || c == '_' || c == '-');
262+
}
263+
264+
private static void validateChars(String value, IntPredicate predicate, String component) throws MalformedPackageURLException {
265+
char firstChar = value.charAt(0);
266+
267+
if (isDigit(firstChar)) {
268+
throw new MalformedPackageURLException("The PackageURL " + component + " cannot start with a number: " + firstChar);
259269
}
260270

261-
return value;
271+
String invalidChars = value.chars().filter(predicate.negate()).mapToObj(c -> String.valueOf((char) c)).collect(Collectors.joining(", "));
272+
273+
if (!invalidChars.isEmpty()) {
274+
throw new MalformedPackageURLException("The PackageURL " + component + " '" + value + "' contains invalid characters: " + invalidChars);
275+
}
262276
}
263277

264278
private String validateNamespace(final String value) throws MalformedPackageURLException {
@@ -319,7 +333,7 @@ private String validateVersion(final String value) {
319333
}
320334

321335
private Map<String, String> validateQualifiers(final Map<String, String> values) throws MalformedPackageURLException {
322-
if (values == null) {
336+
if (values == null || values.isEmpty()) {
323337
return null;
324338
}
325339
for (Map.Entry<String, String> entry : values.entrySet()) {
@@ -337,10 +351,7 @@ private void validateKey(final String value) throws MalformedPackageURLException
337351
throw new MalformedPackageURLException("Qualifier key is invalid: " + value);
338352
}
339353

340-
if (isDigit(value.charAt(0))
341-
|| !value.chars().allMatch(c -> isLowerCase(c) || (isDigit(c)) || c == '.' || c == '-' || c == '_')) {
342-
throw new MalformedPackageURLException("Qualifier key is invalid: " + value);
343-
}
354+
validateChars(value, PackageURL::isValidCharForKey, "qualifier key");
344355
}
345356

346357
private String validatePath(final String value, final boolean isSubpath) throws MalformedPackageURLException {
@@ -463,7 +474,7 @@ private static String uriEncode(String source, Charset charset) {
463474
}
464475

465476
private static boolean isUnreserved(int c) {
466-
return (isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c);
477+
return (isValidCharForKey(c) || c == '~');
467478
}
468479

469480
private static boolean isAlpha(int c) {
@@ -474,6 +485,10 @@ private static boolean isDigit(int c) {
474485
return (c >= '0' && c <= '9');
475486
}
476487

488+
private static boolean isAlphaNumeric(int c) {
489+
return (isDigit(c) || isAlpha(c));
490+
}
491+
477492
private static boolean isUpperCase(int c) {
478493
return (c >= 'A' && c <= 'Z');
479494
}
@@ -656,6 +671,23 @@ private void verifyTypeConstraints(String type, String namespace, String name) t
656671
}
657672
}
658673

674+
private Map<String, String> parseQualifiers(final Map<String, String> qualifiers) throws MalformedPackageURLException {
675+
if (qualifiers == null || qualifiers.isEmpty()) {
676+
return null;
677+
}
678+
679+
try {
680+
final TreeMap<String, String> results = qualifiers.entrySet().stream()
681+
.filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
682+
.collect(TreeMap::new,
683+
(map, value) -> map.put(toLowerCase(value.getKey()), value.getValue()),
684+
TreeMap::putAll);
685+
return validateQualifiers(results);
686+
} catch (ValidationException ex) {
687+
throw new MalformedPackageURLException(ex.getMessage());
688+
}
689+
}
690+
659691
@SuppressWarnings("StringSplitter")//reason: surprising behavior is okay in this case
660692
private Map<String, String> parseQualifiers(final String encodedString) throws MalformedPackageURLException {
661693
try {

src/test/java/com/github/packageurl/PackageURLBuilderTest.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,26 +95,22 @@ public void testPackageURLBuilder() throws MalformedPackageURLException {
9595

9696
@Test
9797
public void testPackageURLBuilderException1() throws MalformedPackageURLException {
98-
exception.expect(MalformedPackageURLException.class);
99-
exception.expectMessage("contains a qualifier key with an empty or null");
10098
PackageURL purl = PackageURLBuilder.aPackageURL()
10199
.withType("type")
102100
.withName("name")
103101
.withQualifier("key","")
104102
.build();
105-
Assert.fail("Build should fail due to invalid qualifier (empty value)");
103+
assertNull(purl.getQualifiers());
106104
}
107105

108106
@Test
109107
public void testPackageURLBuilderException1Null() throws MalformedPackageURLException {
110-
exception.expect(MalformedPackageURLException.class);
111-
exception.expectMessage("contains a qualifier key with an empty or null");
112-
PackageURLBuilder.aPackageURL()
108+
PackageURL purl = PackageURLBuilder.aPackageURL()
113109
.withType("type")
114110
.withName("name")
115111
.withQualifier("key",null)
116112
.build();
117-
Assert.fail("Build should fail due to invalid qualifier (null value)");
113+
assertNull(purl.getQualifiers());
118114
}
119115

120116
@Test
@@ -216,4 +212,4 @@ private void assertBuilderMatch(PackageURL expected, PackageURLBuilder actual) t
216212

217213
}
218214

219-
}
215+
}

src/test/java/com/github/packageurl/PackageURLTest.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323

2424
import java.io.IOException;
2525
import java.io.InputStream;
26+
import java.util.Locale;
2627
import java.util.TreeMap;
2728

2829
import org.apache.commons.io.IOUtils;
2930
import org.json.JSONArray;
3031
import org.json.JSONObject;
32+
import org.junit.AfterClass;
3133
import org.junit.Assert;
3234
import org.junit.BeforeClass;
3335
import org.junit.Rule;
@@ -42,17 +44,25 @@
4244
* @author Steve Springett
4345
*/
4446
public class PackageURLTest {
45-
4647
@Rule
4748
public ExpectedException exception = ExpectedException.none();
4849

4950
private static JSONArray json = new JSONArray();
5051

52+
private static Locale defaultLocale;
53+
5154
@BeforeClass
5255
public static void setup() throws IOException {
5356
InputStream is = PackageURLTest.class.getResourceAsStream("/test-suite-data.json");
5457
String jsonTxt = IOUtils.toString(is, "UTF-8");
5558
json = new JSONArray(jsonTxt);
59+
defaultLocale = Locale.getDefault();
60+
Locale.setDefault(new Locale("tr"));
61+
}
62+
63+
@AfterClass
64+
public static void resetLocale() {
65+
Locale.setDefault(defaultLocale);
5666
}
5767

5868
@Test
@@ -266,6 +276,38 @@ public void testConstructorWithDuplicateQualifiers() throws MalformedPackageURLE
266276
Assert.fail("constructor with url with duplicate qualifiers should have thrown an error and this line should not be reached");
267277
}
268278

279+
@Test
280+
public void testConstructorDuplicateQualifiersMixedCase() throws MalformedPackageURLException {
281+
exception.expect(MalformedPackageURLException.class);
282+
283+
PackageURL purl = new PackageURL("pkg://generic/name?key=one&KEY=two");
284+
Assert.fail("constructor with url with duplicate qualifiers should have thrown an error and this line should not be reached");
285+
}
286+
287+
@Test
288+
public void testConstructorWithUppercaseKey() throws MalformedPackageURLException {
289+
PackageURL purl = new PackageURL("pkg://generic/name?KEY=one");
290+
Assert.assertNotNull(purl.getQualifiers());
291+
Assert.assertEquals("one", purl.getQualifiers().get("key"));
292+
TreeMap<String, String> qualifiers = new TreeMap<>();
293+
qualifiers.put("key", "one");
294+
PackageURL purl2 = new PackageURL("generic", null, "name", null, qualifiers, null);
295+
Assert.assertEquals(purl, purl2);
296+
}
297+
298+
@Test
299+
public void testConstructorWithEmptyKey() throws MalformedPackageURLException {
300+
PackageURL purl = new PackageURL("pkg://generic/name?KEY");
301+
Assert.assertNull(purl.getQualifiers());
302+
TreeMap<String, String> qualifiers = new TreeMap<>();
303+
qualifiers.put("KEY", null);
304+
PackageURL purl2 = new PackageURL("generic", null, "name", null, qualifiers, null);
305+
Assert.assertEquals(purl, purl2);
306+
qualifiers.put("KEY", "");
307+
PackageURL purl3 = new PackageURL("generic", null, "name", null, qualifiers, null);
308+
Assert.assertEquals(purl2, purl3);
309+
}
310+
269311
@Test
270312
public void testStandardTypes() {
271313
exception = ExpectedException.none();

0 commit comments

Comments
 (0)