29
29
import java .nio .ByteBuffer ;
30
30
import java .nio .charset .StandardCharsets ;
31
31
import java .util .Arrays ;
32
+ import java .util .BitSet ;
32
33
import java .util .Collections ;
33
34
import java .util .Map ;
34
35
import java .util .Objects ;
@@ -60,6 +61,110 @@ public final class PackageURL implements Serializable {
60
61
61
62
private static final char PERCENT_CHAR = '%' ;
62
63
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
+
63
168
/**
64
169
* Constructs a new PackageURL object by parsing the specified string.
65
170
*
@@ -82,7 +187,7 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
82
187
* @since 1.0.0
83
188
*/
84
189
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 );
86
191
}
87
192
88
193
/**
@@ -406,7 +511,7 @@ private String validateName(final String value) throws MalformedPackageURLExcept
406
511
}
407
512
}
408
513
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 )
410
515
throws MalformedPackageURLException {
411
516
if (values == null || values .isEmpty ()) {
412
517
return null ;
@@ -417,6 +522,7 @@ private String validateName(final String value) throws MalformedPackageURLExcept
417
522
validateKey (key );
418
523
validateValue (key , entry .getValue ());
419
524
}
525
+
420
526
return values ;
421
527
}
422
528
@@ -498,12 +604,12 @@ private String canonicalize(boolean coordinatesOnly) {
498
604
final StringBuilder purl = new StringBuilder ();
499
605
purl .append (SCHEME_PART ).append (type ).append ('/' );
500
606
if (namespace != null ) {
501
- purl .append (encodePath (namespace ));
607
+ purl .append (encodePath (namespace , PCHAR ));
502
608
purl .append ('/' );
503
609
}
504
- purl .append (percentEncode (name ));
610
+ purl .append (percentEncode (name , PCHAR ));
505
611
if (version != null ) {
506
- purl .append ('@' ).append (percentEncode (version ));
612
+ purl .append ('@' ).append (percentEncode (version , PCHAR ));
507
613
}
508
614
509
615
if (!coordinatesOnly ) {
@@ -517,23 +623,27 @@ private String canonicalize(boolean coordinatesOnly) {
517
623
}
518
624
purl .append (entry .getKey ());
519
625
purl .append ('=' );
520
- purl .append (percentEncode (entry .getValue ()));
626
+ purl .append (percentEncode (entry .getValue (), QUERY ));
521
627
separator = true ;
522
628
}
523
629
}
524
630
if (subpath != null ) {
525
- purl .append ('#' ).append (encodePath (subpath ));
631
+ purl .append ('#' ).append (encodePath (subpath , FRAGMENT ));
526
632
}
527
633
}
528
634
return purl .toString ();
529
635
}
530
636
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 );
533
643
}
534
644
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 );
537
647
}
538
648
539
649
private static boolean isAlpha (int c ) {
@@ -596,14 +706,14 @@ private static int indexOfPercentChar(final byte[] bytes, final int start) {
596
706
.orElse (-1 );
597
707
}
598
708
599
- private static int indexOfUnsafeChar (final byte [] bytes , final int start ) {
709
+ private static int indexOfUnsafeChar (final byte [] bytes , final int start , BitSet safe ) {
600
710
return IntStream .range (start , bytes .length )
601
- .filter (i -> shouldEncode (bytes [i ]))
711
+ .filter (i -> shouldEncode (bytes [i ], safe ))
602
712
.findFirst ()
603
713
.orElse (-1 );
604
714
}
605
715
606
- private static byte percentDecode (final byte [] bytes , final int start ) {
716
+ static byte percentDecode (final byte [] bytes , final int start ) {
607
717
if (start + 2 >= bytes .length ) {
608
718
throw new ValidationException ("Incomplete percent encoding at offset " + start + " with value '"
609
719
+ new String (bytes , start , bytes .length - start , StandardCharsets .UTF_8 ) + "'" );
@@ -671,7 +781,11 @@ private static boolean isPercent(int c) {
671
781
return (c == PERCENT_CHAR );
672
782
}
673
783
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 ) {
675
789
if (source .isEmpty ()) {
676
790
return source ;
677
791
}
@@ -682,7 +796,7 @@ private static String percentEncode(final String source) {
682
796
boolean changed = false ;
683
797
684
798
for (byte b : bytes ) {
685
- if (shouldEncode (b )) {
799
+ if (shouldEncode (b , safe )) {
686
800
changed = true ;
687
801
byte b1 = (byte ) Character .toUpperCase (Character .forDigit ((b >> 4 ) & 0xF , 16 ));
688
802
byte b2 = (byte ) Character .toUpperCase (Character .forDigit (b & 0xF , 16 ));
@@ -818,8 +932,7 @@ private void verifyTypeConstraints(String type, @Nullable String namespace, @Nul
818
932
}
819
933
}
820
934
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 )
823
936
throws MalformedPackageURLException {
824
937
try {
825
938
final TreeMap <String , String > results = Arrays .stream (encodedString .split ("&" ))
@@ -850,8 +963,10 @@ private String[] parsePath(final String path, final boolean isSubpath) {
850
963
.toArray (String []::new );
851
964
}
852
965
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 ("/" ));
855
970
}
856
971
857
972
/**
0 commit comments