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