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-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 4c83821cf Keep abbreviateMiddle off surrogate pair boundaries (#1758)
4c83821cf is described below

commit 4c83821cf01bfd13314ffb6a026b9c2e96fd2b63
Author: kdelay <[email protected]>
AuthorDate: Thu Jul 30 20:22:48 2026 +0900

    Keep abbreviateMiddle off surrogate pair boundaries (#1758)
---
 .../java/org/apache/commons/lang3/StringUtils.java | 11 +++++++--
 .../commons/lang3/StringUtilsAbbreviateTest.java   | 28 ++++++++++++++++++++++
 2 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java 
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index 17cdad6d9..5b8b03d72 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -429,8 +429,15 @@ public static String abbreviateMiddle(final String str, 
final String middle, fin
             return str;
         }
         final int targetString = length - middle.length();
-        final int startOffset = targetString / 2 + targetString % 2;
-        final int endOffset = str.length() - targetString / 2;
+        int startOffset = targetString / 2 + targetString % 2;
+        int endOffset = str.length() - targetString / 2;
+        // keep both cuts off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, startOffset)) {
+            startOffset--;
+        }
+        if (splitsSurrogatePair(str, endOffset)) {
+            endOffset++;
+        }
         return str.substring(0, startOffset) + middle + 
str.substring(endOffset);
     }
 
diff --git 
a/src/test/java/org/apache/commons/lang3/StringUtilsAbbreviateTest.java 
b/src/test/java/org/apache/commons/lang3/StringUtilsAbbreviateTest.java
index 771cf1955..55d2f0fa2 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsAbbreviateTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsAbbreviateTest.java
@@ -207,6 +207,34 @@ void testAbbreviateMiddle() {
         assertEquals("ab.ef", StringUtils.abbreviateMiddle("abcdef", ".", 5));
     }
 
+    @Test
+    void testAbbreviateMiddleSurrogatePair() {
+        // U+1F600 GRINNING FACE is a single supplementary code point stored 
as a surrogate pair
+        final String grin = "😀";
+        // the head cut backs off the pair so the middle is never preceded by 
a lone high surrogate
+        assertEquals("a." + "b", StringUtils.abbreviateMiddle("a" + grin + 
grin + "b", ".", 4));
+        // the tail cut skips the orphaned low surrogate so the middle is 
never followed by one
+        assertEquals(grin + "..", StringUtils.abbreviateMiddle(grin + "abc" + 
grin, "..", 5));
+        // a cut that lands between two whole code points is unchanged
+        assertEquals("ab.d", StringUtils.abbreviateMiddle("ab" + grin + "cd", 
".", 4));
+        assertEquals("ab.f", StringUtils.abbreviateMiddle("abcdef", ".", 4));
+        // results stay within length and never contain an unpaired surrogate
+        final String source = "a" + grin + "b" + grin + "cd" + grin + "ef";
+        for (int len = 3; len < source.length(); len++) {
+            final String result = StringUtils.abbreviateMiddle(source, ".", 
len);
+            assertTrue(result.length() <= len, () -> "result longer than 
length: " + result);
+            for (int i = 0; i < result.length(); i++) {
+                final char ch = result.charAt(i);
+                if (Character.isHighSurrogate(ch)) {
+                    assertTrue(i + 1 < result.length() && 
Character.isLowSurrogate(result.charAt(i + 1)), "lone high surrogate in: " + 
result);
+                    i++; // skip the paired low surrogate
+                } else {
+                    assertFalse(Character.isLowSurrogate(ch), "lone low 
surrogate in: " + result);
+                }
+            }
+        }
+    }
+
     @Test
     void testAbbreviateSurrogatePair() {
         // U+1F600 GRINNING FACE is a single supplementary code point stored 
as a surrogate pair

Reply via email to