Repository: helidon-io/helidon (java)

Issue:
Query params with URI encoded enclosed in brackets is not decoded
## Environment Details
* Helidon Version: 4.0.10
* Helidon SE

----------

## Problem Description

If a query parameter contains URI encoded content enclosed by square brackets, the enclosed content is not decoded.
E.g. `http://localhost:8080?msg='[Hello%20World]'`

## Steps to reproduce

Add this test to `UriQueryTest` under `common/uri`:
```java
@Test
void testEncodedWithinBrackets() {
    UriQuery uriQuery = UriQuery.create("msg=[Hello%20World]");
    assertThat(uriQuery.get("msg"), is("[Hello World]"));
}
```



Pull request:
4.0.11: Update UriEncoding.decode to expose a decodeQuery method

### Description

Backport #9006 to 4.0.11

* UriEncoding.decodeQuery does not ignore %s within []
* UriEncoding.decodeUri ignores %s within [] to support IPv6 literal (E.g. http://[fe80::1%lo0]:8080)

See https://tools.ietf.org/html/rfc6874#section-2

Fixes #8993

### Documentation

No impact

Patch:
diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java b/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java
index 70a5e1f213..f7abb5e1ce 100644
--- a/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java
+++ b/common/uri/src/main/java/io/helidon/common/uri/UriEncoding.java
@@ -43,19 +43,27 @@ public final class UriEncoding {
 
     /**
      * Decode a URI segment.
+     * <p>
+     * Percent characters {@code "%s"} found between brackets {@code "[]"} are not decoded to support IPv6 literal.
+     * E.g. {@code http://[fe80::1%lo0]:8080}.
+     * <p>
+     * See <a href="https://tools.ietf.org/html/rfc6874#section-2">RFC 6874, section 2.</a>
      *
      * @param uriSegment URI segment with percent encoding
      * @return decoded string
      */
     public static String decodeUri(String uriSegment) {
-        if (uriSegment.isEmpty()) {
-            return "";
-        }
-        if (uriSegment.indexOf('%') == -1 && uriSegment.indexOf('+') == -1) {
-            return uriSegment;
-        }
+        return decodeUri(uriSegment, true);
+    }
 
-        return decode(uriSegment);
+    /**
+     * Decode a URI query.
+     *
+     * @param uriQuery URI query with percent encoding
+     * @return decoded string
+     */
+    public static String decodeQuery(String uriQuery) {
+        return decodeUri(uriQuery, false);
     }
 
     /**
@@ -123,7 +131,18 @@ public final class UriEncoding {
         appender.append(HEX_DIGITS[b & 0x0F]);
     }
 
-    private static String decode(String string) {
+    private static String decodeUri(String uriSegment, boolean ignorePercentInBrackets) {
+        if (uriSegment.isEmpty()) {
+            return "";
+        }
+        if (uriSegment.indexOf('%') == -1 && uriSegment.indexOf('+') == -1) {
+            return uriSegment;
+        }
+        return decode(uriSegment, ignorePercentInBrackets);
+    }
+
+    // see java.net.URI.decode(String, boolean)
+    private static String decode(String string, boolean ignorePercentInBrackets) {
         int len = string.length();
 
         StringBuilder sb = new StringBuilder(len);
@@ -141,7 +160,7 @@ public final class UriEncoding {
             } else if (betweenBrackets && c == ']') {
                 betweenBrackets = false;
             }
-            if (c != '%' || betweenBrackets) {
+            if (c != '%' || (betweenBrackets && ignorePercentInBrackets)) {
                 sb.append(c == '+' && !betweenBrackets ? ' ' : c);      // handles '+' decoding
                 if (++i >= len) {
                     break;
diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java
index dbe14f2265..fd6544cc80 100644
--- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java
+++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryImpl.java
@@ -31,7 +31,7 @@ import io.helidon.common.mapper.MapperManager;
 import io.helidon.common.mapper.OptionalValue;
 import io.helidon.common.mapper.Value;
 
-import static io.helidon.common.uri.UriEncoding.decodeUri;
+import static io.helidon.common.uri.UriEncoding.decodeQuery;
 
 // must be lazily populated to prevent perf overhead when queries are ignored
 final class UriQueryImpl implements UriQuery {
@@ -215,11 +215,11 @@ final class UriQueryImpl implements UriQuery {
     private void addDecoded(Map<String, List<String>> newQueryParams, String next) {
         int eq = next.indexOf('=');
         if (eq == -1) {
-            newQueryParams.putIfAbsent(decodeUri(next), new LinkedList<>());
+            newQueryParams.putIfAbsent(decodeQuery(next), new LinkedList<>());
         } else {
             String name = next.substring(0, eq);
             String value = next.substring(eq + 1);
-            newQueryParams.computeIfAbsent(decodeUri(name), it -> new LinkedList<>()).add(decodeUri(value));
+            newQueryParams.computeIfAbsent(decodeQuery(name), it -> new LinkedList<>()).add(decodeQuery(value));
         }
     }
 


Test patch:
diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java
index 56d791e35d..e8c1214d97 100644
--- a/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java
+++ b/common/uri/src/test/java/io/helidon/common/uri/UriEncodingTest.java
@@ -30,4 +30,9 @@ class UriEncodingTest {
         assertThat(decodeUri("+hello+world+"), is(" hello world "));
         assertThat(decodeUri("[+]hello[+]world[+]"), is("[+]hello[+]world[+]"));
     }
+
+    @Test
+    void testIPv6Literal() {
+        assertThat(decodeUri("http://[fe80::1%lo0]:8080"), is("http://[fe80::1%lo0]:8080"));
+    }
 }
diff --git a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java
index edb01dd62b..091c4473b5 100644
--- a/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java
+++ b/common/uri/src/test/java/io/helidon/common/uri/UriQueryTest.java
@@ -16,7 +16,6 @@
 
 package io.helidon.common.uri;
 
-import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLEncoder;
 
@@ -41,11 +40,17 @@ class UriQueryTest {
     }
 
     @Test
-    void testEncoded() throws UnsupportedEncodingException {
+    void testEncoded() {
         UriQuery uriQuery = UriQuery.create("a=" + URLEncoder.encode("1&b=2", US_ASCII));
         assertThat(uriQuery.get("a"), is("1&b=2"));
     }
 
+    @Test
+    void testEncodedWithinBrackets() {
+        UriQuery uriQuery = UriQuery.create("msg=[Hello%20World]");
+        assertThat(uriQuery.get("msg"), is("[Hello World]"));
+    }
+
     @Test
     void testEncodedOtherChars() {
         UriQuery uriQuery = UriQuery.create("a=b%26c=d&e=f&e=g&h=x%63%23e%3c");