@@ -74,37 +74,37 @@ public final class PackageURL implements Serializable {
7474 * The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc.
7575 * Required.
7676 */
77- private String type ;
77+ private final String type ;
7878
7979 /**
8080 * The name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization.
8181 * Optional and type-specific.
8282 */
83- private @ Nullable String namespace ;
83+ private final @ Nullable String namespace ;
8484
8585 /**
8686 * The name of the package.
8787 * Required.
8888 */
89- private String name ;
89+ private final String name ;
9090
9191 /**
9292 * The version of the package.
9393 * Optional.
9494 */
95- private @ Nullable String version ;
95+ private final @ Nullable String version ;
9696
9797 /**
9898 * Extra qualifying data for a package such as an OS, architecture, a distro, etc.
9999 * Optional and type-specific.
100100 */
101- private @ Nullable Map <String , String > qualifiers ;
101+ private final @ Nullable Map <String , String > qualifiers ;
102102
103103 /**
104104 * Extra subpath within a package, relative to the package root.
105105 * Optional.
106106 */
107- private @ Nullable String subpath ;
107+ private final @ Nullable String subpath ;
108108
109109 /**
110110 * Constructs a new PackageURL object by parsing the specified string.
@@ -114,7 +114,90 @@ public final class PackageURL implements Serializable {
114114 * @throws NullPointerException if {@code purl} is {@code null}
115115 */
116116 public PackageURL (final String purl ) throws MalformedPackageURLException {
117- parse (requireNonNull (purl , "purl" ));
117+ requireNonNull (purl , "purl" );
118+
119+ if (purl .isEmpty ()) {
120+ throw new MalformedPackageURLException ("Invalid purl: Is empty or null" );
121+ }
122+
123+ try {
124+ if (!purl .startsWith (SCHEME_PART )) {
125+ throw new MalformedPackageURLException (
126+ "Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'" );
127+ }
128+
129+ final int length = purl .length ();
130+ int start = SCHEME_PART .length ();
131+
132+ while (start < length && '/' == purl .charAt (start )) {
133+ start ++;
134+ }
135+
136+ final URI uri = new URI (String .join ("/" , SCHEME_PART , purl .substring (start )));
137+
138+ validateScheme (uri .getScheme ());
139+
140+ // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
141+ if (uri .getRawAuthority () != null ) {
142+ throw new MalformedPackageURLException ("Invalid purl: A purl must NOT contain a URL Authority " );
143+ }
144+
145+ // subpath is optional - check for existence
146+ final String rawFragment = uri .getRawFragment ();
147+ if (rawFragment != null && !rawFragment .isEmpty ()) {
148+ this .subpath = validatePath (parsePath (rawFragment , true ), true );
149+ } else {
150+ this .subpath = null ;
151+ }
152+ // qualifiers are optional - check for existence
153+ final String rawQuery = uri .getRawQuery ();
154+ if (rawQuery != null && !rawQuery .isEmpty ()) {
155+ this .qualifiers = parseQualifiers (rawQuery );
156+ } else {
157+ this .qualifiers = null ;
158+ }
159+ // this is the rest of the purl that needs to be parsed
160+ String remainder = uri .getRawPath ();
161+ // trim trailing '/'
162+ int end = remainder .length () - 1 ;
163+ while (end > 0 && '/' == remainder .charAt (end )) {
164+ end --;
165+ }
166+ remainder = remainder .substring (0 , end + 1 );
167+ // there is exactly one leading '/' at this point
168+ start = 1 ;
169+ // type
170+ int index = remainder .indexOf ('/' , start );
171+ if (index <= start ) {
172+ throw new MalformedPackageURLException ("Invalid purl: does not contain both a type and name" );
173+ }
174+ this .type = toLowerCase (validateType (remainder .substring (start , index )));
175+
176+ start = index + 1 ;
177+
178+ // version is optional - check for existence
179+ index = remainder .lastIndexOf ('@' );
180+ if (index >= start ) {
181+ this .version = validateVersion (this .type , percentDecode (remainder .substring (index + 1 )));
182+ remainder = remainder .substring (0 , index );
183+ } else {
184+ this .version = null ;
185+ }
186+
187+ // The 'remainder' should now consist of an optional namespace and the name
188+ index = remainder .lastIndexOf ('/' );
189+ if (index <= start ) {
190+ this .name = validateName (this .type , percentDecode (remainder .substring (start )));
191+ this .namespace = null ;
192+ } else {
193+ this .name = validateName (this .type , percentDecode (remainder .substring (index + 1 )));
194+ remainder = remainder .substring (0 , index );
195+ this .namespace = validateNamespace (this .type , parsePath (remainder .substring (start ), false ));
196+ }
197+ verifyTypeConstraints (this .type , this .namespace , this .name );
198+ } catch (URISyntaxException e ) {
199+ throw new MalformedPackageURLException ("Invalid purl: " + e .getMessage (), e );
200+ }
118201 }
119202
120203 /**
@@ -685,92 +768,6 @@ static String percentEncode(final String source) {
685768 return changed ? new String (buffer .array (), 0 , buffer .position (), StandardCharsets .UTF_8 ) : source ;
686769 }
687770
688- /**
689- * Given a specified PackageURL, this method will parse the purl and populate this classes
690- * instance fields so that the corresponding getters may be called to retrieve the individual
691- * pieces of the purl.
692- *
693- * @param purl the purl string to parse
694- * @throws MalformedPackageURLException if an exception occurs when parsing
695- */
696- private void parse (final String purl ) throws MalformedPackageURLException {
697- if (purl .isEmpty ()) {
698- throw new MalformedPackageURLException ("Invalid purl: Is empty or null" );
699- }
700-
701- try {
702- if (!purl .startsWith (SCHEME_PART )) {
703- throw new MalformedPackageURLException (
704- "Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'" );
705- }
706-
707- final int length = purl .length ();
708- int start = SCHEME_PART .length ();
709-
710- while (start < length && '/' == purl .charAt (start )) {
711- start ++;
712- }
713-
714- final URI uri = new URI (String .join ("/" , SCHEME_PART , purl .substring (start )));
715-
716- validateScheme (uri .getScheme ());
717-
718- // Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
719- if (uri .getRawAuthority () != null ) {
720- throw new MalformedPackageURLException ("Invalid purl: A purl must NOT contain a URL Authority " );
721- }
722-
723- // subpath is optional - check for existence
724- final String rawFragment = uri .getRawFragment ();
725- if (rawFragment != null && !rawFragment .isEmpty ()) {
726- this .subpath = validatePath (parsePath (rawFragment , true ), true );
727- }
728- // qualifiers are optional - check for existence
729- final String rawQuery = uri .getRawQuery ();
730- if (rawQuery != null && !rawQuery .isEmpty ()) {
731- this .qualifiers = parseQualifiers (rawQuery );
732- }
733- // this is the rest of the purl that needs to be parsed
734- String remainder = uri .getRawPath ();
735- // trim trailing '/'
736- int end = remainder .length () - 1 ;
737- while (end > 0 && '/' == remainder .charAt (end )) {
738- end --;
739- }
740- remainder = remainder .substring (0 , end + 1 );
741- // there is exactly one leading '/' at this point
742- start = 1 ;
743- // type
744- int index = remainder .indexOf ('/' , start );
745- if (index <= start ) {
746- throw new MalformedPackageURLException ("Invalid purl: does not contain both a type and name" );
747- }
748- this .type = toLowerCase (validateType (remainder .substring (start , index )));
749-
750- start = index + 1 ;
751-
752- // version is optional - check for existence
753- index = remainder .lastIndexOf ('@' );
754- if (index >= start ) {
755- this .version = validateVersion (this .type , percentDecode (remainder .substring (index + 1 )));
756- remainder = remainder .substring (0 , index );
757- }
758-
759- // The 'remainder' should now consist of an optional namespace and the name
760- index = remainder .lastIndexOf ('/' );
761- if (index <= start ) {
762- this .name = validateName (this .type , percentDecode (remainder .substring (start )));
763- } else {
764- this .name = validateName (this .type , percentDecode (remainder .substring (index + 1 )));
765- remainder = remainder .substring (0 , index );
766- this .namespace = validateNamespace (this .type , parsePath (remainder .substring (start ), false ));
767- }
768- verifyTypeConstraints (this .type , this .namespace , this .name );
769- } catch (URISyntaxException e ) {
770- throw new MalformedPackageURLException ("Invalid purl: " + e .getMessage (), e );
771- }
772- }
773-
774771 /**
775772 * Some purl types may have specific constraints. This method attempts to verify them.
776773 * @param type the purl type
0 commit comments