@@ -94,8 +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-
98- this .scheme = validateScheme ("pkg" );
9997 this .type = validateType (type );
10098 this .namespace = validateNamespace (namespace );
10199 this .name = validateName (name );
@@ -106,9 +104,14 @@ public PackageURL(final String type, final String namespace, final String name,
106104 }
107105
108106 /**
109- * The PackageURL scheme constant
107+ * The PackageURL scheme constant.
108+ */
109+ public static final String SCHEME = "pkg" ;
110+
111+ /**
112+ * The PackageURL scheme ({@code "pkg"}) constant followed by a colon ({@code ':'}).
110113 */
111- private String scheme ;
114+ private static final String SCHEME_PART = SCHEME + ':' ;
112115
113116 /**
114117 * The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc.
@@ -170,7 +173,7 @@ public PackageURLBuilder toBuilder() {
170173 * @since 1.0.0
171174 */
172175 public String getScheme () {
173- return scheme ;
176+ return SCHEME ;
174177 }
175178
176179 /**
@@ -233,11 +236,10 @@ public String getSubpath() {
233236 return subpath ;
234237 }
235238
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" );
239+ private void validateScheme (final String value ) throws MalformedPackageURLException {
240+ if (!SCHEME .equals (value )) {
241+ throw new MalformedPackageURLException ("The PackageURL scheme '" + value + "' is invalid. It should be '" + SCHEME + "'" );
242+ }
241243 }
242244
243245 private String validateType (final String value ) throws MalformedPackageURLException {
@@ -397,7 +399,7 @@ public String canonicalize() {
397399 */
398400 private String canonicalize (boolean coordinatesOnly ) {
399401 final StringBuilder purl = new StringBuilder ();
400- purl .append (scheme ). append ( ":" );
402+ purl .append (SCHEME_PART );
401403 if (type != null ) {
402404 purl .append (type );
403405 }
@@ -519,73 +521,73 @@ public static String uriDecode(String source) {
519521 */
520522 private void parse (final String purl ) throws MalformedPackageURLException {
521523 if (purl == null || purl .trim ().isEmpty ()) {
522- throw new MalformedPackageURLException ("Invalid purl: Contains an empty or null value " );
524+ throw new MalformedPackageURLException ("Invalid purl: Is empty or null" );
523525 }
524526
525527 try {
526- final URI uri = new URI (purl );
527- // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
528- if (uri .getUserInfo () != null || uri .getPort () != -1 ) {
529- throw new MalformedPackageURLException ("Invalid purl: Contains parts not supported by the purl spec" );
528+ if (!purl .startsWith (SCHEME_PART )) {
529+ throw new MalformedPackageURLException ("Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'" );
530530 }
531531
532- this .scheme = validateScheme (uri .getScheme ());
532+ final int length = purl .length ();
533+ int start = SCHEME_PART .length ();
533534
534- // subpath is optional - check for existence
535- if (uri .getRawFragment () != null && !uri .getRawFragment ().isEmpty ()) {
536- this .subpath = validatePath (parsePath (uri .getRawFragment (), true ), true );
535+ while (start < length && '/' == purl .charAt (start )) {
536+ start ++;
537537 }
538- // This is the purl (minus the scheme) that needs parsed.
539- final StringBuilder remainder = new StringBuilder (uri .getRawSchemeSpecificPart ());
540538
541- // qualifiers are optional - check for existence
542- int index = remainder .lastIndexOf ("?" );
543- if (index >= 0 ) {
544- this .qualifiers = parseQualifiers (remainder .substring (index + 1 ));
545- remainder .setLength (index );
539+ final URI uri = new URI (String .join ("/" , SCHEME_PART , purl .substring (start )));
540+
541+ validateScheme (uri .getScheme ());
542+
543+ // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
544+ if (uri .getRawAuthority () != null ) {
545+ throw new MalformedPackageURLException ("Invalid purl: A purl must NOT contain a URL Authority " );
546+ }
547+
548+ // subpath is optional - check for existence
549+ final String rawFragment = uri .getRawFragment ();
550+ if (rawFragment != null && !rawFragment .isEmpty ()) {
551+ this .subpath = validatePath (parsePath (rawFragment , true ), true );
546552 }
553+ // qualifiers are optional - check for existence
554+ final String rawQuery = uri .getRawQuery ();
555+ if (rawQuery != null && !rawQuery .isEmpty ()) {
556+ this .qualifiers = parseQualifiers (rawQuery );
547557
548- // trim leading and trailing '/'
558+ }
559+ // this is the rest of the purl that needs to be parsed
560+ String remainder = uri .getRawPath ();
561+ // trim trailing '/'
549562 int end = remainder .length () - 1 ;
550563 while (end > 0 && '/' == remainder .charAt (end )) {
551564 end --;
552565 }
553- if (end < remainder .length () - 1 ) {
554- remainder .setLength (end + 1 );
555- }
556- int start = 0 ;
557- while (start < remainder .length () && '/' == remainder .charAt (start )) {
558- start ++;
559- }
560- //there is no need for the "expensive" delete operation if the start is tracked and used throughout the rest
561- // of the parsing.
562- //if (start > 0) {
563- // remainder.delete(0, start);
564- //}
565-
566+ remainder = remainder .substring (0 , end + 1 );
567+ // there is exactly one leading '/' at this point
568+ start = 1 ;
566569 // type
567- index = remainder .indexOf ("/" , start );
570+ int index = remainder .indexOf ('/' , start );
568571 if (index <= start ) {
569572 throw new MalformedPackageURLException ("Invalid purl: does not contain both a type and name" );
570573 }
571574 this .type = validateType (remainder .substring (start , index ).toLowerCase ());
572- //remainder.delete(0, index + 1);
573575 start = index + 1 ;
574576
575577 // version is optional - check for existence
576- index = remainder .lastIndexOf ("@" );
578+ index = remainder .lastIndexOf ('@' );
577579 if (index >= start ) {
578580 this .version = validateVersion (percentDecode (remainder .substring (index + 1 )));
579- remainder . setLength ( index );
581+ remainder = remainder . substring ( 0 , index );
580582 }
581583
582- // The 'remainder' should now consist of the an optional namespace, and the name
583- index = remainder .lastIndexOf ("/" );
584+ // The 'remainder' should now consist of an optional namespace and the name
585+ index = remainder .lastIndexOf ('/' );
584586 if (index <= start ) {
585587 this .name = validateName (percentDecode (remainder .substring (start )));
586588 } else {
587589 this .name = validateName (percentDecode (remainder .substring (index + 1 )));
588- remainder . setLength ( index );
590+ remainder = remainder . substring ( 0 , index );
589591 this .namespace = validateNamespace (parsePath (remainder .substring (start ), false ));
590592 }
591593 verifyTypeConstraints (this .type , this .namespace , this .name );
@@ -672,8 +674,7 @@ public boolean isBaseEquals(final PackageURL purl) {
672674 * @since 1.4.0
673675 */
674676 public boolean isCoordinatesEquals (final PackageURL purl ) {
675- return Objects .equals (scheme , purl .scheme ) &&
676- Objects .equals (type , purl .type ) &&
677+ return Objects .equals (type , purl .type ) &&
677678 Objects .equals (namespace , purl .namespace ) &&
678679 Objects .equals (name , purl .name ) &&
679680 Objects .equals (version , purl .version );
@@ -708,8 +709,7 @@ public boolean equals(Object o) {
708709 if (this == o ) return true ;
709710 if (o == null || getClass () != o .getClass ()) return false ;
710711 final PackageURL other = (PackageURL ) o ;
711- return Objects .equals (scheme , other .scheme ) &&
712- Objects .equals (type , other .type ) &&
712+ return Objects .equals (type , other .type ) &&
713713 Objects .equals (namespace , other .namespace ) &&
714714 Objects .equals (name , other .name ) &&
715715 Objects .equals (version , other .version ) &&
@@ -719,7 +719,7 @@ public boolean equals(Object o) {
719719
720720 @ Override
721721 public int hashCode () {
722- return Objects .hash (scheme , type , namespace , name , version , qualifiers , subpath );
722+ return Objects .hash (type , namespace , name , version , qualifiers , subpath );
723723 }
724724
725725 /**
0 commit comments