2929import java .nio .ByteBuffer ;
3030import java .nio .charset .StandardCharsets ;
3131import java .util .Arrays ;
32+ import java .util .BitSet ;
3233import java .util .Collections ;
3334import java .util .Map ;
3435import java .util .Objects ;
@@ -60,6 +61,110 @@ public final class PackageURL implements Serializable {
6061
6162 private static final char PERCENT_CHAR = '%' ;
6263
64+ private static final int NBITS = 128 ;
65+
66+ private static final BitSet DIGIT = new BitSet (NBITS );
67+
68+ static {
69+ IntStream .rangeClosed ('0' , '9' ).forEach (DIGIT ::set );
70+ }
71+
72+ private static final BitSet LOWER = new BitSet (NBITS );
73+
74+ static {
75+ IntStream .rangeClosed ('a' , 'z' ).forEach (LOWER ::set );
76+ }
77+
78+ private static final BitSet UPPER = new BitSet (NBITS );
79+
80+ static {
81+ IntStream .rangeClosed ('A' , 'Z' ).forEach (UPPER ::set );
82+ }
83+
84+ private static final BitSet ALPHA = new BitSet (NBITS );
85+
86+ static {
87+ ALPHA .or (LOWER );
88+ ALPHA .or (UPPER );
89+ }
90+
91+ private static final BitSet ALPHA_DIGIT = new BitSet (NBITS );
92+
93+ static {
94+ ALPHA_DIGIT .or (ALPHA );
95+ ALPHA_DIGIT .or (DIGIT );
96+ }
97+
98+ private static final BitSet UNRESERVED = new BitSet (NBITS );
99+
100+ static {
101+ UNRESERVED .or (ALPHA_DIGIT );
102+ UNRESERVED .set ('-' );
103+ UNRESERVED .set ('.' );
104+ UNRESERVED .set ('_' );
105+ UNRESERVED .set ('~' );
106+ }
107+
108+ private static final BitSet GEN_DELIMS = new BitSet (NBITS );
109+
110+ static {
111+ GEN_DELIMS .set (':' );
112+ GEN_DELIMS .set ('/' );
113+ GEN_DELIMS .set ('?' );
114+ GEN_DELIMS .set ('#' );
115+ GEN_DELIMS .set ('[' );
116+ GEN_DELIMS .set (']' );
117+ GEN_DELIMS .set ('@' );
118+ }
119+
120+ private static final BitSet SUB_DELIMS = new BitSet (NBITS );
121+
122+ static {
123+ SUB_DELIMS .set ('!' );
124+ SUB_DELIMS .set ('$' );
125+ SUB_DELIMS .set ('&' );
126+ SUB_DELIMS .set ('\'' );
127+ SUB_DELIMS .set ('(' );
128+ SUB_DELIMS .set (')' );
129+ SUB_DELIMS .set ('*' );
130+ SUB_DELIMS .set ('+' );
131+ SUB_DELIMS .set (',' );
132+ SUB_DELIMS .set (';' );
133+ SUB_DELIMS .set ('=' );
134+ }
135+
136+ private static final BitSet PCHAR = new BitSet (NBITS );
137+
138+ static {
139+ PCHAR .or (UNRESERVED );
140+ PCHAR .or (SUB_DELIMS );
141+ PCHAR .set (':' );
142+ PCHAR .clear ('&' ); // XXX: Why?
143+ }
144+
145+ private static final BitSet QUERY = new BitSet (NBITS );
146+
147+ static {
148+ QUERY .or (GEN_DELIMS );
149+ QUERY .or (PCHAR );
150+ QUERY .set ('/' );
151+ QUERY .set ('?' );
152+ QUERY .clear ('#' );
153+ QUERY .clear ('&' );
154+ QUERY .clear ('=' );
155+ }
156+
157+ private static final BitSet FRAGMENT = new BitSet (NBITS );
158+
159+ static {
160+ FRAGMENT .or (GEN_DELIMS );
161+ FRAGMENT .or (PCHAR );
162+ FRAGMENT .set ('/' );
163+ FRAGMENT .set ('?' );
164+ FRAGMENT .set ('&' );
165+ FRAGMENT .clear ('#' );
166+ }
167+
63168 /**
64169 * The PackageURL scheme constant
65170 */
@@ -128,7 +233,7 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
128233 * @since 1.0.0
129234 */
130235 public PackageURL (final String type , final String name ) throws MalformedPackageURLException {
131- this (type , null , name , null , null , null );
236+ this (type , null , name , null , ( Map < String , String >) null , null );
132237 }
133238
134239 /**
@@ -419,6 +524,7 @@ private static String validateName(final String type, final String value) throws
419524 validateKey (key );
420525 validateValue (key , entry .getValue ());
421526 }
527+
422528 return values ;
423529 }
424530
@@ -500,12 +606,12 @@ private String canonicalize(boolean coordinatesOnly) {
500606 final StringBuilder purl = new StringBuilder ();
501607 purl .append (SCHEME_PART ).append (type ).append ('/' );
502608 if (namespace != null ) {
503- purl .append (encodePath (namespace ));
609+ purl .append (encodePath (namespace , PCHAR ));
504610 purl .append ('/' );
505611 }
506- purl .append (percentEncode (name ));
612+ purl .append (percentEncode (name , PCHAR ));
507613 if (version != null ) {
508- purl .append ('@' ).append (percentEncode (version ));
614+ purl .append ('@' ).append (percentEncode (version , PCHAR ));
509615 }
510616
511617 if (!coordinatesOnly ) {
@@ -519,23 +625,27 @@ private String canonicalize(boolean coordinatesOnly) {
519625 }
520626 purl .append (entry .getKey ());
521627 purl .append ('=' );
522- purl .append (percentEncode (entry .getValue ()));
628+ purl .append (percentEncode (entry .getValue (), QUERY ));
523629 separator = true ;
524630 }
525631 }
526632 if (subpath != null ) {
527- purl .append ('#' ).append (encodePath (subpath ));
633+ purl .append ('#' ).append (encodePath (subpath , FRAGMENT ));
528634 }
529635 }
530636 return purl .toString ();
531637 }
532638
533- private static boolean isUnreserved (int c ) {
534- return (isValidCharForKey (c ) || c == '~' );
639+ private static boolean isUnreserved (int c , BitSet safe ) {
640+ if (c < 0 || c >= NBITS ) {
641+ return false ;
642+ }
643+
644+ return safe .get (c );
535645 }
536646
537- private static boolean shouldEncode (int c ) {
538- return !isUnreserved (c );
647+ private static boolean shouldEncode (int c , BitSet safe ) {
648+ return !isUnreserved (c , safe );
539649 }
540650
541651 private static boolean isAlpha (int c ) {
@@ -666,7 +776,11 @@ private static boolean isPercent(int c) {
666776 return (c == PERCENT_CHAR );
667777 }
668778
669- private static String percentEncode (final String source ) {
779+ static String percentEncode (final String source ) {
780+ return percentEncode (source , UNRESERVED );
781+ }
782+
783+ private static String percentEncode (final String source , final BitSet safe ) {
670784 if (source .isEmpty ()) {
671785 return source ;
672786 }
@@ -677,7 +791,7 @@ private static String percentEncode(final String source) {
677791 boolean changed = false ;
678792
679793 for (byte b : bytes ) {
680- if (shouldEncode (b )) {
794+ if (shouldEncode (b , safe )) {
681795 changed = true ;
682796 byte b1 = (byte ) Character .toUpperCase (Character .forDigit ((b >> 4 ) & 0xF , 16 ));
683797 byte b2 = (byte ) Character .toUpperCase (Character .forDigit (b & 0xF , 16 ));
@@ -813,8 +927,7 @@ private static void verifyTypeConstraints(String type, @Nullable String namespac
813927 }
814928 }
815929
816- @ SuppressWarnings ("StringSplitter" ) // reason: surprising behavior is okay in this case
817- private static @ Nullable Map <String , String > parseQualifiers (final String encodedString )
930+ static @ Nullable Map <String , String > parseQualifiers (final String encodedString )
818931 throws MalformedPackageURLException {
819932 try {
820933 final TreeMap <String , String > results = Arrays .stream (encodedString .split ("&" ))
@@ -845,8 +958,10 @@ private static String[] parsePath(final String path, final boolean isSubpath) {
845958 .toArray (String []::new );
846959 }
847960
848- private static String encodePath (final String path ) {
849- return Arrays .stream (path .split ("/" )).map (PackageURL ::percentEncode ).collect (Collectors .joining ("/" ));
961+ private String encodePath (final String path , BitSet safe ) {
962+ return Arrays .stream (path .split ("/" ))
963+ .map (source -> percentEncode (source , safe ))
964+ .collect (Collectors .joining ("/" ));
850965 }
851966
852967 /**
0 commit comments