From 7f4c447c4a1990d5a9a094f671b221f248be01b6 Mon Sep 17 00:00:00 2001 From: David Walluck Date: Thu, 20 Feb 2025 16:13:54 -0500 Subject: [PATCH] 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. Fixes #150 Closes #153 Fixes #154 --- .../com/github/packageurl/PackageURL.java | 60 +++++++++++-------- .../com/github/packageurl/PackageURLTest.java | 13 ++++ 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index 4824b42..3b5b303 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -21,6 +21,7 @@ */ package com.github.packageurl; +import java.io.ByteArrayOutputStream; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; @@ -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) { @@ -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; } /** diff --git a/src/test/java/com/github/packageurl/PackageURLTest.java b/src/test/java/com/github/packageurl/PackageURLTest.java index d7ddf18..5981bac 100644 --- a/src/test/java/com/github/packageurl/PackageURLTest.java +++ b/src/test/java/com/github/packageurl/PackageURLTest.java @@ -55,6 +55,19 @@ 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/summit-ast@2.2.0%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"); + 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();