2323
2424import static java .util .Objects .requireNonNull ;
2525
26+ import com .github .packageurl .type .PackageTypeFactory ;
2627import java .io .Serializable ;
2728import java .net .URI ;
2829import java .net .URISyntaxException ;
@@ -77,34 +78,34 @@ public final class PackageURL implements Serializable {
7778 private final String type ;
7879
7980 /**
80- * The name prefix such as a Maven groupid , a Docker image owner, a GitHub user or organization.
81+ * The name prefix such as a Maven groupId , a Docker image owner, a GitHub user or organization.
8182 * Optional and type-specific.
8283 */
83- private final @ Nullable String namespace ;
84+ private @ Nullable String namespace ;
8485
8586 /**
8687 * The name of the package.
8788 * Required.
8889 */
89- private final String name ;
90+ private String name ;
9091
9192 /**
9293 * The version of the package.
9394 * Optional.
9495 */
95- private final @ Nullable String version ;
96+ private @ Nullable String version ;
9697
9798 /**
9899 * Extra qualifying data for a package such as an OS, architecture, a distro, etc.
99100 * Optional and type-specific.
100101 */
101- private final @ Nullable Map <String , String > qualifiers ;
102+ private @ Nullable Map <String , String > qualifiers ;
102103
103104 /**
104105 * Extra subpath within a package, relative to the package root.
105106 * Optional.
106107 */
107- private final @ Nullable String subpath ;
108+ private @ Nullable String subpath ;
108109
109110 /**
110111 * Constructs a new PackageURL object by parsing the specified string.
@@ -194,7 +195,6 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
194195 remainder = remainder .substring (0 , index );
195196 this .namespace = validateNamespace (this .type , parsePath (remainder .substring (start ), false ));
196197 }
197- verifyTypeConstraints (this .type , this .namespace , this .name );
198198 } catch (URISyntaxException e ) {
199199 throw new MalformedPackageURLException ("Invalid purl: " + e .getMessage (), e );
200200 }
@@ -264,7 +264,6 @@ public PackageURL(
264264 this .version = validateVersion (this .type , version );
265265 this .qualifiers = parseQualifiers (qualifiers );
266266 this .subpath = validateSubpath (subpath );
267- verifyTypeConstraints (this .type , this .namespace , this .name );
268267 }
269268
270269 /**
@@ -360,11 +359,11 @@ private static String validateType(final String value) throws MalformedPackageUR
360359 return value ;
361360 }
362361
363- private static boolean isValidCharForType (int c ) {
362+ public static boolean isValidCharForType (int c ) {
364363 return (isAlphaNumeric (c ) || c == '.' || c == '+' || c == '-' );
365364 }
366365
367- private static boolean isValidCharForKey (int c ) {
366+ public static boolean isValidCharForKey (int c ) {
368367 return (isAlphaNumeric (c ) || c == '.' || c == '_' || c == '-' );
369368 }
370369
@@ -538,6 +537,12 @@ private static void validateValue(final String key, final @Nullable String value
538537 }
539538 }
540539
540+ public PackageURL normalize () throws MalformedPackageURLException {
541+ PackageTypeFactory .getInstance ().validateComponents (type , namespace , name , version , qualifiers , subpath );
542+ return PackageTypeFactory .getInstance ()
543+ .normalizeComponents (type , namespace , name , version , qualifiers , subpath );
544+ }
545+
541546 /**
542547 * Returns the canonicalized representation of the purl.
543548 *
@@ -565,6 +570,17 @@ public String canonicalize() {
565570 * @since 1.3.2
566571 */
567572 private String canonicalize (boolean coordinatesOnly ) {
573+ try {
574+ PackageURL packageURL = normalize ();
575+ namespace = packageURL .getNamespace ();
576+ name = packageURL .getName ();
577+ version = packageURL .getVersion ();
578+ qualifiers = packageURL .getQualifiers ();
579+ subpath = packageURL .getSubpath ();
580+ } catch (MalformedPackageURLException e ) {
581+ throw new ValidationException ("Normalization failed" , e );
582+ }
583+
568584 final StringBuilder purl = new StringBuilder ();
569585 purl .append (SCHEME_PART ).append (type ).append ('/' );
570586 if (namespace != null ) {
@@ -577,7 +593,7 @@ private String canonicalize(boolean coordinatesOnly) {
577593 }
578594
579595 if (!coordinatesOnly ) {
580- if (qualifiers != null ) {
596+ if (! qualifiers . isEmpty () ) {
581597 purl .append ('?' );
582598 Set <Map .Entry <String , String >> entries = qualifiers .entrySet ();
583599 boolean separator = false ;
@@ -606,18 +622,22 @@ private static boolean shouldEncode(int c) {
606622 return !isUnreserved (c );
607623 }
608624
609- private static boolean isAlpha (int c ) {
625+ public static boolean isAlpha (int c ) {
610626 return (isLowerCase (c ) || isUpperCase (c ));
611627 }
612628
613629 private static boolean isDigit (int c ) {
614630 return (c >= '0' && c <= '9' );
615631 }
616632
617- private static boolean isAlphaNumeric (int c ) {
633+ public static boolean isAlphaNumeric (int c ) {
618634 return (isDigit (c ) || isAlpha (c ));
619635 }
620636
637+ public static boolean isWhitespace (int c ) {
638+ return (c == ' ' || c == '\t' || c == '\r' || c == '\n' );
639+ }
640+
621641 private static boolean isUpperCase (int c ) {
622642 return (c >= 'A' && c <= 'Z' );
623643 }
@@ -642,7 +662,7 @@ private static int toLowerCase(int c) {
642662 return isUpperCase (c ) ? (c ^ 0x20 ) : c ;
643663 }
644664
645- static String toLowerCase (String s ) {
665+ public static String toLowerCase (String s ) {
646666 int pos = indexOfFirstUpperCaseChar (s );
647667
648668 if (pos == -1 ) {
@@ -770,22 +790,6 @@ static String percentEncode(final String source) {
770790 return changed ? new String (buffer .array (), 0 , buffer .position (), StandardCharsets .UTF_8 ) : source ;
771791 }
772792
773- /**
774- * Some purl types may have specific constraints. This method attempts to verify them.
775- * @param type the purl type
776- * @param namespace the purl namespace
777- * @throws MalformedPackageURLException if constraints are not met
778- */
779- private static void verifyTypeConstraints (String type , @ Nullable String namespace , @ Nullable String name )
780- throws MalformedPackageURLException {
781- if (StandardTypes .MAVEN .equals (type )) {
782- if (isEmpty (namespace ) || isEmpty (name )) {
783- throw new MalformedPackageURLException (
784- "The PackageURL specified is invalid. Maven requires both a namespace and name." );
785- }
786- }
787- }
788-
789793 private static @ Nullable Map <String , String > parseQualifiers (final @ Nullable Map <String , String > qualifiers )
790794 throws MalformedPackageURLException {
791795 if (qualifiers == null || qualifiers .isEmpty ()) {
@@ -1107,15 +1111,15 @@ public static final class StandardTypes {
11071111 * @deprecated use {@link #DEB} instead
11081112 */
11091113 @ Deprecated
1110- public static final String DEBIAN = "deb" ;
1114+ public static final String DEBIAN = DEB ;
11111115 /**
11121116 * Nixos packages.
11131117 *
11141118 * @since 1.1.0
11151119 * @deprecated use {@link #NIX} instead
11161120 */
11171121 @ Deprecated
1118- public static final String NIXPKGS = "nix" ;
1122+ public static final String NIXPKGS = NIX ;
11191123
11201124 private StandardTypes () {}
11211125 }
0 commit comments