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 4d053dc89 StrBuilder.indexOf materializes the whole builder as a 
String per call; deleteAll/replaceAll multiply it into ~750 GB churn on a 1 MB 
builder (f026).
4d053dc89 is described below

commit 4d053dc896df7c1165673637607fcc86a146eb10
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 15:34:08 2026 -0400

    StrBuilder.indexOf materializes the whole builder as a String per call;
    deleteAll/replaceAll multiply it into ~750 GB churn on a 1 MB builder
    (f026).
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/lang3/CharSequenceUtils.java    | 33 +++++++++-----
 .../commons/lang3/CharSequenceUtilsTest.java       | 52 ++++++++++++++++++++++
 3 files changed, 76 insertions(+), 10 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 5f075851e..70dc9d021 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -275,6 +275,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">ArrayUtils.addAll/insert length arithmetic overflows (undeclared 
NegativeArraySizeException) while sibling concat is overflow-checked 
(f023).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">FormattableUtils.append pads right-justified output with insert(0) per 
char; O(width^2); '%500000s' costs ~1.25e11 char moves (f024).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Attacker-controlled exception message forges frames in 
getRootCauseStackTrace output AND suppresses all real frames, beyond cosmetic 
log spoofing (f025).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StrBuilder.indexOf materializes the whole builder as a String per 
call; deleteAll/replaceAll multiply it into ~750 GB churn on a 1 MB builder 
(f026).</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/CharSequenceUtils.java 
b/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
index c86ed6f6e..2d07f55bb 100644
--- a/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
+++ b/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
@@ -63,6 +63,11 @@ private static boolean equalsIgnoreCase(final int cp1, final 
int cp2) {
 
     /**
      * Used by the indexOf(CharSequence methods) as a green implementation of 
indexOf.
+     * <p>
+     * {@link CharSequence} types without a dedicated branch are scanned in 
place rather than materialized with {@code toString()}: for builder
+     * types (for example {@code org.apache.commons.lang3.text.StrBuilder}), 
{@code toString()} copies the whole buffer, and callers that invoke
+     * this method once per occurrence (such as {@code deleteAll}/{@code 
replaceAll}) would multiply that copy into allocation-quadratic churn.
+     * </p>
      *
      * @param cs         The {@link CharSequence} to be processed.
      * @param searchChar The {@link CharSequence} to be searched for.
@@ -82,16 +87,24 @@ static int indexOf(final CharSequence cs, final 
CharSequence searchChar, final i
         if (cs instanceof StringBuffer) {
             return ((StringBuffer) cs).indexOf(searchChar.toString(), start);
         }
-        return cs.toString().indexOf(searchChar.toString(), start);
-//        if (cs instanceof String && searchChar instanceof String) {
-//            // TODO: Do we assume searchChar is usually relatively small;
-//            //       If so then calling toString() on it is better than 
reverting to
-//            //       the green implementation in the else block
-//            return ((String) cs).indexOf((String) searchChar, start);
-//        } else {
-//            // TODO: Implement rather than convert to String
-//            return cs.toString().indexOf(searchChar.toString(), start);
-//        }
+        // Direct scan without copying cs; matches the semantics of 
String.indexOf(String, int).
+        final int len1 = cs.length();
+        final int len2 = searchChar.length();
+        final int from = Math.max(start, 0);
+        if (len2 == 0) {
+            return Math.min(from, len1);
+        }
+        if (len2 > len1 - from) {
+            return StringUtils.INDEX_NOT_FOUND;
+        }
+        final char char0 = searchChar.charAt(0);
+        final int max = len1 - len2;
+        for (int i = from; i <= max; i++) {
+            if (cs.charAt(i) == char0 && checkLaterThan1(cs, searchChar, len2, 
i)) {
+                return i;
+            }
+        }
+        return StringUtils.INDEX_NOT_FOUND;
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/lang3/CharSequenceUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/CharSequenceUtilsTest.java
index b45892533..e2b666572 100644
--- a/src/test/java/org/apache/commons/lang3/CharSequenceUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/CharSequenceUtilsTest.java
@@ -206,6 +206,58 @@ void testLastIndexOfWithDifferentCharSequences(final 
CharSequence cs, final Char
         assertEquals(expected, CharSequenceUtils.lastIndexOf(cs, search, 
start));
     }
 
+    /**
+     * Tests that the direct-scan fallback in {@link 
CharSequenceUtils#indexOf(CharSequence, CharSequence, int)} (taken by 
CharSequence
+     * types without a dedicated dispatch branch, such as {@code 
org.apache.commons.lang3.text.StrBuilder}) matches
+     * {@link String#indexOf(String, int)} semantics exactly and never 
materializes the searched sequence via {@code toString()}.
+     */
+    @Test
+    void testNewIndexOf() {
+        
testNewIndexOfSingle("808087847-1321060740-635567660180086727-925755305", 
"-1321060740-635567660");
+        testNewIndexOfSingle("", "");
+        testNewIndexOfSingle("1", "");
+        testNewIndexOfSingle("", "1");
+        testNewIndexOfSingle("1", "1");
+        testNewIndexOfSingle("11", "1");
+        testNewIndexOfSingle("1", "11");
+        testNewIndexOfSingle("apache", "a");
+        testNewIndexOfSingle("apache", "p");
+        testNewIndexOfSingle("apache", "e");
+        testNewIndexOfSingle("apache", "x");
+        testNewIndexOfSingle("oraoraoraora", "r");
+        testNewIndexOfSingle("mudamudamudamuda", "d");
+        testNewIndexOfSingle("junk-ststarting", "starting");
+        // The searched sequence must not be copied by the fallback.
+        final CharSequence noToString = new WrapperString("hello world") {
+            @Override
+            public String toString() {
+                throw new AssertionError("cs.toString() must not be called by 
indexOf");
+            }
+        };
+        assertEquals(6, CharSequenceUtils.indexOf(noToString, "world", 0));
+        assertEquals(-1, CharSequenceUtils.indexOf(noToString, "worlds", 0));
+    }
+
+    private void testNewIndexOfSingle(final CharSequence a, final CharSequence 
b) {
+        final int maxa = Math.max(a.length(), b.length());
+        for (int i = -maxa - 10; i <= maxa + 10; i++) {
+            testNewIndexOfSingle(a, b, i);
+        }
+        testNewIndexOfSingle(a, b, Integer.MIN_VALUE);
+        testNewIndexOfSingle(a, b, Integer.MAX_VALUE);
+    }
+
+    private void testNewIndexOfSingle(final CharSequence a, final CharSequence 
b, final int start) {
+        testNewIndexOfSingleSingle(a, b, start);
+        testNewIndexOfSingleSingle(b, a, start);
+    }
+
+    private void testNewIndexOfSingleSingle(final CharSequence a, final 
CharSequence b, final int start) {
+        assertEquals(a.toString().indexOf(b.toString(), start),
+                CharSequenceUtils.indexOf(new WrapperString(a.toString()), new 
WrapperString(b.toString()), start),
+                "testNewIndexOf fails! original : " + a + " seg : " + b + " 
start : " + start);
+    }
+
     @Test
     void testNewLastIndexOf() {
         
testNewLastIndexOfSingle("808087847-1321060740-635567660180086727-925755305", 
"-1321060740-635567660", 21);

Reply via email to