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 c76ce4c9ef..b2a2633fe2 100644
--- a/spring-core/src/main/java/org/springframework/util/StringUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java
@@ -800,32 +800,27 @@ public abstract class StringUtils {
}
/**
- * Decode the given encoded URI component value. Based on the following rules:
- *
- * - Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"},
- * and {@code "0"} through {@code "9"} stay the same.
- * - Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
- * - A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
- * - For all other characters (including those already decoded), the output is undefined.
- *
- * @param source the encoded String
- * @param charset the character set
+ * Decode the given encoded URI component value by replacing "{@code %xy}" sequences
+ * by an hexadecimal representation of the character in the specified charset, letting other
+ * characters unchanged.
+ * @param source the encoded {@code String}
+ * @param charset the character encoding to use to decode the "{@code %xy}" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
- * @see java.net.URLDecoder#decode(String, String)
+ * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String uriDecode(String source, Charset charset) {
- Assert.notNull(charset, "Charset must not be null");
int length = source.length();
- if (length == 0) {
+ int firstPercentIndex = source.indexOf('%');
+ if (length == 0 || firstPercentIndex < 0) {
return source;
}
StringBuilder output = new StringBuilder(length);
- boolean changed = false;
+ output.append(source, 0, firstPercentIndex);
byte[] bytes = null;
- int i = 0;
+ int i = firstPercentIndex;
while (i < length) {
char ch = source.charAt(i);
if (ch == '%') {
@@ -848,7 +843,6 @@ public abstract class StringUtils {
}
output.append(new String(bytes, 0, pos, charset));
- changed = true;
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
@@ -859,7 +853,7 @@ public abstract class StringUtils {
i++;
}
}
- return (changed ? output.toString() : source);
+ return output.toString();
}
/**
diff --git a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
index de159baad4..a0ef085a07 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -374,15 +374,16 @@ public abstract class UriUtils {
}
/**
- * Decode the given encoded URI component.
- * See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules.
- * @param source the encoded String
- * @param charset the character encoding to use
+ * Decode the given encoded URI component value by replacing "{@code %xy}" sequences
+ * by an hexadecimal representation of the character in the specified charset, letting other
+ * characters unchanged.
+ * @param source the encoded {@code String}
+ * @param charset the character encoding to use to decode the "{@code %xy}" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
* @see StringUtils#uriDecode(String, Charset)
- * @see java.net.URLDecoder#decode(String, String)
+ * @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String decode(String source, Charset charset) {
return StringUtils.uriDecode(source, charset);
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 4edcd64430..edb148b6ea 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -108,6 +108,8 @@ class UriUtilsTests {
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");
+ assertThat(UriUtils.decode("\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo("śpřìńġ");
+ assertThat(UriUtils.decode("%20\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo(" śpřìńġ");
}
@Test