Skip to content

Commit

Permalink
Fix URL encoding and decoding
Browse files Browse the repository at this point in the history
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.

Fixes package-url#150
Closes package-url#153
Fixes package-url#154
  • Loading branch information
dwalluck committed Feb 20, 2025
1 parent ee6dda9 commit 26d8498
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 26 deletions.
60 changes: 34 additions & 26 deletions src/main/java/com/github/packageurl/PackageURL.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
package com.github.packageurl;

import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
Expand Down Expand Up @@ -441,22 +442,26 @@ private String percentEncode(final String input) {
}

private static String uriEncode(String source, Charset charset) {
if (source == null || source.length() == 0) {
if (source == null || source.isEmpty()) {
return source;
}

boolean changed = false;
StringBuilder builder = new StringBuilder();
for (byte b : source.getBytes(charset)) {
byte[] bytes = source.getBytes(charset);

for (byte b : bytes) {
if (isUnreserved(b)) {
builder.append((char) b);
}
else {
// Substitution: A '%' followed by the hexadecimal representation of the ASCII value of the replaced character
builder.append('%');
builder.append(Integer.toHexString(b).toUpperCase());
builder.append(Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)));
builder.append(Character.toUpperCase(Character.forDigit(b & 0xF, 16)));
changed = true;
}
}
return builder.toString();
return changed ? builder.toString() : source;
}

private static boolean isUnreserved(int c) {
Expand All @@ -479,34 +484,37 @@ private static boolean isDigit(int c) {
* @return a decoded String
*/
private String percentDecode(final String input) {
if (input == null) {
return null;
}
final String decoded = uriDecode(input);
if (!decoded.equals(input)) {
return decoded;
}
return input;
return uriDecode(input);
}

public static String uriDecode(String source) {
if (source == null) {
return source;
return null;
}
int length = source.length();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
if (source.charAt(i) == '%') {
String str = source.substring(i + 1, i + 3);
char c = (char) Integer.parseInt(str, 16);
builder.append(c);
i += 2;
}
else {
builder.append(source.charAt(i));

boolean changed = false;
byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream buffer = new ByteArrayOutputStream(bytes.length);

for (int i = 0; i < bytes.length; i++) {
int b = bytes[i];

if (b == '%') {
if (i + 2 >= bytes.length) {
return source;
}

int b1 = Character.digit(bytes[++i], 16);
int b2 = Character.digit(bytes[++i], 16);
buffer.write((char) ((b1 << 4) + b2));
changed = true;
} else {
buffer.write(b);
}
}
return builder.toString();

byte[] b = buffer.toByteArray();
return changed ? new String(b, StandardCharsets.UTF_8) : source;
}

/**
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/com/github/packageurl/PackageURLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.TreeMap;

import org.apache.commons.io.IOUtils;
Expand Down Expand Up @@ -55,6 +57,26 @@ public static void setup() throws IOException {
json = new JSONArray(jsonTxt);
}

@Test
public void testEncoding1() throws MalformedPackageURLException {
PackageURL purl = new PackageURL("maven", "com.google.summit", "summit-ast", "2.2.0\n", null, null);
Assert.assertEquals("pkg:maven/com.google.summit/[email protected]%0A", purl.toString());
}

@Test
public void testEncoding2() throws MalformedPackageURLException {
PackageURL purl = new PackageURL("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5");
// d09c6963726f73d0be66742ed0956e746974d1834672616dd0b5776f726bd0a1d0be72d0b5

byte[] ba = purl.getName().getBytes(StandardCharsets.UTF_8);
StringBuilder str = new StringBuilder();
for (byte b : ba) str.append(String.format("%x", b));
String s = str.toString();
System.out.println("XXX: " + s);
Assert.assertEquals("Мicrosоft.ЕntitуFramеworkСоrе", purl.getName());
Assert.assertEquals("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5", purl.toString());
}

@Test
public void testConstructorParsing() throws Exception {
exception = ExpectedException.none();
Expand Down

0 comments on commit 26d8498

Please sign in to comment.