Skip to content

Commit 93fadd6

Browse files
Merge pull request #98 from pablogalegoc/fix-encoding-rfc3986
URI encode/decode in conformance with RFC 3986
2 parents 4b812de + 1aec64c commit 93fadd6

2 files changed

Lines changed: 67 additions & 20 deletions

File tree

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

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@
2222
package com.github.packageurl;
2323

2424
import java.io.Serializable;
25-
import java.io.UnsupportedEncodingException;
2625
import java.net.URI;
2726
import java.net.URISyntaxException;
28-
import java.net.URLDecoder;
29-
import java.net.URLEncoder;
27+
import java.nio.charset.Charset;
3028
import java.nio.charset.StandardCharsets;
3129
import java.util.Arrays;
3230
import java.util.Collections;
@@ -55,7 +53,6 @@
5553
public 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

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)