@@ -98,7 +98,7 @@ public PackageURL(final String type, final String namespace, final String name,
9898 this .namespace = validateNamespace (namespace );
9999 this .name = validateName (name );
100100 this .version = validateVersion (version );
101- this .qualifiers = validateQualifiers (qualifiers );
101+ this .qualifiers = parseQualifiers (qualifiers );
102102 this .subpath = validatePath (subpath , true );
103103 verifyTypeConstraints (this .type , this .namespace , this .name );
104104 }
@@ -223,7 +223,7 @@ public String getVersion() {
223223 * @since 1.0.0
224224 */
225225 public Map <String , String > getQualifiers () {
226- return (qualifiers != null )? Collections .unmodifiableMap (qualifiers ) : null ;
226+ return (qualifiers != null ) ? Collections .unmodifiableMap (qualifiers ) : null ;
227227 }
228228
229229 /**
@@ -247,15 +247,17 @@ private String validateType(final String value) throws MalformedPackageURLExcept
247247 throw new MalformedPackageURLException ("The PackageURL type cannot be null or empty" );
248248 }
249249
250- if (isDigit (value .charAt (0 ))) {
251- throw new MalformedPackageURLException ("The PackageURL type cannot start with a number" );
250+ char firstChar = value .charAt (0 );
251+
252+ if (isDigit (firstChar )) {
253+ throw new MalformedPackageURLException ("The PackageURL type cannot start with a number: " + firstChar );
252254 }
253255
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" );
256+ String invalidChars = value .chars ().filter (c -> ! (c == '.' || c == '+' || c == '-'
257+ || isAlphaNumeric (c ))). mapToObj ( c -> String . valueOf (( char ) c )). collect ( Collectors . joining ( ", " ));
258+
259+ if (! invalidChars . isEmpty ( )) {
260+ throw new MalformedPackageURLException ("The PackageURL type " + "'" + value + "' contains invalid characters: " + invalidChars );
259261 }
260262
261263 return value ;
@@ -319,7 +321,7 @@ private String validateVersion(final String value) {
319321 }
320322
321323 private Map <String , String > validateQualifiers (final Map <String , String > values ) throws MalformedPackageURLException {
322- if (values == null ) {
324+ if (values == null || values . isEmpty () ) {
323325 return null ;
324326 }
325327 for (Map .Entry <String , String > entry : values .entrySet ()) {
@@ -337,9 +339,17 @@ private void validateKey(final String value) throws MalformedPackageURLException
337339 throw new MalformedPackageURLException ("Qualifier key is invalid: " + value );
338340 }
339341
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 );
342+ char firstChar = value .charAt (0 );
343+
344+ if (isDigit (firstChar )) {
345+ throw new MalformedPackageURLException ("The PackageURL type cannot start with a number: " + firstChar );
346+ }
347+
348+ String invalidChars = value .chars ().filter (c -> !(c == '.' || c == '-' || c == '_'
349+ || isAlphaNumeric (c ))).mapToObj (c -> String .valueOf ((char ) c )).collect (Collectors .joining (", " ));
350+
351+ if (!invalidChars .isEmpty ()) {
352+ throw new MalformedPackageURLException ("The PackageURL qualifier key " + "'" + value + "' contains invalid characters: " + invalidChars );
343353 }
344354 }
345355
@@ -463,7 +473,7 @@ private static String uriEncode(String source, Charset charset) {
463473 }
464474
465475 private static boolean isUnreserved (int c ) {
466- return (isAlpha ( c ) || isDigit (c ) || '-' == c || '.' == c || '_' == c || '~' == c );
476+ return (isAlphaNumeric (c ) || '-' == c || '.' == c || '_' == c || '~' == c );
467477 }
468478
469479 private static boolean isAlpha (int c ) {
@@ -474,6 +484,10 @@ private static boolean isDigit(int c) {
474484 return (c >= '0' && c <= '9' );
475485 }
476486
487+ private static boolean isAlphaNumeric (int c ) {
488+ return (isDigit (c ) || isAlpha (c ));
489+ }
490+
477491 private static boolean isUpperCase (int c ) {
478492 return (c >= 'A' && c <= 'Z' );
479493 }
@@ -656,6 +670,24 @@ private void verifyTypeConstraints(String type, String namespace, String name) t
656670 }
657671 }
658672
673+ private Map <String , String > parseQualifiers (final Map <String , String > qualifiers ) throws MalformedPackageURLException {
674+ if (qualifiers == null || qualifiers .isEmpty ()) {
675+ return null ;
676+ }
677+
678+ try {
679+ final TreeMap <String , String > results = qualifiers .entrySet ().stream ()
680+ .filter (entry -> entry .getValue () != null && !entry .getValue ().isEmpty ())
681+ .collect (TreeMap ::new ,
682+ (map , value ) -> map .put (toLowerCase (value .getKey ()), value .getValue ()),
683+ TreeMap ::putAll );
684+ return validateQualifiers (results );
685+ } catch (ValidationException ex ) {
686+ throw new MalformedPackageURLException (ex .getMessage ());
687+ }
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