Skip to content

Commit e195ffc

Browse files
author
Pablo Galego
committed
URI encode/decode in conformance with RFC 3986
1 parent bd3241a commit e195ffc

2 files changed

Lines changed: 68 additions & 18 deletions

File tree

src/main/java/com/github/packageurl/PackageURL.java

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
*/
2222
package com.github.packageurl;
2323

24+
import java.io.ByteArrayOutputStream;
2425
import java.io.Serializable;
2526
import java.io.UnsupportedEncodingException;
2627
import java.net.URI;
2728
import java.net.URISyntaxException;
28-
import java.net.URLDecoder;
29-
import java.net.URLEncoder;
29+
import java.nio.charset.Charset;
3030
import java.nio.charset.StandardCharsets;
3131
import java.util.Arrays;
3232
import java.util.Collections;
@@ -432,16 +432,38 @@ private String canonicalize(boolean coordinatesOnly) {
432432
* @return an encoded String
433433
*/
434434
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
435+
return uriEncode(input, StandardCharsets.UTF_8);
436+
}
437+
438+
private static String uriEncode(String source, Charset charset) {
439+
if (source == null || source.length() == 0) {
440+
return source;
444441
}
442+
443+
StringBuilder builder = new StringBuilder();
444+
for (byte b : source.getBytes(charset)) {
445+
if (isUnreserved(b)) {
446+
builder.append((char) b);
447+
}
448+
else {
449+
// Substitution: A '%' followed by the hexadecimal representation of the ASCII value of the replaced character
450+
builder.append('%');
451+
builder.append(Integer.toHexString(b).toUpperCase());
452+
}
453+
}
454+
return builder.toString();
455+
}
456+
457+
private static boolean isUnreserved(int c) {
458+
return (isAlpha(c) || isDigit(c) || '-' == c || '.' == c || '_' == c || '~' == c);
459+
}
460+
461+
private static boolean isAlpha(int c) {
462+
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
463+
}
464+
465+
private static boolean isDigit(int c) {
466+
return (c >= '0' && c <= '9');
445467
}
446468

447469
/**
@@ -455,17 +477,33 @@ private String percentDecode(final String input) {
455477
if (input == null) {
456478
return null;
457479
}
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
480+
final String decoded = uriDecode(input);
481+
if (!decoded.equals(input)) {
482+
return decoded;
465483
}
466484
return input;
467485
}
468486

487+
public static String uriDecode(String source) {
488+
if (source == null) {
489+
return source;
490+
}
491+
int length = source.length();
492+
StringBuilder builder = new StringBuilder();
493+
for (int i = 0; i < length; i++) {
494+
if (source.charAt(i) == '%') {
495+
String str = source.substring(i + 1, i + 3);
496+
char c = (char) Integer.parseInt(str, 16);
497+
builder.append(c);
498+
i += 2;
499+
}
500+
else {
501+
builder.append(source.charAt(i));
502+
}
503+
}
504+
return builder.toString();
505+
}
506+
469507
/**
470508
* Given a specified PackageURL, this method will parse the purl and populate this classes
471509
* instance fields so that the corresponding getters may be called to retrieve the individual

src/test/resources/test-suite-data.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,18 @@
311311
"subpath": null,
312312
"is_invalid": false
313313
},
314+
{
315+
"description": "valid debian purl containing a plus in the name and version",
316+
"purl": "pkg:deb/debian/g++-10@10.2.1+6",
317+
"canonical_purl": "pkg:deb/debian/g%2B%2B-10@10.2.1%2B6",
318+
"type": "deb",
319+
"namespace": "debian",
320+
"name": "g++-10",
321+
"version": "10.2.1+6",
322+
"qualifiers": null,
323+
"subpath": null,
324+
"is_invalid": false
325+
},
314326
{
315327
"description": "checks for invalid qualifier keys",
316328
"purl": "pkg:npm/myartifact@1.0.0?in%20production=true",

0 commit comments

Comments
 (0)