diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java b/flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java index 042f3700a05..02ed7dedfb7 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/UrlUtil.java @@ -17,6 +17,7 @@ import jakarta.servlet.http.HttpServletRequest; +import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -147,6 +148,10 @@ public static String encodeURIComponent(String path) { * space character, making it suitable for decoding strings encoded with * JavaScript's {@code encodeURIComponent()} or * {@link #encodeURIComponent(String)}. + *
+ * Only percent-encoded escapes are decoded. Characters that are not escaped
+ * are kept as they are, so an already decoded string containing for example
+ * {@code ü} is returned unchanged.
*
* @param encoded
* the percent-encoded string
@@ -159,40 +164,47 @@ public static String decodeURIComponent(String encoded) {
}
Matcher matcher = PERCENT_ENCODED.matcher(encoded);
+ if (!matcher.find()) {
+ // Nothing is percent-encoded, so the input is already decoded
+ return encoded;
+ }
+
StringBuilder result = new StringBuilder();
+ // Consecutive escapes are collected so that a multi-byte UTF-8
+ // character split over several escapes is decoded as one character
+ ByteArrayOutputStream escapedBytes = new ByteArrayOutputStream();
int lastEnd = 0;
- while (matcher.find()) {
- // Append text before the match
- result.append(encoded, lastEnd, matcher.start());
-
- // Decode the hex value
- String hex = matcher.group(1);
- int value = Integer.parseInt(hex, 16);
- result.append((char) value);
-
+ do {
+ if (matcher.start() != lastEnd) {
+ // Text between two escapes ends the current byte sequence
+ appendDecoded(result, escapedBytes);
+ result.append(encoded, lastEnd, matcher.start());
+ }
+ escapedBytes.write(Integer.parseInt(matcher.group(1), 16));
lastEnd = matcher.end();
- }
+ } while (matcher.find());
+
+ appendDecoded(result, escapedBytes);
- // Append remaining text
+ // Append remaining text, which is not encoded and thus kept as-is
result.append(encoded, lastEnd, encoded.length());
- // Handle multi-byte UTF-8 sequences
- byte[] bytes = new byte[result.length()];
- boolean hasMultibyte = false;
- for (int i = 0; i < result.length(); i++) {
- char c = result.charAt(i);
- if (c > 127) {
- hasMultibyte = true;
- }
- bytes[i] = (byte) c;
- }
+ return result.toString();
+ }
- if (hasMultibyte) {
- return new String(bytes, StandardCharsets.UTF_8);
+ /**
+ * Decodes the collected percent-encoded bytes as UTF-8 into the given
+ * builder and resets the byte sequence. Characters that were not
+ * percent-encoded are appended separately so that they are not mistaken for
+ * UTF-8 bytes.
+ */
+ private static void appendDecoded(StringBuilder result,
+ ByteArrayOutputStream escapedBytes) {
+ if (escapedBytes.size() > 0) {
+ result.append(escapedBytes.toString(StandardCharsets.UTF_8));
+ escapedBytes.reset();
}
-
- return result.toString();
}
/**
diff --git a/flow-server/src/main/java/com/vaadin/flow/router/internal/PathUtil.java b/flow-server/src/main/java/com/vaadin/flow/router/internal/PathUtil.java
index 4d981eef4ce..138b2eaaba0 100644
--- a/flow-server/src/main/java/com/vaadin/flow/router/internal/PathUtil.java
+++ b/flow-server/src/main/java/com/vaadin/flow/router/internal/PathUtil.java
@@ -68,6 +68,13 @@ public static List
+ * The path is expected to be percent-encoded. A path that is already
+ * decoded, such as the one of a servlet request or the one an application
+ * passes to {@link com.vaadin.flow.component.UI#navigate(String)}, is
+ * decoded a second time here, which consumes a percent sign that the path
+ * contains as a character of its own. See
+ * #25690.
*
* @param path
* url path to split into segments and decode. The path may also
diff --git a/flow-server/src/test/java/com/vaadin/flow/internal/ResourceFolderUtilTest.java b/flow-server/src/test/java/com/vaadin/flow/internal/ResourceFolderUtilTest.java
index 7a45be2fcfe..e5aa8889510 100644
--- a/flow-server/src/test/java/com/vaadin/flow/internal/ResourceFolderUtilTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/internal/ResourceFolderUtilTest.java
@@ -84,6 +84,26 @@ void folderPathContainsSpace_filesInTheJarAreVisited() throws IOException {
assertEquals(List.of("one.txt"), names);
}
+ @Test
+ void folderPathContainsLiteralNonAsciiCharacter_filesInTheJarAreVisited()
+ throws IOException {
+ File jar = new File(temporaryFolder, "themes.jar");
+ try (JarOutputStream jarStream = new JarOutputStream(
+ new FileOutputStream(jar))) {
+ writeEntry(jarStream, "thèmes/");
+ writeEntry(jarStream, "thèmes/one.txt");
+ }
+
+ // A jar URL does not have to be percent-encoded, so the entry name can
+ // reach the utility with the characters it has in the jar
+ List