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

commit 98e5b46adf691d942c12484161cfebf6f69c4561
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 8 09:33:20 2026 +0800

    branch-4.1: [fix](jsonb) Stop truncating JSON text at a U+0000 inside a 
string #68244 (#68735)
    
    Cherry-picked from #68244
    
    Co-authored-by: Jerry Hu <[email protected]>
---
 be/src/util/jsonb_utils.h                          |  9 +++-
 be/test/util/jsonb_parser_simd_test.cpp            | 37 +++++++++++++
 .../table_function/test_json_text_embedded_nul.out | 19 +++++++
 .../test_json_text_embedded_nul.groovy             | 62 ++++++++++++++++++++++
 4 files changed, 125 insertions(+), 2 deletions(-)

diff --git a/be/src/util/jsonb_utils.h b/be/src/util/jsonb_utils.h
index abf05b4d311..05b2ab5ad08 100644
--- a/be/src/util/jsonb_utils.h
+++ b/be/src/util/jsonb_utils.h
@@ -107,8 +107,11 @@ private:
             break;
         }
         case JsonbType::T_String: {
+            // getBlobLen() is the stored payload length. length() would drop a
+            // trailing NUL that belongs to the value, so it cannot be used 
here;
+            // the writer stores strings by their exact length and never pads.
             string_to_json(val->unpack<JsonbStringVal>()->getBlob(),
-                           val->unpack<JsonbStringVal>()->length());
+                           val->unpack<JsonbStringVal>()->getBlobLen());
             break;
         }
         case JsonbType::T_Binary: {
@@ -162,7 +165,9 @@ private:
             return;
         }
         char char_buffer[16];
-        for (const char* ptr = str; ptr != str + len && *ptr; ++ptr) {
+        // A JSON string may legally contain U+0000, so the loop must be 
bounded by
+        // the length only; the NUL itself is escaped as \u0000 by the default 
branch.
+        for (const char* ptr = str; ptr != str + len; ++ptr) {
             if ((unsigned char)*ptr > 31 && *ptr != '\"' && *ptr != '\\') {
                 os_.put(*ptr);
             } else {
diff --git a/be/test/util/jsonb_parser_simd_test.cpp 
b/be/test/util/jsonb_parser_simd_test.cpp
index 53a666955ba..8f5b418a193 100644
--- a/be/test/util/jsonb_parser_simd_test.cpp
+++ b/be/test/util/jsonb_parser_simd_test.cpp
@@ -400,4 +400,41 @@ TEST_F(JsonbParserTest, ParseJsonWithToLongKey) {
     EXPECT_FALSE(st.ok());
     std::cout << st.msg() << std::endl;
 }
+
+TEST_F(JsonbParserTest, ParseJsonWithEscapedNulInString) {
+    std::string_view json_with_nul = R"({"key":"a\u0000b"})";
+    std::string_view expected_json_with_nul = R"({"key":"a\u0000b"})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
+TEST_F(JsonbParserTest, ParseJsonWithEscapedNulInNestedArray) {
+    std::string_view json_with_nul = R"({"key":["a\u0000b","c"]})";
+    std::string_view expected_json_with_nul = R"({"key":["a\u0000b","c"]})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
+TEST_F(JsonbParserTest, ParseJsonWithTrailingNulInString) {
+    std::string_view json_with_nul = R"({"key":"a\u0000"})";
+    std::string_view expected_json_with_nul = R"({"key":"a\u0000"})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
+TEST_F(JsonbParserTest, ParseJsonWithOnlyNulInString) {
+    std::string_view json_with_nul = R"({"key":"\u0000"})";
+    std::string_view expected_json_with_nul = R"({"key":"\u0000"})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
+TEST_F(JsonbParserTest, ParseJsonWithTrailingNulInNestedArray) {
+    std::string_view json_with_nul = R"({"key":["a\u0000","\u0000"]})";
+    std::string_view expected_json_with_nul = 
R"({"key":["a\u0000","\u0000"]})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
+TEST_F(JsonbParserTest, ParseJsonWithEscapedNulInKey) {
+    std::string_view json_with_nul = R"({"a\u0000b":1})";
+    std::string_view expected_json_with_nul = R"({"a\u0000b":1})";
+    EXPECT_EQ(parse_json_and_check(json_with_nul, expected_json_with_nul), 
Status::OK());
+}
+
 } // namespace doris
\ No newline at end of file
diff --git 
a/regression-test/data/query_p0/sql_functions/table_function/test_json_text_embedded_nul.out
 
b/regression-test/data/query_p0/sql_functions/table_function/test_json_text_embedded_nul.out
new file mode 100644
index 00000000000..a3c8fb2de52
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/table_function/test_json_text_embedded_nul.out
@@ -0,0 +1,19 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !json_each_text_nested_array --
+x      12      ["a\\u0000b"]
+
+-- !json_each_text_nested_object --
+x      16      {"y":"a\\u0000b"}
+
+-- !json_each_text_trailing_nul --
+x      20      ["a\\u0000","\\u0000"]
+
+-- !explode_json_array_string_direct --
+3      610062
+
+-- !explode_json_array_string_nested --
+12     ["a\\u0000b"]
+
+-- !explode_json_array_string_trailing_nul --
+11     ["a\\u0000"]
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/table_function/test_json_text_embedded_nul.groovy
 
b/regression-test/suites/query_p0/sql_functions/table_function/test_json_text_embedded_nul.groovy
new file mode 100644
index 00000000000..7e1e3beeea7
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/table_function/test_json_text_embedded_nul.groovy
@@ -0,0 +1,62 @@
+// 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("test_json_text_embedded_nul") {
+    // unhex('5C') is a backslash, so every JSON literal below really contains
+    // \u0000, the legal escape for U+0000 inside a JSON string. Rendering such
+    // a nested value back to text must not stop at the NUL.
+
+    qt_json_each_text_nested_array '''
+        SELECT k, length(v), v
+        FROM (SELECT 1) d
+        LATERAL VIEW json_each_text(concat('{"x":["a', unhex('5C'), 
'u0000b"]}')) t AS k, v
+    '''
+
+    qt_json_each_text_nested_object '''
+        SELECT k, length(v), v
+        FROM (SELECT 1) d
+        LATERAL VIEW json_each_text(concat('{"x":{"y":"a', unhex('5C'), 
'u0000b"}}')) t AS k, v
+    '''
+
+    // A NUL at the very end of a string used to be trimmed by the 
stored-length
+    // helper, so keep a dedicated case for it.
+    qt_json_each_text_trailing_nul '''
+        SELECT k, length(v), v
+        FROM (SELECT 1) d
+        LATERAL VIEW json_each_text(concat('{"x":["a', unhex('5C'), 
'u0000","', unhex('5C'), 'u0000"]}')) t AS k, v
+    '''
+
+    // A direct string element is copied by length, so it keeps the raw NUL 
byte;
+    // compare it through hex() to keep the expected output printable.
+    qt_explode_json_array_string_direct '''
+        SELECT length(c), hex(c)
+        FROM (SELECT 1) d
+        LATERAL VIEW explode_json_array_string(concat('["a', unhex('5C'), 
'u0000b"]')) t AS c
+    '''
+
+    qt_explode_json_array_string_nested '''
+        SELECT length(c), c
+        FROM (SELECT 1) d
+        LATERAL VIEW explode_json_array_string(concat('[["a', unhex('5C'), 
'u0000b"]]')) t AS c
+    '''
+
+    qt_explode_json_array_string_trailing_nul '''
+        SELECT length(c), c
+        FROM (SELECT 1) d
+        LATERAL VIEW explode_json_array_string(concat('[["a', unhex('5C'), 
'u0000"]]')) t AS c
+    '''
+}


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

Reply via email to