This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 07b1c842507 branch-4.2: [fix](function) Preserve invalid URL decoded 
bytes (#67841) (#69107)
07b1c842507 is described below

commit 07b1c84250758e839e6835cdaff3168884bdedf1
Author: morrySnow <[email protected]>
AuthorDate: Thu Oct 15 19:07:35 2026 +0800

    branch-4.2: [fix](function) Preserve invalid URL decoded bytes (#67841) 
(#69107)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    Related PR: https://github.com/apache/doris/pull/67841
    
    Problem Summary: FE URL_DECODE constant folding silently replaced
    invalid UTF-8 decoded bytes, unlike BE byte-preserving execution.
    Validate the decoded byte stream and defer malformed UTF-8 to BE while
    retaining valid UTF-8 folding.
    
    ### Release note
    
    Preserve invalid UTF-8 bytes in URL_DECODE results consistently with BE
    execution.
    
    ### Backport validation
    
    - Test: Unit Test: StringArithmeticTest; FE Checkstyle (0 violations).
    - Combined eight-backport validation: 283 tests across 13 classes, 0
    failures/errors after correcting identity-cast assertions. This
    standalone backport has not had a separate unit-test run.
    - Targeted regression: url_decode_invalid_utf8 passed through
    run-regression-test.sh on the combined eight-backport ASAN deployment.
    This standalone branch and the full regression suite were not tested
    separately.
    - Full build.sh --fe and ASAN build.sh --be succeeded on the combined
    eight-backport tree; deployment smoke validation passed.
    - Behavior changed: Yes (invalid UTF-8 decoded bytes are preserved)
    - Does this need documentation: No
    
    (cherry picked from commit 366fd37b283e55916bbe1a951e703a78a2f7fa3d)
    
    ### Scope
    
    This is an independent backport of #67841 onto branch-4.2.
    The branch contains one commit and does not include the other seven
    picks.
    The 283-test result above was obtained from the combined backport tree;
    this standalone branch has not had a separate unit-test run.
    
    ### Check List (For Author)
    
    - Test:
    - [x] Unit Test: 283 targeted tests passed on the combined backport
    tree.
    - [x] Manual test: standalone patch matches the validated pick; FE
    Checkstyle passed on the combined tree.
    - [x] Regression test: url_decode_invalid_utf8 passed on the combined
    eight-backport ASAN deployment; this standalone branch was not tested
    separately. The full regression suite was not run.
    - Behavior changed: see the backport validation details above.
    - Does this need documentation: No.
---
 .../functions/executable/StringArithmetic.java     | 51 ++++++++++++++++++++++
 .../functions/executable/StringArithmeticTest.java | 25 +++++++++++
 .../scalar_function/url_decode_invalid_utf8.out    |  9 ++++
 .../scalar_function/url_decode_invalid_utf8.groovy | 39 +++++++++++++++++
 4 files changed, 124 insertions(+)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
index 1f86b5e19cb..db0742117fa 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmetic.java
@@ -45,10 +45,14 @@ import org.apache.doris.nereids.types.ArrayType;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
+import java.io.ByteArrayOutputStream;
 import java.io.UnsupportedEncodingException;
 import java.math.BigInteger;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -1070,6 +1074,11 @@ public class StringArithmetic {
      */
     @ExecFunction(name = "url_decode")
     public static Expression urlDecode(StringLikeLiteral first) {
+        if (!isValidUtf8AfterUrlDecode(first.getValue())) {
+            // String literals cannot preserve an invalid UTF-8 byte sequence. 
Let BE evaluate it
+            // instead of folding the replacement characters produced by 
java.net.URLDecoder.
+            throw new IllegalArgumentException("URL-decoded value is not valid 
UTF-8");
+        }
         try {
             return castStringLikeLiteral(first, 
URLDecoder.decode(first.getValue(), StandardCharsets.UTF_8.name()));
         } catch (UnsupportedEncodingException e) {
@@ -1077,6 +1086,48 @@ public class StringArithmetic {
         }
     }
 
+    private static boolean isValidUtf8AfterUrlDecode(String value) {
+        ByteArrayOutputStream decodedBytes = new 
ByteArrayOutputStream(value.length());
+        int index = 0;
+        while (index < value.length()) {
+            char current = value.charAt(index);
+            if (current == '%') {
+                if (index + 2 >= value.length()) {
+                    // Preserve URLDecoder's existing malformed-escape 
handling.
+                    return true;
+                }
+                int high = Character.digit(value.charAt(index + 1), 16);
+                int low = Character.digit(value.charAt(index + 2), 16);
+                if (high < 0 || low < 0) {
+                    // Preserve URLDecoder's existing malformed-escape 
handling.
+                    return true;
+                }
+                decodedBytes.write((high << 4) + low);
+                index += 3;
+            } else if (current == '+') {
+                decodedBytes.write(' ');
+                index++;
+            } else {
+                int start = index;
+                while (index < value.length() && value.charAt(index) != '%' && 
value.charAt(index) != '+') {
+                    index++;
+                }
+                byte[] originalBytes = value.substring(start, 
index).getBytes(StandardCharsets.UTF_8);
+                decodedBytes.write(originalBytes, 0, originalBytes.length);
+            }
+        }
+
+        try {
+            StandardCharsets.UTF_8.newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPORT)
+                    .onUnmappableCharacter(CodingErrorAction.REPORT)
+                    .decode(ByteBuffer.wrap(decodedBytes.toByteArray()));
+            return true;
+        } catch (CharacterCodingException e) {
+            return false;
+        }
+    }
+
     /**
      * Executable arithmetic functions urlencode
      */
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
index 985b0029b82..5381bc8e062 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/StringArithmeticTest.java
@@ -17,9 +17,14 @@
 
 package org.apache.doris.nereids.trees.expressions.functions.executable;
 
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.ExpressionEvaluator;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.UrlDecode;
 import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.FloatLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
 
 import org.junit.jupiter.api.Assertions;
@@ -74,4 +79,24 @@ class StringArithmeticTest {
 
         Assertions.assertEquals(2, result.getValue());
     }
+
+    @Test
+    void testUrlDecodeDoesNotFoldInvalidUtf8() {
+        String[] invalidUtf8Values = {"%80", "%C0%AF", "%E0%80%80", 
"%ED%A0%80", "%FF"};
+        for (String value : invalidUtf8Values) {
+            UrlDecode urlDecode = new UrlDecode(new StringLiteral(value));
+            Assertions.assertSame(urlDecode, 
ExpressionEvaluator.INSTANCE.eval(urlDecode), value);
+        }
+    }
+
+    @Test
+    void testUrlDecodeStillFoldsValidUtf8() {
+        assertUrlDecodeValue("%E4%B8%AD+text", "中 text");
+        assertUrlDecodeValue("%EF%BF%BD", "�");
+    }
+
+    private void assertUrlDecodeValue(String encoded, String expected) {
+        Expression result = ExpressionEvaluator.INSTANCE.eval(new 
UrlDecode(new StringLiteral(encoded)));
+        Assertions.assertEquals(expected, ((StringLikeLiteral) 
result).getValue());
+    }
 }
diff --git 
a/regression-test/data/nereids_function_p0/scalar_function/url_decode_invalid_utf8.out
 
b/regression-test/data/nereids_function_p0/scalar_function/url_decode_invalid_utf8.out
new file mode 100644
index 00000000000..3a1c28a3f41
--- /dev/null
+++ 
b/regression-test/data/nereids_function_p0/scalar_function/url_decode_invalid_utf8.out
@@ -0,0 +1,9 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !invalid_utf8_constant --
+80     C0AF    E08080  EDA080  FF
+
+-- !invalid_utf8_runtime --
+80     C0AF    E08080  EDA080  FF
+
+-- !valid_utf8_constant --
+中 text EFBFBD
diff --git 
a/regression-test/suites/nereids_function_p0/scalar_function/url_decode_invalid_utf8.groovy
 
b/regression-test/suites/nereids_function_p0/scalar_function/url_decode_invalid_utf8.groovy
new file mode 100644
index 00000000000..6c9de7d9b03
--- /dev/null
+++ 
b/regression-test/suites/nereids_function_p0/scalar_function/url_decode_invalid_utf8.groovy
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("url_decode_invalid_utf8") {
+    qt_invalid_utf8_constant """
+        SELECT HEX(URL_DECODE('%80')),
+               HEX(URL_DECODE('%C0%AF')),
+               HEX(URL_DECODE('%E0%80%80')),
+               HEX(URL_DECODE('%ED%A0%80')),
+               HEX(URL_DECODE('%FF'))
+    """
+
+    qt_invalid_utf8_runtime """
+        SELECT HEX(URL_DECODE(IF(number = 0, '%80', ''))),
+               HEX(URL_DECODE(IF(number = 0, '%C0%AF', ''))),
+               HEX(URL_DECODE(IF(number = 0, '%E0%80%80', ''))),
+               HEX(URL_DECODE(IF(number = 0, '%ED%A0%80', ''))),
+               HEX(URL_DECODE(IF(number = 0, '%FF', '')))
+        FROM numbers("number" = "1")
+    """
+
+    qt_valid_utf8_constant """
+        SELECT URL_DECODE('%E4%B8%AD+text'), HEX(URL_DECODE('%EF%BF%BD'))
+    """
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to