3131import java .util .Map ;
3232import java .util .Objects ;
3333import java .util .TreeMap ;
34+ import java .util .function .IntPredicate ;
3435import java .util .regex .Pattern ;
3536import 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 {
0 commit comments