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 73dadf009 Keep StringUtils left, right, mid, and overlay off surrogate 
pair boundaries (#1776)
73dadf009 is described below

commit 73dadf0094cd3557149312b85c88b7b3d20018fc
Author: gaurav kumar pandey <[email protected]>
AuthorDate: Sun Aug 23 01:21:46 2026 +0530

    Keep StringUtils left, right, mid, and overlay off surrogate pair 
boundaries (#1776)
---
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/commons/lang3/StringUtils.java | 35 +++++++-
 .../commons/lang3/StringUtilsSubstringTest.java    | 92 ++++++++++++++++++++++
 .../org/apache/commons/lang3/StringUtilsTest.java  | 25 ++++++
 4 files changed, 149 insertions(+), 4 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f964c2704..021fab1b8 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -248,6 +248,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="alhuda, Gary 
Gregory">Fix CharRange.contains(CharRange) for negated argument ranges 
(#1775).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix SpotBugs USO_UNSAFE_METHOD_SYNCHRONIZATION in CharSet.</action>
     <action                   type="fix" dev="ggregory" due-to="alhuda, Gary 
Gregory">Keep StopWatch.formatSplitTime from clamping splits to int millis 
(#1777).</action>
+    <action                   type="fix" dev="ggregory" due-to="gaurav kumar 
pandey, Gary Gregory">Keep StringUtils left, right, mid, and overlay off 
surrogate pair boundaries (#1776).</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java 
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index 6d06ff1ae..824fb4583 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -5161,7 +5161,12 @@ public static String left(final String str, final int 
len) {
         if (str.length() <= len) {
             return str;
         }
-        return str.substring(0, len);
+        int cut = len;
+        // keep the cut off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, cut)) {
+            cut--;
+        }
+        return str.substring(0, cut);
     }
 
     /**
@@ -5433,10 +5438,20 @@ public static String mid(final String str, int pos, 
final int len) {
         if (pos < 0) {
             pos = 0;
         }
+        int start = pos;
+        // keep the start off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, start)) {
+            start++;
+        }
         if (str.length() - pos <= len) {
-            return str.substring(pos);
+            return str.substring(start);
         }
-        return str.substring(pos, pos + len);
+        int end = pos + len;
+        // keep both cuts off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, end)) {
+            end--;
+        }
+        return str.substring(start, Math.max(start, end));
     }
 
     /**
@@ -5661,6 +5676,13 @@ public static String overlay(final String str, String 
overlay, int start, int en
             start = end;
             end = temp;
         }
+        // keep both cuts off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, start)) {
+            start--;
+        }
+        if (splitsSurrogatePair(str, end)) {
+            end++;
+        }
         return str.substring(0, start) + overlay + str.substring(end);
     }
 
@@ -6955,7 +6977,12 @@ public static String right(final String str, final int 
len) {
         if (str.length() <= len) {
             return str;
         }
-        return str.substring(str.length() - len);
+        int start = str.length() - len;
+        // keep the cut off the middle of a surrogate pair so the result is 
never left holding a lone surrogate
+        if (splitsSurrogatePair(str, start)) {
+            start++;
+        }
+        return str.substring(start);
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/lang3/StringUtilsSubstringTest.java 
b/src/test/java/org/apache/commons/lang3/StringUtilsSubstringTest.java
index 9c7d18620..605ff8bc6 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsSubstringTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsSubstringTest.java
@@ -17,8 +17,10 @@
 package org.apache.commons.lang3;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
 
@@ -399,4 +401,94 @@ void testSubstringsBetween_StringStringString() {
             assertEquals(0, results.length);
         }
 
+    @Test
+    void testLeftSurrogatePair() {
+        // U+1F600 GRINNING FACE is a supplementary code point stored as a 
surrogate pair
+        final String grin = "😀";
+        assertEquals("", StringUtils.left(grin, 0));
+        assertEquals("", StringUtils.left(grin, 1));
+        assertEquals(grin, StringUtils.left(grin, 2));
+        assertEquals(grin, StringUtils.left(grin, 3));
+
+        assertEquals("a", StringUtils.left("a" + grin, 1));
+        assertEquals("a", StringUtils.left("a" + grin, 2));
+        assertEquals("a" + grin, StringUtils.left("a" + grin, 3));
+
+        final String source = "a" + grin + "b" + grin + "cd" + grin + "ef";
+        for (int len = 0; len <= source.length(); len++) {
+            final String result = StringUtils.left(source, len);
+            assertTrue(result.length() <= len, () -> "result longer than len: 
" + 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 testMidSurrogatePair() {
+        // U+1F600 GRINNING FACE is a supplementary code point stored as a 
surrogate pair
+        final String grin = "😀";
+        assertEquals("", StringUtils.mid(grin, 0, 0));
+        assertEquals("", StringUtils.mid(grin, 0, 1));
+        assertEquals(grin, StringUtils.mid(grin, 0, 2));
+        assertEquals(grin, StringUtils.mid(grin, 0, 3));
+        assertEquals("", StringUtils.mid(grin, 1, 1));
+        assertEquals("", StringUtils.mid(grin, 1, 2));
+
+        assertEquals("a", StringUtils.mid("a" + grin + "b", 0, 2));
+        assertEquals(grin, StringUtils.mid("a" + grin + "b", 1, 2));
+        assertEquals("b", StringUtils.mid("a" + grin + "b", 2, 2));
+
+        final String source = "a" + grin + "b" + grin + "cd" + grin + "ef";
+        for (int pos = 0; pos <= source.length(); pos++) {
+            for (int len = 0; len <= source.length(); len++) {
+                final String result = StringUtils.mid(source, pos, len);
+                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 testRightSurrogatePair() {
+        // U+1F600 GRINNING FACE is a supplementary code point stored as a 
surrogate pair
+        final String grin = "😀";
+        assertEquals("", StringUtils.right(grin, 0));
+        assertEquals("", StringUtils.right(grin, 1));
+        assertEquals(grin, StringUtils.right(grin, 2));
+        assertEquals(grin, StringUtils.right(grin, 3));
+
+        assertEquals("a", StringUtils.right(grin + "a", 1));
+        assertEquals("a", StringUtils.right(grin + "a", 2));
+        assertEquals(grin + "a", StringUtils.right(grin + "a", 3));
+
+        final String source = "a" + grin + "b" + grin + "cd" + grin + "ef";
+        for (int len = 0; len <= source.length(); len++) {
+            final String result = StringUtils.right(source, len);
+            assertTrue(result.length() <= len, () -> "result longer than len: 
" + 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);
+                }
+            }
+        }
+    }
+
 }
diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
index e27c9f90e..d4976574d 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
@@ -1461,6 +1461,31 @@ void testOverlay_StringStringIntInt() {
         assertEquals("abcdefzzzz", StringUtils.overlay("abcdef", "zzzz", 10, 
8));
     }
 
+    @Test
+    void testOverlaySurrogatePair() {
+        final String grin = "😀";
+        // overlaying across surrogate pair boundary backs off start and 
advances end
+        assertEquals("X", StringUtils.overlay(grin, "X", 1, 1));
+        assertEquals("aXb", StringUtils.overlay("a" + grin + "b", "X", 1, 3));
+        assertEquals("aXb", StringUtils.overlay("a" + grin + "b", "X", 2, 2));
+
+        final String source = "a" + grin + "b" + grin + "cd" + grin + "ef";
+        for (int start = 0; start <= source.length(); start++) {
+            for (int end = 0; end <= source.length(); end++) {
+                final String result = StringUtils.overlay(source, "X", start, 
end);
+                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);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Tests {@code prependIfMissing}.
      */

Reply via email to