2222package com .github .packageurl ;
2323
2424import java .io .Serializable ;
25- import java .io .UnsupportedEncodingException ;
2625import java .net .URI ;
2726import java .net .URISyntaxException ;
28- import java .net .URLDecoder ;
29- import java .net .URLEncoder ;
27+ import java .nio .charset .Charset ;
3028import java .nio .charset .StandardCharsets ;
3129import java .util .Arrays ;
3230import java .util .Collections ;
5553public final class PackageURL implements Serializable {
5654
5755 private static final long serialVersionUID = 3243226021636427586L ;
58- private static final String UTF8 = StandardCharsets .UTF_8 .name ();
5956 private static final Pattern PATH_SPLITTER = Pattern .compile ("/" );
6057
6158 /**
@@ -432,16 +429,38 @@ private String canonicalize(boolean coordinatesOnly) {
432429 * @return an encoded String
433430 */
434431 private String percentEncode (final String input ) {
435- try {
436- return URLEncoder .encode (input , UTF8 )
437- .replace ("+" , "%20" )
438- // "*" is a reserved character in RFC 3986.
439- .replace ("*" , "%2A" )
440- // "~" is an unreserved character in RFC 3986.
441- .replace ("%7E" , "~" );
442- } catch (UnsupportedEncodingException e ) {
443- return input ; // this should never occur
432+ return uriEncode (input , StandardCharsets .UTF_8 );
433+ }
434+
435+ private static String uriEncode (String source , Charset charset ) {
436+ if (source == null || source .length () == 0 ) {
437+ return source ;
444438 }
439+
440+ StringBuilder builder = new StringBuilder ();
441+ for (byte b : source .getBytes (charset )) {
442+ if (isUnreserved (b )) {
443+ builder .append ((char ) b );
444+ }
445+ else {
446+ // Substitution: A '%' followed by the hexadecimal representation of the ASCII value of the replaced character
447+ builder .append ('%' );
448+ builder .append (Integer .toHexString (b ).toUpperCase ());
449+ }
450+ }
451+ return builder .toString ();
452+ }
453+
454+ private static boolean isUnreserved (int c ) {
455+ return (isAlpha (c ) || isDigit (c ) || '-' == c || '.' == c || '_' == c || '~' == c );
456+ }
457+
458+ private static boolean isAlpha (int c ) {
459+ return ((c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' ));
460+ }
461+
462+ private static boolean isDigit (int c ) {
463+ return (c >= '0' && c <= '9' );
445464 }
446465
447466 /**
@@ -455,17 +474,33 @@ private String percentDecode(final String input) {
455474 if (input == null ) {
456475 return null ;
457476 }
458- try {
459- final String decoded = URLDecoder .decode (input , UTF8 );
460- if (!decoded .equals (input )) {
461- return decoded ;
462- }
463- } catch (UnsupportedEncodingException e ) {
464- return input ; // this should never occur
477+ final String decoded = uriDecode (input );
478+ if (!decoded .equals (input )) {
479+ return decoded ;
465480 }
466481 return input ;
467482 }
468483
484+ public static String uriDecode (String source ) {
485+ if (source == null ) {
486+ return source ;
487+ }
488+ int length = source .length ();
489+ StringBuilder builder = new StringBuilder ();
490+ for (int i = 0 ; i < length ; i ++) {
491+ if (source .charAt (i ) == '%' ) {
492+ String str = source .substring (i + 1 , i + 3 );
493+ char c = (char ) Integer .parseInt (str , 16 );
494+ builder .append (c );
495+ i += 2 ;
496+ }
497+ else {
498+ builder .append (source .charAt (i ));
499+ }
500+ }
501+ return builder .toString ();
502+ }
503+
469504 /**
470505 * Given a specified PackageURL, this method will parse the purl and populate this classes
471506 * instance fields so that the corresponding getters may be called to retrieve the individual
0 commit comments