diff --git a/README.md b/README.md index 761675f..c44050d 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ compared to other solutions like the standard `URLEncoder` in the JDK or ```java UrlEncoder.encode("a test &"); // -> "a%20test%20%26" UrlEncoder.encode("%#okékÉȢ smile!😁"); // -> "%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81" -UrlEncoder.encode("?test=a test", "?="); // -> ?test=a%20test -UrlEncoder.encode("foo bar", true); // -> foo+bar (encode space to +) +UrlEncoder.encode("?test=a test", "?="); // -> "?test=a%20test" +UrlEncoder.encode("foo bar", true); // -> "foo+bar" (encode space to +) UrlEncoder.decode("a%20test%20%26"); // -> "a test &" UrlEncoder.decode("%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81"); // -> "%#okékÉȢ smile!😁" +UrlEncoder.decode("foo+bar", true); // -> "foo bar" (decode + to space) ``` ## Gradle, Maven, etc. diff --git a/lib/src/main/java/com/uwyn/urlencoder/UrlEncoder.java b/lib/src/main/java/com/uwyn/urlencoder/UrlEncoder.java index c5aff0a..27b9a2f 100644 --- a/lib/src/main/java/com/uwyn/urlencoder/UrlEncoder.java +++ b/lib/src/main/java/com/uwyn/urlencoder/UrlEncoder.java @@ -69,6 +69,20 @@ public final class UrlEncoder { * @since 1.0 */ public static String decode(String source) { + return decode(source, false); + } + + /** + * Transforms a provided String URL into a new string, + * containing decoded URL characters in the UTF-8 encoding. + * + * @param source The string URL that has to be decoded + * @param plusToSpace Convert any {@code +} to space. + * @return The decoded String object. + * @see #encode(String, String) + * @since 1.0 + */ + public static String decode(String source, boolean plusToSpace) { if (source == null || source.isEmpty()) { return source; } @@ -83,10 +97,7 @@ public final class UrlEncoder { ch = source.charAt(i); if (ch == '%') { - if (out == null) { - out = new StringBuilder(length); - out.append(source, 0, i); - } + out = startConstructingIfNeeded(out, source, i); if (bytes_buffer == null) { // the remaining characters divided by the length @@ -119,7 +130,10 @@ public final class UrlEncoder { bytes_pos = 0; } - if (out != null) { + if (plusToSpace && ch == '+') { + out = startConstructingIfNeeded(out, source, i); + out.append(" "); + } else if (out != null) { out.append(ch); } @@ -138,6 +152,14 @@ public final class UrlEncoder { return out.toString(); } + private static StringBuilder startConstructingIfNeeded(StringBuilder out, String source, int currentSourcePosition) { + if (out == null) { + out = new StringBuilder(source.length()); + out.append(source, 0, currentSourcePosition); + } + return out; + } + /** * Transforms a provided String object into a new string, * containing only valid URL characters in the UTF-8 encoding. @@ -210,31 +232,28 @@ public final class UrlEncoder { } i += 1; } else { - if (out == null) { - out = new StringBuilder(source.length()); - out.append(source, 0, i); - } - if (spaceToPlus && ch == ' ') { - out.append('+'); - i += 1; - } else { - var cp = source.codePointAt(i); - if (cp < 0x80) { + out = startConstructingIfNeeded(out, source, i); + + var cp = source.codePointAt(i); + if (cp < 0x80) { + if (spaceToPlus && ch == ' ') { + out.append('+'); + } else { appendUrlEncodedByte(out, cp); - i += 1; - } else if (Character.isBmpCodePoint(cp)) { - for (var b : Character.toString(ch).getBytes(StandardCharsets.UTF_8)) { - appendUrlEncodedByte(out, b); - } - i += 1; - } else if (Character.isSupplementaryCodePoint(cp)) { - var high = Character.highSurrogate(cp); - var low = Character.lowSurrogate(cp); - for (var b : new String(new char[]{high, low}).getBytes(StandardCharsets.UTF_8)) { - appendUrlEncodedByte(out, b); - } - i += 2; } + i += 1; + } else if (Character.isBmpCodePoint(cp)) { + for (var b : Character.toString(ch).getBytes(StandardCharsets.UTF_8)) { + appendUrlEncodedByte(out, b); + } + i += 1; + } else if (Character.isSupplementaryCodePoint(cp)) { + var high = Character.highSurrogate(cp); + var low = Character.lowSurrogate(cp); + for (var b : new String(new char[]{high, low}).getBytes(StandardCharsets.UTF_8)) { + appendUrlEncodedByte(out, b); + } + i += 2; } } } diff --git a/lib/src/test/java/com/uwyn/urlencoder/UrlEncoderTest.java b/lib/src/test/java/com/uwyn/urlencoder/UrlEncoderTest.java index 62e1db5..be7408d 100644 --- a/lib/src/test/java/com/uwyn/urlencoder/UrlEncoderTest.java +++ b/lib/src/test/java/com/uwyn/urlencoder/UrlEncoderTest.java @@ -101,10 +101,17 @@ class UrlEncoderTest { assertEquals("foo bar", UrlEncoder.encode("foo bar", " ", true)); } + @Test + void testDecodePlusToSpace() { + assertEquals("foo bar", UrlEncoder.decode("foo+bar", true)); + assertEquals("foo bar foo", UrlEncoder.decode("foo+bar++foo", true)); + assertEquals("foo bar foo", UrlEncoder.decode("foo+%20bar%20+foo", true)); + } + @ParameterizedTest(name = "processMain(-d {1}) should be {0}") @MethodSource("validMap") void testMainDecode(String expected, String source) { - var result = UrlEncoder.processMain(new String[]{"-d", source}); + var result = UrlEncoder.processMain("-d", source); assertEquals(expected, result.output); assertEquals(0, result.status, "processMain(-d " + source + ").status"); } @@ -112,7 +119,7 @@ class UrlEncoderTest { @ParameterizedTest(name = "processMain(-e {0})") @MethodSource("validMap") void testMainEncode(String source, String expected) { - var result = UrlEncoder.processMain(new String[]{source}); + var result = UrlEncoder.processMain(source); assertEquals(expected, result.output); assertEquals(0, result.status, "processMain(-e " + source + ").status"); } @@ -120,20 +127,20 @@ class UrlEncoderTest { @ParameterizedTest(name = "processMain(-d {0})") @MethodSource("invalid") void testMainEncodeWithExceptions(String source) { - assertThrows(IllegalArgumentException.class, () -> UrlEncoder.processMain(new String[]{"-d", source}), source); + assertThrows(IllegalArgumentException.class, () -> UrlEncoder.processMain("-d", source), source); } @Test void testMainTooManyArgs() { - assertTrue(UrlEncoder.processMain(new String[]{"foo", "bar", "test"}).output.contains("Usage :"), "too many args"); + assertTrue(UrlEncoder.processMain("foo", "bar", "test").output.contains("Usage :"), "too many args"); } @Test void testMainWithEmptyArgs() { - assertTrue(UrlEncoder.processMain(new String[]{" ", " "}).output.contains("Usage :"), "processMain(' ', ' ')"); - assertTrue(UrlEncoder.processMain(new String[]{"foo", " "}).output.contains("Usage :"), "processMain('foo', ' ')"); - assertTrue(UrlEncoder.processMain(new String[]{" ", "foo"}).output.contains("Usage :"), "processMain(' ', 'foo')"); - assertTrue(UrlEncoder.processMain(new String[]{"-d ", ""}).output.contains("Usage :"), "processMain('-d', '')"); + assertTrue(UrlEncoder.processMain(" ", " ").output.contains("Usage :"), "processMain(' ', ' ')"); + assertTrue(UrlEncoder.processMain("foo", " ").output.contains("Usage :"), "processMain('foo', ' ')"); + assertTrue(UrlEncoder.processMain(" ", "foo").output.contains("Usage :"), "processMain(' ', 'foo')"); + assertTrue(UrlEncoder.processMain("-d ", "").output.contains("Usage :"), "processMain('-d', '')"); assertEquals("%20", UrlEncoder.processMain(new String[]{"-e", " "}).output, "processMain('-e', ' ')"); assertEquals(" ", UrlEncoder.processMain(new String[]{"-d", " "}).output, "processMain('-d', ' ')"); } @@ -141,7 +148,7 @@ class UrlEncoderTest { @ParameterizedTest @ValueSource(strings = {"", "-d", "-e"}) void testMainWithInvalidArgs(String arg) { - var result = UrlEncoder.processMain(new String[]{arg}); + var result = UrlEncoder.processMain(arg); assertTrue(result.output.contains("Usage :"), "processMain('" + arg + "')"); assertEquals(1, result.status, "processMain('" + arg + "').status"); } @@ -149,14 +156,14 @@ class UrlEncoderTest { @ParameterizedTest(name = "processMain(-e {0})") @MethodSource("validMap") void testMainWithOption(String source, String expected) { - var result = UrlEncoder.processMain(new String[]{"-e", source}); + var result = UrlEncoder.processMain("-e", source); assertEquals(expected, result.output); assertEquals(0, result.status, "processMain(-e " + source + ").status"); } @Test void testMainWithUnknownOptions() { - assertTrue(UrlEncoder.processMain(new String[]{"-p"}).output.contains("Usage :"), "processMain(-p)"); - assertTrue(UrlEncoder.processMain(new String[]{"-"}).output.contains("Usage :"), "processMain(-)"); + assertTrue(UrlEncoder.processMain("-p").output.contains("Usage :"), "processMain(-p)"); + assertTrue(UrlEncoder.processMain("-").output.contains("Usage :"), "processMain(-)"); } } \ No newline at end of file