@@ -94,7 +94,6 @@ public PackageURL(final String type, final String name) throws MalformedPackageU
9494 public PackageURL (final String type , final String namespace , final String name , final String version ,
9595 final TreeMap <String , String > qualifiers , final String subpath )
9696 throws MalformedPackageURLException {
97-
9897 this .scheme = validateScheme ("pkg" );
9998 this .type = toLowerCase (validateType (type ));
10099 this .namespace = validateNamespace (namespace );
@@ -106,9 +105,14 @@ public PackageURL(final String type, final String namespace, final String name,
106105 }
107106
108107 /**
109- * The PackageURL scheme constant
108+ * The PackageURL scheme constant.
109+ */
110+ public static final String SCHEME = "pkg" ;
111+
112+ /**
113+ * The PackageURL scheme ({@code "pkg"}) constant followed by a colon ({@code ':'}).
110114 */
111- private String scheme ;
115+ private static final String SCHEME_PART = SCHEME + ':' ;
112116
113117 /**
114118 * The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc.
@@ -170,7 +174,7 @@ public PackageURLBuilder toBuilder() {
170174 * @since 1.0.0
171175 */
172176 public String getScheme () {
173- return scheme ;
177+ return SCHEME ;
174178 }
175179
176180 /**
@@ -233,11 +237,10 @@ public String getSubpath() {
233237 return subpath ;
234238 }
235239
236- private String validateScheme (final String value ) throws MalformedPackageURLException {
237- if ("pkg" .equals (value )) {
238- return "pkg" ;
239- }
240- throw new MalformedPackageURLException ("The PackageURL scheme is invalid" );
240+ private void validateScheme (final String value ) throws MalformedPackageURLException {
241+ if (!SCHEME .equals (value )) {
242+ throw new MalformedPackageURLException ("The PackageURL scheme '" + value + "' is invalid. It should be '" + SCHEME + "'" );
243+ }
241244 }
242245
243246 private String validateType (final String value ) throws MalformedPackageURLException {
@@ -364,8 +367,8 @@ private String validatePath(final String[] segments, final boolean isSubpath) th
364367 }
365368 return segment ;
366369 }).collect (Collectors .joining ("/" ));
367- } catch (ValidationException ex ) {
368- throw new MalformedPackageURLException (ex . getMessage () );
370+ } catch (ValidationException e ) {
371+ throw new MalformedPackageURLException (e );
369372 }
370373 }
371374
@@ -398,7 +401,7 @@ public String canonicalize() {
398401 */
399402 private String canonicalize (boolean coordinatesOnly ) {
400403 final StringBuilder purl = new StringBuilder ();
401- purl .append (scheme ). append ( ":" );
404+ purl .append (SCHEME_PART );
402405 if (type != null ) {
403406 purl .append (type );
404407 }
@@ -563,78 +566,79 @@ public static String uriDecode(String source) {
563566 */
564567 private void parse (final String purl ) throws MalformedPackageURLException {
565568 if (purl == null || purl .trim ().isEmpty ()) {
566- throw new MalformedPackageURLException ("Invalid purl: Contains an empty or null value " );
569+ throw new MalformedPackageURLException ("Invalid purl: Is empty or null" );
567570 }
568571
569572 try {
570- final URI uri = new URI (purl );
571- // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
572- if (uri .getUserInfo () != null || uri .getPort () != -1 ) {
573- throw new MalformedPackageURLException ("Invalid purl: Contains parts not supported by the purl spec" );
573+ if (!purl .startsWith (SCHEME_PART )) {
574+ throw new MalformedPackageURLException ("Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'" );
574575 }
575576
576- this .scheme = validateScheme (uri .getScheme ());
577+ final int length = purl .length ();
578+ int start = SCHEME_PART .length ();
577579
578- // subpath is optional - check for existence
579- if (uri .getRawFragment () != null && !uri .getRawFragment ().isEmpty ()) {
580- this .subpath = validatePath (parsePath (uri .getRawFragment (), true ), true );
580+ while (start < length && '/' == purl .charAt (start )) {
581+ start ++;
581582 }
582- // This is the purl (minus the scheme) that needs parsed.
583- final StringBuilder remainder = new StringBuilder (uri .getRawSchemeSpecificPart ());
584583
585- // qualifiers are optional - check for existence
586- int index = remainder .lastIndexOf ("?" );
587- if (index >= 0 ) {
588- this .qualifiers = parseQualifiers (remainder .substring (index + 1 ));
589- remainder .setLength (index );
584+ final URI uri = new URI (String .join ("/" , SCHEME_PART , purl .substring (start )));
585+
586+ validateScheme (uri .getScheme ());
587+
588+ // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
589+ if (uri .getRawAuthority () != null ) {
590+ throw new MalformedPackageURLException ("Invalid purl: A purl must NOT contain a URL Authority " );
591+ }
592+
593+ // subpath is optional - check for existence
594+ final String rawFragment = uri .getRawFragment ();
595+ if (rawFragment != null && !rawFragment .isEmpty ()) {
596+ this .subpath = validatePath (parsePath (rawFragment , true ), true );
590597 }
598+ // qualifiers are optional - check for existence
599+ final String rawQuery = uri .getRawQuery ();
600+ if (rawQuery != null && !rawQuery .isEmpty ()) {
601+ this .qualifiers = parseQualifiers (rawQuery );
591602
592- // trim leading and trailing '/'
603+ }
604+ // this is the rest of the purl that needs to be parsed
605+ String remainder = uri .getRawPath ();
606+ // trim trailing '/'
593607 int end = remainder .length () - 1 ;
594608 while (end > 0 && '/' == remainder .charAt (end )) {
595609 end --;
596610 }
597- if (end < remainder .length () - 1 ) {
598- remainder .setLength (end + 1 );
599- }
600- int start = 0 ;
601- while (start < remainder .length () && '/' == remainder .charAt (start )) {
602- start ++;
603- }
604- //there is no need for the "expensive" delete operation if the start is tracked and used throughout the rest
605- // of the parsing.
606- //if (start > 0) {
607- // remainder.delete(0, start);
608- //}
609-
611+ remainder = remainder .substring (0 , end + 1 );
612+ // there is exactly one leading '/' at this point
613+ start = 1 ;
610614 // type
611- index = remainder .indexOf ("/" , start );
615+ int index = remainder .indexOf ('/' , start );
612616 if (index <= start ) {
613617 throw new MalformedPackageURLException ("Invalid purl: does not contain both a type and name" );
614618 }
615619 this .type = toLowerCase (validateType (remainder .substring (start , index )));
616- //remainder.delete(0, index + 1);
620+
617621 start = index + 1 ;
618622
619623 // version is optional - check for existence
620- index = remainder .lastIndexOf ("@" );
624+ index = remainder .lastIndexOf ('@' );
621625 if (index >= start ) {
622626 this .version = validateVersion (percentDecode (remainder .substring (index + 1 )));
623- remainder . setLength ( index );
627+ remainder = remainder . substring ( 0 , index );
624628 }
625629
626- // The 'remainder' should now consist of the an optional namespace, and the name
627- index = remainder .lastIndexOf ("/" );
630+ // The 'remainder' should now consist of an optional namespace and the name
631+ index = remainder .lastIndexOf ('/' );
628632 if (index <= start ) {
629633 this .name = validateName (percentDecode (remainder .substring (start )));
630634 } else {
631635 this .name = validateName (percentDecode (remainder .substring (index + 1 )));
632- remainder . setLength ( index );
636+ remainder = remainder . substring ( 0 , index );
633637 this .namespace = validateNamespace (parsePath (remainder .substring (start ), false ));
634638 }
635639 verifyTypeConstraints (this .type , this .namespace , this .name );
636640 } catch (URISyntaxException e ) {
637- throw new MalformedPackageURLException ("Invalid purl: " + e .getMessage ());
641+ throw new MalformedPackageURLException ("Invalid purl: " + e .getMessage (), e );
638642 }
639643 }
640644
@@ -669,8 +673,8 @@ private Map<String, String> parseQualifiers(final String encodedString) throws M
669673 },
670674 TreeMap <String , String >::putAll );
671675 return validateQualifiers (results );
672- } catch (ValidationException ex ) {
673- throw new MalformedPackageURLException (ex . getMessage () );
676+ } catch (ValidationException e ) {
677+ throw new MalformedPackageURLException (e );
674678 }
675679 }
676680
@@ -717,8 +721,7 @@ public boolean isBaseEquals(final PackageURL purl) {
717721 * @since 1.4.0
718722 */
719723 public boolean isCoordinatesEquals (final PackageURL purl ) {
720- return Objects .equals (scheme , purl .scheme ) &&
721- Objects .equals (type , purl .type ) &&
724+ return Objects .equals (type , purl .type ) &&
722725 Objects .equals (namespace , purl .namespace ) &&
723726 Objects .equals (name , purl .name ) &&
724727 Objects .equals (version , purl .version );
@@ -753,8 +756,7 @@ public boolean equals(Object o) {
753756 if (this == o ) return true ;
754757 if (o == null || getClass () != o .getClass ()) return false ;
755758 final PackageURL other = (PackageURL ) o ;
756- return Objects .equals (scheme , other .scheme ) &&
757- Objects .equals (type , other .type ) &&
759+ return Objects .equals (type , other .type ) &&
758760 Objects .equals (namespace , other .namespace ) &&
759761 Objects .equals (name , other .name ) &&
760762 Objects .equals (version , other .version ) &&
@@ -764,7 +766,7 @@ public boolean equals(Object o) {
764766
765767 @ Override
766768 public int hashCode () {
767- return Objects .hash (scheme , type , namespace , name , version , qualifiers , subpath );
769+ return Objects .hash (type , namespace , name , version , qualifiers , subpath );
768770 }
769771
770772 /**
0 commit comments