This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-text.git
The following commit(s) were added to refs/heads/master by this push:
new f6db25e3 Keep TextStringBuilder.reverse from splitting surrogate pairs
(#756)
f6db25e3 is described below
commit f6db25e32356b2e1fcaeeb8ec7100eee3342193b
Author: alhuda <[email protected]>
AuthorDate: Sat Jun 27 18:20:52 2026 +0530
Keep TextStringBuilder.reverse from splitting surrogate pairs (#756)
---
.../org/apache/commons/text/TextStringBuilder.java | 21 ++++++++++++++++++---
.../apache/commons/text/TextStringBuilderTest.java | 15 +++++++++++++++
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/commons/text/TextStringBuilder.java
b/src/main/java/org/apache/commons/text/TextStringBuilder.java
index d2adf157..10f391fd 100644
--- a/src/main/java/org/apache/commons/text/TextStringBuilder.java
+++ b/src/main/java/org/apache/commons/text/TextStringBuilder.java
@@ -2879,10 +2879,25 @@ public class TextStringBuilder implements CharSequence,
Appendable, Serializable
}
final int half = size / 2;
final char[] buf = buffer;
+ boolean hasSurrogates = false;
for (int leftIdx = 0, rightIdx = size - 1; leftIdx < half; leftIdx++,
rightIdx--) {
- final char swap = buf[leftIdx];
- buf[leftIdx] = buf[rightIdx];
- buf[rightIdx] = swap;
+ final char left = buf[leftIdx];
+ final char right = buf[rightIdx];
+ buf[leftIdx] = right;
+ buf[rightIdx] = left;
+ hasSurrogates |= Character.isSurrogate(left) ||
Character.isSurrogate(right);
+ }
+ if (hasSurrogates) {
+ // The plain swap leaves each surrogate pair in low-high order;
restore the high-low order so a
+ // reversed supplementary code point stays a valid pair, matching
StringBuilder#reverse().
+ for (int i = 0; i < size - 1; i++) {
+ if (Character.isLowSurrogate(buf[i]) &&
Character.isHighSurrogate(buf[i + 1])) {
+ final char low = buf[i];
+ buf[i] = buf[i + 1];
+ buf[i + 1] = low;
+ i++;
+ }
+ }
}
return this;
}
diff --git a/src/test/java/org/apache/commons/text/TextStringBuilderTest.java
b/src/test/java/org/apache/commons/text/TextStringBuilderTest.java
index 649fc4ea..3aafdb0d 100644
--- a/src/test/java/org/apache/commons/text/TextStringBuilderTest.java
+++ b/src/test/java/org/apache/commons/text/TextStringBuilderTest.java
@@ -2137,6 +2137,21 @@ class TextStringBuilderTest {
assertEquals("true", sb.reverse().toString());
}
+ @Test
+ void testReverseSurrogatePairs() {
+ // U+1F600 GRINNING FACE is a supplementary code point encoded as a
surrogate pair; reversing
+ // must keep the pair intact (high before low) like StringBuilder, not
split it into garbage.
+ final String emoji = "😀";
+ assertEquals(new StringBuilder("a" + emoji +
"b").reverse().toString(), new TextStringBuilder("a" + emoji +
"b").reverse().toString());
+ assertEquals("b" + emoji + "a", new TextStringBuilder("a" + emoji +
"b").reverse().toString());
+
+ final String emoji2 = "😁";
+ assertEquals(emoji2 + emoji, new TextStringBuilder(emoji +
emoji2).reverse().toString());
+
+ // An unpaired high surrogate has no partner and is left where it
lands.
+ assertEquals("b\uD800a", new
TextStringBuilder("a\uD800b").reverse().toString());
+ }
+
@Test
void testRightString() {
final TextStringBuilder sb = new TextStringBuilder("left right");