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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new e63c8e56f fix(message): abbreviate message properties on code point 
boundaries (#4467)
e63c8e56f is described below

commit e63c8e56f7b593093391b9d8abed0fe4b6300241
Author: 0 <[email protected]>
AuthorDate: Mon Sep 21 12:22:55 2026 +0800

    fix(message): abbreviate message properties on code point boundaries (#4467)
    
    The per-value cap was counted in UTF-16 chars, so abbreviation could split a
    surrogate pair and publish a lone surrogate, and hasOversizedProperty 
measured
    value.length() — a property made of astral characters could stay under the 
char
    cap while exceeding the intended code-point budget and never get 
abbreviated.
    
    Both now work in code points (codePointCount for the oversize check,
    offsetByCodePoints for the cut), so a value is either passed through whole 
or
    abbreviated at a code-point boundary with the ellipsis appended.
    
    Maintainer edits on top of the contribution: the constant was renamed
    MAX_PROPERTY_VALUE_CHARS -> MAX_PROPERTY_VALUE_CODE_POINTS because it no 
longer
    counts chars (its javadoc references and the test usages were updated with 
it),
    and the EMOJI fixture moved up into the test class's field block.
---
 .../studio/common/util/MessagePropertyDisplay.java | 31 ++++++++++---
 .../common/util/MessagePropertyDisplayTest.java    | 54 ++++++++++++++++++++++
 2 files changed, 79 insertions(+), 6 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplay.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplay.java
index e12357581..379987e37 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplay.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplay.java
@@ -25,7 +25,7 @@ import java.util.Map;
 
 /**
  * Shared rendering limits for message property maps, used by the message 
explorer and the DLQ
- * drawer so both apply the same {@value #MAX_PROPERTIES}-entry / {@value 
#MAX_PROPERTY_VALUE_CHARS}-char
+ * drawer so both apply the same {@value #MAX_PROPERTIES}-entry / {@value 
#MAX_PROPERTY_VALUE_CODE_POINTS}-code-point
  * caps instead of duplicating the logic. {@link #userProperties} additionally 
drops the broker-set
  * system keys ({@link MessageConst#STRING_HASH_SET}) so a view labelled "user 
properties" is not
  * crowded out by system entries once the cap and alphabetical ordering are 
applied.
@@ -33,7 +33,14 @@ import java.util.Map;
 public final class MessagePropertyDisplay {
 
     public static final int MAX_PROPERTIES = 64;
-    public static final int MAX_PROPERTY_VALUE_CHARS = 1024;
+
+    /**
+     * Caps a single property value by code point rather than by UTF-16 {@code 
char}. The two differ
+     * for supplementary characters (an emoji, a CJK extension character), 
which occupy two chars:
+     * cutting by char can land between a high and a low surrogate and emit 
half a character, which
+     * is not a code point and serializes to an invalid JSON escape.
+     */
+    public static final int MAX_PROPERTY_VALUE_CODE_POINTS = 1024;
 
     private MessagePropertyDisplay() {
     }
@@ -65,16 +72,28 @@ public final class MessagePropertyDisplay {
         return limited;
     }
 
-    /** True when any value exceeds {@link #MAX_PROPERTY_VALUE_CHARS} and 
would be abbreviated. */
+    /**
+     * True when any value exceeds {@link #MAX_PROPERTY_VALUE_CODE_POINTS} 
code points and would be
+     * abbreviated. Counted in code points for the same reason {@link 
#abbreviate} cuts on a code
+     * point boundary: counting chars would call a value oversized that {@code 
abbreviate} leaves
+     * untouched, and the panel would tell the operator a value is abbreviated 
when it is not.
+     */
     public static boolean hasOversizedProperty(Map<String, String> properties) 
{
         return properties != null && properties.values().stream()
-                .anyMatch(value -> value != null && value.length() > 
MAX_PROPERTY_VALUE_CHARS);
+                .anyMatch(value -> value != null && codePointCount(value) > 
MAX_PROPERTY_VALUE_CODE_POINTS);
     }
 
     private static String abbreviate(String value) {
-        if (value == null || value.length() <= MAX_PROPERTY_VALUE_CHARS) {
+        if (value == null || codePointCount(value) <= 
MAX_PROPERTY_VALUE_CODE_POINTS) {
             return value;
         }
-        return value.substring(0, MAX_PROPERTY_VALUE_CHARS) + "...";
+        // offsetByCodePoints always lands on a code point boundary, so a 
supplementary character is
+        // kept whole or dropped whole, never split into a lone surrogate.
+        int end = value.offsetByCodePoints(0, MAX_PROPERTY_VALUE_CODE_POINTS);
+        return value.substring(0, end) + "...";
+    }
+
+    private static int codePointCount(String value) {
+        return value.codePointCount(0, value.length());
     }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplayTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplayTest.java
index e3edf1bd1..ab6fad956 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplayTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/util/MessagePropertyDisplayTest.java
@@ -25,6 +25,9 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class MessagePropertyDisplayTest {
 
+    /** Emoji is one code point, two UTF-16 chars: the case the cap has to 
survive. */
+    private static final String EMOJI = "\uD83D\uDE00";
+
     @Test
     void userPropertiesShouldDropBrokerSystemKeysTest() {
         Map<String, String> properties = new HashMap<>();
@@ -64,4 +67,55 @@ class MessagePropertyDisplayTest {
         assertThat(MessagePropertyDisplay.hasOversizedProperty(Map.of("k", 
"short"))).isFalse();
         
assertThat(MessagePropertyDisplay.hasOversizedProperty(null)).isFalse();
     }
+
+    @Test
+    void limitPropertiesShouldNotSplitASurrogatePairTest() {
+        // The cap lands between the two chars of the emoji when the value is 
counted in chars.
+        String value = 
"a".repeat(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS - 1) + EMOJI + 
"tail";
+        String abbreviated = 
MessagePropertyDisplay.limitProperties(Map.of("big", value)).get("big");
+        assertThat(hasUnpairedSurrogate(abbreviated)).isFalse();
+        assertThat(abbreviated)
+                
.isEqualTo("a".repeat(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS - 
1) + EMOJI + "...");
+    }
+
+    @Test
+    void limitPropertiesShouldKeepASupplementaryCharacterThatFitsTheCapTest() {
+        String value = 
"a".repeat(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS - 2) + EMOJI + 
"tail";
+        String abbreviated = 
MessagePropertyDisplay.limitProperties(Map.of("big", value)).get("big");
+        assertThat(hasUnpairedSurrogate(abbreviated)).isFalse();
+        // 1022 a's + the emoji + 4 tail code points is 1027, so the cut keeps 
everything up to and
+        // including the first tail character, and the emoji survives whole.
+        assertThat(abbreviated)
+                
.isEqualTo("a".repeat(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS - 
2) + EMOJI + "t...");
+    }
+
+    @Test
+    void hasOversizedPropertyShouldCountCodePointsNotCharsTest() {
+        // 1024 code points, 1624 chars: over the char cap, exactly at the 
code point cap, and
+        // abbreviate leaves it untouched. Counting chars would call it 
oversized.
+        String atCap = EMOJI.repeat(600) + 
"a".repeat(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS - 600);
+        
assertThat(atCap.length()).isGreaterThan(MessagePropertyDisplay.MAX_PROPERTY_VALUE_CODE_POINTS);
+        assertThat(MessagePropertyDisplay.hasOversizedProperty(Map.of("big", 
atCap))).isFalse();
+        assertThat(MessagePropertyDisplay.limitProperties(Map.of("big", 
atCap)).get("big")).isEqualTo(atCap);
+
+        // One more code point is over the cap, and is abbreviated.
+        String overCap = atCap + "a";
+        assertThat(MessagePropertyDisplay.hasOversizedProperty(Map.of("big", 
overCap))).isTrue();
+        assertThat(MessagePropertyDisplay.limitProperties(Map.of("big", 
overCap)).get("big")).endsWith("...");
+    }
+
+    private static boolean hasUnpairedSurrogate(String value) {
+        for (int index = 0; index < value.length(); index++) {
+            char current = value.charAt(index);
+            if (Character.isHighSurrogate(current)) {
+                if (index + 1 >= value.length() || 
!Character.isLowSurrogate(value.charAt(index + 1))) {
+                    return true;
+                }
+                index++;
+            } else if (Character.isLowSurrogate(current)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

Reply via email to