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;
+ }
}