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 * Constructs a new PackageURL object by parsing the specified string.
65170 *
@@ -82,7 +187,7 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
82187 * @since 1.0.0
83188 */
84189 public PackageURL (final String type , final String name ) throws MalformedPackageURLException {
85- this (type , null , name , null , null , null );
190+ this (type , null , name , null , ( Map < String , String >) null , null );
86191 }
87192
88193 /**
@@ -406,7 +511,7 @@ private String validateName(final String value) throws MalformedPackageURLExcept
406511 }
407512 }
408513
409- private @ Nullable Map <String , String > validateQualifiers (final @ Nullable Map <String , String > values )
514+ private static @ Nullable Map <String , String > validateQualifiers (final @ Nullable Map <String , String > values )
410515 throws MalformedPackageURLException {
411516 if (values == null || values .isEmpty ()) {
412517 return null ;
@@ -417,6 +522,7 @@ private String validateName(final String value) throws MalformedPackageURLExcept
417522 validateKey (key );
418523 validateValue (key , entry .getValue ());
419524 }
525+
420526 return values ;
421527 }
422528
@@ -498,12 +604,12 @@ private String canonicalize(boolean coordinatesOnly) {
498604 final StringBuilder purl = new StringBuilder ();
499605 purl .append (SCHEME_PART ).append (type ).append ('/' );
500606 if (namespace != null ) {
501- purl .append (encodePath (namespace ));
607+ purl .append (encodePath (namespace , PCHAR ));
502608 purl .append ('/' );
503609 }
504- purl .append (percentEncode (name ));
610+ purl .append (percentEncode (name , PCHAR ));
505611 if (version != null ) {
506- purl .append ('@' ).append (percentEncode (version ));
612+ purl .append ('@' ).append (percentEncode (version , PCHAR ));
507613 }
508614
509615 if (!coordinatesOnly ) {
@@ -517,23 +623,27 @@ private String canonicalize(boolean coordinatesOnly) {
517623 }
518624 purl .append (entry .getKey ());
519625 purl .append ('=' );
520- purl .append (percentEncode (entry .getValue ()));
626+ purl .append (percentEncode (entry .getValue (), QUERY ));
521627 separator = true ;
522628 }
523629 }
524630 if (subpath != null ) {
525- purl .append ('#' ).append (encodePath (subpath ));
631+ purl .append ('#' ).append (encodePath (subpath , FRAGMENT ));
526632 }
527633 }
528634 return purl .toString ();
529635 }
530636
531- private static boolean isUnreserved (int c ) {
532- return (isValidCharForKey (c ) || c == '~' );
637+ private static boolean isUnreserved (int c , BitSet safe ) {
638+ if (c < 0 || c >= NBITS ) {
639+ return false ;
640+ }
641+
642+ return safe .get (c );
533643 }
534644
535- private static boolean shouldEncode (int c ) {
536- return !isUnreserved (c );
645+ private static boolean shouldEncode (int c , BitSet safe ) {
646+ return !isUnreserved (c , safe );
537647 }
538648
539649 private static boolean isAlpha (int c ) {
@@ -596,14 +706,14 @@ private static int indexOfPercentChar(final byte[] bytes, final int start) {
596706 .orElse (-1 );
597707 }
598708
599- private static int indexOfUnsafeChar (final byte [] bytes , final int start ) {
709+ private static int indexOfUnsafeChar (final byte [] bytes , final int start , BitSet safe ) {
600710 return IntStream .range (start , bytes .length )
601- .filter (i -> shouldEncode (bytes [i ]))
711+ .filter (i -> shouldEncode (bytes [i ], safe ))
602712 .findFirst ()
603713 .orElse (-1 );
604714 }
605715
606- private static byte percentDecode (final byte [] bytes , final int start ) {
716+ static byte percentDecode (final byte [] bytes , final int start ) {
607717 if (start + 2 >= bytes .length ) {
608718 throw new ValidationException ("Incomplete percent encoding at offset " + start + " with value '"
609719 + new String (bytes , start , bytes .length - start , StandardCharsets .UTF_8 ) + "'" );
@@ -671,7 +781,11 @@ private static boolean isPercent(int c) {
671781 return (c == PERCENT_CHAR );
672782 }
673783
674- private static String percentEncode (final String source ) {
784+ static String percentEncode (final String source ) {
785+ return percentEncode (source , UNRESERVED );
786+ }
787+
788+ private static String percentEncode (final String source , final BitSet safe ) {
675789 if (source .isEmpty ()) {
676790 return source ;
677791 }
@@ -682,7 +796,7 @@ private static String percentEncode(final String source) {
682796 boolean changed = false ;
683797
684798 for (byte b : bytes ) {
685- if (shouldEncode (b )) {
799+ if (shouldEncode (b , safe )) {
686800 changed = true ;
687801 byte b1 = (byte ) Character .toUpperCase (Character .forDigit ((b >> 4 ) & 0xF , 16 ));
688802 byte b2 = (byte ) Character .toUpperCase (Character .forDigit (b & 0xF , 16 ));
@@ -818,8 +932,7 @@ private void verifyTypeConstraints(String type, @Nullable String namespace, @Nul
818932 }
819933 }
820934
821- @ SuppressWarnings ("StringSplitter" ) // reason: surprising behavior is okay in this case
822- private @ Nullable Map <String , String > parseQualifiers (final String encodedString )
935+ static @ Nullable Map <String , String > parseQualifiers (final String encodedString )
823936 throws MalformedPackageURLException {
824937 try {
825938 final TreeMap <String , String > results = Arrays .stream (encodedString .split ("&" ))
@@ -850,8 +963,10 @@ private String[] parsePath(final String path, final boolean isSubpath) {
850963 .toArray (String []::new );
851964 }
852965
853- private String encodePath (final String path ) {
854- return Arrays .stream (path .split ("/" )).map (PackageURL ::percentEncode ).collect (Collectors .joining ("/" ));
966+ private String encodePath (final String path , BitSet safe ) {
967+ return Arrays .stream (path .split ("/" ))
968+ .map (source -> percentEncode (source , safe ))
969+ .collect (Collectors .joining ("/" ));
855970 }
856971
857972 /**
0 commit comments