Skip to content

Commit ee295df

Browse files
committed
Fix URL encoding and decoding
The methods `uriEncode` and `uriDecode` did not properly handle percent-encoding. In particular, `uriEncode` didn't properly output two uppercase hex digits and `urlDecode` did not properly handle non-ASCII characters. Aditionally, if no percent-encoding was performed, these methods will now return the original string. Fixes package-url#150 Closes package-url#153 Fixes package-url#154
1 parent ee6dda9 commit ee295df

File tree

2 files changed

+49
-29
lines changed

2 files changed

+49
-29
lines changed

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

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222
package com.github.packageurl;
2323

24+
import java.io.ByteArrayOutputStream;
2425
import java.io.Serializable;
2526
import java.net.URI;
2627
import java.net.URISyntaxException;
@@ -441,22 +442,25 @@ private String percentEncode(final String input) {
441442
}
442443

443444
private static String uriEncode(String source, Charset charset) {
444-
if (source == null || source.length() == 0) {
445+
if (source == null || source.isEmpty()) {
445446
return source;
446447
}
447448

448-
StringBuilder builder = new StringBuilder();
449-
for (byte b : source.getBytes(charset)) {
449+
boolean changed = false;
450+
StringBuilder builder = new StringBuilder(source.length());
451+
byte[] bytes = source.getBytes(charset);
452+
453+
for (byte b : bytes) {
450454
if (isUnreserved(b)) {
451455
builder.append((char) b);
452-
}
453-
else {
454-
// Substitution: A '%' followed by the hexadecimal representation of the ASCII value of the replaced character
456+
} else {
455457
builder.append('%');
456-
builder.append(Integer.toHexString(b).toUpperCase());
458+
builder.append(Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)));
459+
builder.append(Character.toUpperCase(Character.forDigit(b & 0xF, 16)));
460+
changed = true;
457461
}
458462
}
459-
return builder.toString();
463+
return changed ? builder.toString() : source;
460464
}
461465

462466
private static boolean isUnreserved(int c) {
@@ -479,34 +483,37 @@ private static boolean isDigit(int c) {
479483
* @return a decoded String
480484
*/
481485
private String percentDecode(final String input) {
482-
if (input == null) {
483-
return null;
484-
}
485-
final String decoded = uriDecode(input);
486-
if (!decoded.equals(input)) {
487-
return decoded;
488-
}
489-
return input;
486+
return uriDecode(input);
490487
}
491488

492489
public static String uriDecode(String source) {
493-
if (source == null) {
490+
if (source == null || source.isEmpty()) {
494491
return source;
495492
}
496-
int length = source.length();
497-
StringBuilder builder = new StringBuilder();
498-
for (int i = 0; i < length; i++) {
499-
if (source.charAt(i) == '%') {
500-
String str = source.substring(i + 1, i + 3);
501-
char c = (char) Integer.parseInt(str, 16);
502-
builder.append(c);
503-
i += 2;
504-
}
505-
else {
506-
builder.append(source.charAt(i));
493+
494+
boolean changed = false;
495+
byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
496+
ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);
497+
498+
for (int i = 0; i < bytes.length; i++) {
499+
int b = bytes[i];
500+
501+
if (b == '%') {
502+
if (i + 2 >= bytes.length) {
503+
return source;
504+
}
505+
506+
int b1 = Character.digit(bytes[++i], 16);
507+
int b2 = Character.digit(bytes[++i], 16);
508+
buffer.write((char) ((b1 << 4) + b2));
509+
changed = true;
510+
} else {
511+
buffer.write(b);
507512
}
508513
}
509-
return builder.toString();
514+
515+
byte[] b = buffer.toByteArray();
516+
return changed ? new String(b, StandardCharsets.UTF_8) : source;
510517
}
511518

512519
/**

src/test/java/com/github/packageurl/PackageURLTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ public static void setup() throws IOException {
5555
json = new JSONArray(jsonTxt);
5656
}
5757

58+
@Test
59+
public void testEncoding1() throws MalformedPackageURLException {
60+
PackageURL purl = new PackageURL("maven", "com.google.summit", "summit-ast", "2.2.0\n", null, null);
61+
Assert.assertEquals("pkg:maven/com.google.summit/summit-ast@2.2.0%0A", purl.toString());
62+
}
63+
64+
@Test
65+
public void testEncoding2() throws MalformedPackageURLException {
66+
PackageURL purl = new PackageURL("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5");
67+
Assert.assertEquals("Мicrosоft.ЕntitуFramеworkСоrе", purl.getName());
68+
Assert.assertEquals("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5", purl.toString());
69+
}
70+
5871
@Test
5972
public void testConstructorParsing() throws Exception {
6073
exception = ExpectedException.none();

0 commit comments

Comments
 (0)