diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 834783122694..c76ce4c9efc7 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -16,7 +16,6 @@ package org.springframework.util; -import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.ArrayList; @@ -25,6 +24,7 @@ import java.util.Collections; import java.util.Deque; import java.util.Enumeration; +import java.util.HexFormat; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -816,38 +816,50 @@ public static boolean pathEquals(String path1, String path2) { * @see java.net.URLDecoder#decode(String, String) */ public static String uriDecode(String source, Charset charset) { + Assert.notNull(charset, "Charset must not be null"); int length = source.length(); if (length == 0) { return source; } - Assert.notNull(charset, "Charset must not be null"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + StringBuilder output = new StringBuilder(length); boolean changed = false; - for (int i = 0; i < length; i++) { - int ch = source.charAt(i); + byte[] bytes = null; + int i = 0; + while (i < length) { + char ch = source.charAt(i); if (ch == '%') { - if (i + 2 < length) { - char hex1 = source.charAt(i + 1); - char hex2 = source.charAt(i + 2); - int u = Character.digit(hex1, 16); - int l = Character.digit(hex2, 16); - if (u == -1 || l == -1) { - throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + try { + if (bytes == null) { + bytes = new byte[(length - i) / 3]; + } + + int pos = 0; + while (i + 2 < length && ch == '%') { + bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3); + i += 3; + if (i < length) { + ch = source.charAt(i); + } } - baos.write((char) ((u << 4) + l)); - i += 2; + + if (i < length && ch == '%') { + throw new IllegalArgumentException("Incomplete trailing escape (%) pattern"); + } + + output.append(new String(bytes, 0, pos, charset)); changed = true; } - else { + catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); } } else { - baos.write(ch); + output.append(ch); + i++; } } - return (changed ? StreamUtils.copyToString(baos, charset) : source); + return (changed ? output.toString() : source); } /** diff --git a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java index 16c4e131bbf2..4edcd64430ff 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriUtilsTests.java @@ -107,12 +107,19 @@ void decode() { assertThat(UriUtils.decode("T%C5%8Dky%C5%8D", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d"); assertThat(UriUtils.decode("/Z%C3%BCrich", CHARSET)).as("Invalid encoded result").isEqualTo("/Z\u00fcrich"); assertThat(UriUtils.decode("T\u014dky\u014d", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d"); + assertThat(UriUtils.decode("%20\u2019", CHARSET)).as("Invalid encoded result").isEqualTo(" \u2019"); } @Test void decodeInvalidSequence() { assertThatIllegalArgumentException().isThrownBy(() -> UriUtils.decode("foo%2", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("foo%", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("%", CHARSET)); + assertThatIllegalArgumentException().isThrownBy(() -> + UriUtils.decode("%zz", CHARSET)); } @Test