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

sandynz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new da38be837eb mysql: escape JSON binlog control characters (#38800)
da38be837eb is described below

commit da38be837eb600ffafe75f755821604c45d3362b
Author: Hongsheng Zhong <[email protected]>
AuthorDate: Thu Jun 4 20:17:35 2026 +0800

    mysql: escape JSON binlog control characters (#38800)
    
    * mysql: escape JSON binlog control characters
    
    * Update RELEASE-NOTES.md
---
 RELEASE-NOTES.md                                   |  3 +-
 .../column/value/string/MySQLJsonValueDecoder.java | 36 +++++++++++++++++++---
 .../value/string/MySQLJsonValueDecoderTest.java    | 13 ++++++++
 .../general/MySQLMigrationGeneralE2EIT.java        |  5 ++-
 4 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 72134e2ff89..99cbdb0275b 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -22,7 +22,8 @@
 1. Proxy: Force PostgreSQL backend `client_encoding` to UTF8 and reject 
non-UTF8 values to keep pgjdbc parameter encoding consistent - 
[#38645](https://github.com/apache/shardingsphere/pull/38645)
 1. Mode: Fix rule metadata not removed from memory after dropping rules in 
Etcd cluster mode - 
[#38561](https://github.com/apache/shardingsphere/pull/38561)
 1. Pipeline: Fix MySQL JSON literal decoding in migration - 
[#38622](https://github.com/apache/shardingsphere/pull/38622)
-1. Pipeline: Fix MySQL zero-value temporal binlog decoding with fractional 
precision in migration - 
[#35531](https://github.com/apache/shardingsphere/issues/35531)
+1. Pipeline: Fix MySQL zero-value temporal binlog decoding with fractional 
precision in migration - 
[#38629](https://github.com/apache/shardingsphere/pull/38629)
+1. Pipeline: Fix escape MySQL JSON binlog control characters - 
[#38800](https://github.com/apache/shardingsphere/pull/38800)
 1. Sharding: Support ORDER BY MySQL VARBINARY column by wrapping byte[] values 
in a Comparable adapter - 
[#38699](https://github.com/apache/shardingsphere/pull/38699)
 
 ### Enhancements
diff --git 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
index 4e8201e16df..d1056f03274 100644
--- 
a/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
+++ 
b/database/protocol/dialect/mysql/src/main/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoder.java
@@ -117,7 +117,8 @@ public final class MySQLJsonValueDecoder {
             if (0 < i) {
                 stringBuilder.append(',');
             }
-            stringBuilder.append('"').append(keys[i]).append("\":");
+            outputString(keys[i], stringBuilder);
+            stringBuilder.append(':');
             decodeValueEntry(isSmall, byteBuf, stringBuilder);
         }
         stringBuilder.append('}');
@@ -233,10 +234,37 @@ public final class MySQLJsonValueDecoder {
         out.append('"');
         for (int i = 0; i < str.length(); ++i) {
             char c = str.charAt(i);
-            if (c == '"' || c == '\\') {
-                out.append('\\');
+            switch (c) {
+                case '"':
+                case '\\':
+                    out.append('\\').append(c);
+                    break;
+                case '\b':
+                    out.append("\\b");
+                    break;
+                case '\f':
+                    out.append("\\f");
+                    break;
+                case '\n':
+                    out.append("\\n");
+                    break;
+                case '\r':
+                    out.append("\\r");
+                    break;
+                case '\t':
+                    out.append("\\t");
+                    break;
+                default:
+                    if (c < ' ') {
+                        out.append("\\u00");
+                        if (c < 0x10) {
+                            out.append('0');
+                        }
+                        out.append(Integer.toHexString(c));
+                    } else {
+                        out.append(c);
+                    }
             }
-            out.append(c);
         }
         out.append('"');
     }
diff --git 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
index c4d48d1b02a..d89b845027c 100644
--- 
a/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
+++ 
b/database/protocol/dialect/mysql/src/test/java/org/apache/shardingsphere/database/protocol/mysql/packet/binlog/row/column/value/string/MySQLJsonValueDecoderTest.java
@@ -280,6 +280,13 @@ class MySQLJsonValueDecoderTest {
                 Arguments.of("decode top-level literal false", 
mockTopLevelLiteralByteBuf(JsonValueTypes.LITERAL_FALSE), "false"),
                 Arguments.of("decode top-level string", 
mockTopLevelStringByteBuf("hello"), "\"hello\""),
                 Arguments.of("decode top-level string with escaped chars", 
mockTopLevelStringByteBuf("a\"\\b"), "\"a\\\"\\\\b\""),
+                Arguments.of("decode top-level string with backspace", 
mockTopLevelStringByteBuf("a\bb"), "\"a\\bb\""),
+                Arguments.of("decode top-level string with form feed", 
mockTopLevelStringByteBuf("a\fb"), "\"a\\fb\""),
+                Arguments.of("decode top-level string with newline", 
mockTopLevelStringByteBuf("a\nb"), "\"a\\nb\""),
+                Arguments.of("decode top-level string with carriage return", 
mockTopLevelStringByteBuf("a\rb"), "\"a\\rb\""),
+                Arguments.of("decode top-level string with tab", 
mockTopLevelStringByteBuf("a\tb"), "\"a\\tb\""),
+                Arguments.of("decode top-level string with unicode control 
char 0x01", mockTopLevelStringByteBuf("a" + (char) 0x01 + "b"), 
"\"a\\u0001b\""),
+                Arguments.of("decode top-level string with unicode control 
char 0x1f", mockTopLevelStringByteBuf("a" + (char) 0x1f + "b"), 
"\"a\\u001fb\""),
                 Arguments.of("decode top-level int32", 
mockTopLevelInt32ByteBuf(123), "123"),
                 Arguments.of("decode top-level double", 
mockTopLevelDoubleByteBuf(45.6D), "45.6"),
                 Arguments.of("decode int16 top-level", 
mockTopLevelInt16ByteBuf(), "-32768"),
@@ -320,6 +327,12 @@ class MySQLJsonValueDecoderTest {
                         
"{\"key1\":9223372036854775807,\"key2\":9223372036854775808}"),
                 Arguments.of("decode small object with double", 
Arrays.asList(new Object[][]{new Object[]{JsonValueTypes.DOUBLE, "key1", 
Double.MAX_VALUE}}),
                         "{\"key1\":1.7976931348623157E308}"),
+                Arguments.of("decode small object with tab string", 
Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key1", "a\tb"}),
+                        "{\"key1\":\"a\\tb\"}"),
+                Arguments.of("decode small object with unicode control 
string", Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key1", 
"a" + (char) 0x01 + "b"}),
+                        "{\"key1\":\"a\\u0001b\"}"),
+                Arguments.of("decode small object with tab key", 
Collections.singletonList(new Object[]{JsonValueTypes.STRING, "key\t1", 
"value1"}),
+                        "{\"key\\t1\":\"value1\"}"),
                 Arguments.of("decode small object with string",
                         Arrays.asList(new Object[]{JsonValueTypes.STRING, 
"key1", value1}, new Object[]{JsonValueTypes.STRING, "key2", value2}, new 
Object[]{JsonValueTypes.STRING, "key3", value3},
                                 new Object[]{JsonValueTypes.STRING, "key4", 
value4}),
diff --git 
a/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
 
b/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
index 30572e86e71..c5910a6b094 100644
--- 
a/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
+++ 
b/test/e2e/operation/pipeline/src/test/java/org/apache/shardingsphere/test/e2e/operation/pipeline/cases/migration/general/MySQLMigrationGeneralE2EIT.java
@@ -84,7 +84,10 @@ class MySQLMigrationGeneralE2EIT extends 
AbstractMigrationE2EIT {
             containerComposer.startIncrementTask(
                     new 
E2EIncrementalTask(containerComposer.getSourceDataSource(), SOURCE_TABLE_NAME, 
new SnowflakeKeyGenerateAlgorithm(), containerComposer.getDatabaseType(), 30));
             
TimeUnit.SECONDS.timedJoin(containerComposer.getIncreaseTaskThread(), 30L);
-            orderDAO.insert(10000L, 1, "OK");
+            containerComposer.sourceExecuteWithLog(String.format("""
+                    INSERT INTO %s (order_id, user_id, status, t_json) VALUES
+                    (10000, 1, 'OK', 
'{"backspace":"a\\\\bb","form_feed":"a\\\\fb","newline":"a\\\\nb","carriage_return":"a\\\\rb","tab":"a\\\\tb","unicode":"a\\\\u0001b","tab\\\\tkey":"value"}')
+                    """, SOURCE_TABLE_NAME));
             orderItemDAO.insert(10000L, 10000L, 1, "OK");
             distSQLFacade.pauseJob(orderJobId);
             distSQLFacade.resumeJob(orderJobId);

Reply via email to