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 4c9d42c15 ExtendedMessageFormat.applyPattern() is quadratic: full 
pattern.toCharArray() per token (f008).
4c9d42c15 is described below

commit 4c9d42c157e41f5136f6729cebeb7fafa4abde5f
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 07:52:15 2026 -0400

    ExtendedMessageFormat.applyPattern() is quadratic: full
    pattern.toCharArray() per token (f008).
    
    A 300 KB pattern drives ~180 GB of transient allocation where
    java.text.MessageFormat parses in one linear pass (f008).
---
 src/changes/changes.xml                            |  2 +
 .../commons/lang3/text/ExtendedMessageFormat.java  | 53 ++++++++++++----------
 2 files changed, 30 insertions(+), 25 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 2550f2532..ae949589c 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -256,6 +256,8 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">StringUtils.replaceEachRepeatedly derives its recursion budget from 
the input itself; the documented StackOverflowError protection fails on large 
tables, and expanding rules amplify text 64x even at the default TTL 
(f005).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">EventUtils.EventBindingInvocationHandler.invoke() dispatches 
Object.hashCode/equals/toString into the bound business method 
(empty-eventTypes default), or returns null -> NPE from hash collections 
(non-empty) (f006).</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">LocaleUtils static caches no longer grows on invalid input to 
LocaleUtils.countriesByLanguage(String) (f007).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">LocaleUtils static caches no longer grows on invalid input to 
LocaleUtils.countriesByLanguage(String) (f007).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">ExtendedMessageFormat.applyPattern() is quadratic: full 
pattern.toCharArray() per token (f008).</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/text/ExtendedMessageFormat.java 
b/src/main/java/org/apache/commons/lang3/text/ExtendedMessageFormat.java
index 74425e47d..738f369b1 100644
--- a/src/main/java/org/apache/commons/lang3/text/ExtendedMessageFormat.java
+++ b/src/main/java/org/apache/commons/lang3/text/ExtendedMessageFormat.java
@@ -145,14 +145,15 @@ public ExtendedMessageFormat(final String pattern, final 
Map<String, ? extends F
      * Consume a quoted string, adding it to {@code appendTo} if
      * specified.
      *
-     * @param pattern pattern to parse
+     * @param pattern pattern to parse, as a char array created once by the 
caller (avoids copying
+     *        the entire pattern for every token parsed)
      * @param pos current parse position
      * @param appendTo optional StringBuilder to append
      * @return {@code appendTo}
      */
-    private StringBuilder appendQuotedString(final String pattern, final 
ParsePosition pos,
+    private StringBuilder appendQuotedString(final char[] pattern, final 
ParsePosition pos,
             final StringBuilder appendTo) {
-        assert pattern.toCharArray()[pos.getIndex()] == QUOTE :
+        assert pattern[pos.getIndex()] == QUOTE :
             "Quoted string must start with quote character";
 
         // handle quote character at the beginning of the string
@@ -162,11 +163,10 @@ private StringBuilder appendQuotedString(final String 
pattern, final ParsePositi
         next(pos);
 
         final int start = pos.getIndex();
-        final char[] c = pattern.toCharArray();
-        for (int i = pos.getIndex(); i < pattern.length(); i++) {
-            if (c[pos.getIndex()] == QUOTE) {
+        for (int i = pos.getIndex(); i < pattern.length; i++) {
+            if (pattern[pos.getIndex()] == QUOTE) {
                 next(pos);
-                return appendTo == null ? null : appendTo.append(c, start,
+                return appendTo == null ? null : appendTo.append(pattern, 
start,
                         pos.getIndex() - start);
             }
             next(pos);
@@ -197,19 +197,19 @@ public final void applyPattern(final String pattern) {
         while (pos.getIndex() < pattern.length()) {
             switch (c[pos.getIndex()]) {
             case QUOTE:
-                appendQuotedString(pattern, pos, stripCustom);
+                appendQuotedString(c, pos, stripCustom);
                 break;
             case START_FE:
                 fmtCount++;
-                seekNonWs(pattern, pos);
+                seekNonWs(c, pos);
                 final int start = pos.getIndex();
-                final int index = readArgumentIndex(pattern, next(pos));
+                final int index = readArgumentIndex(pattern, c, next(pos));
                 stripCustom.append(START_FE).append(index);
-                seekNonWs(pattern, pos);
+                seekNonWs(c, pos);
                 Format format = null;
                 String formatDescription = null;
                 if (c[pos.getIndex()] == START_FMT) {
-                    formatDescription = parseFormatDescription(pattern,
+                    formatDescription = parseFormatDescription(pattern, c,
                             next(pos));
                     format = getFormat(formatDescription);
                     if (format == null) {
@@ -298,10 +298,10 @@ private Format getFormat(final String desc) {
     /**
      * Consume quoted string only
      *
-     * @param pattern pattern to parse
+     * @param pattern pattern to parse, as a char array created once by the 
caller
      * @param pos current parse position
      */
-    private void getQuotedString(final String pattern, final ParsePosition 
pos) {
+    private void getQuotedString(final char[] pattern, final ParsePosition 
pos) {
         appendQuotedString(pattern, pos, null);
     }
 
@@ -325,17 +325,18 @@ private String insertFormats(final String pattern, final 
ArrayList<String> custo
         }
         final StringBuilder sb = new StringBuilder(pattern.length() * 2);
         final ParsePosition pos = new ParsePosition(0);
+        final char[] chars = pattern.toCharArray();
         int fe = -1;
         int depth = 0;
         while (pos.getIndex() < pattern.length()) {
             final char c = pattern.charAt(pos.getIndex());
             switch (c) {
             case QUOTE:
-                appendQuotedString(pattern, pos, sb);
+                appendQuotedString(chars, pos, sb);
                 break;
             case START_FE:
                 depth++;
-                sb.append(START_FE).append(readArgumentIndex(pattern, 
next(pos)));
+                sb.append(START_FE).append(readArgumentIndex(pattern, chars, 
next(pos)));
                 // do not look for custom patterns when they are embedded, 
e.g. in a choice
                 if (depth == 1) {
                     fe++;
@@ -371,12 +372,13 @@ private ParsePosition next(final ParsePosition pos) {
      * Parse the format component of a format element.
      *
      * @param pattern string to parse
+     * @param chars the pattern as a char array created once by the caller
      * @param pos current parse position
      * @return Format description String
      */
-    private String parseFormatDescription(final String pattern, final 
ParsePosition pos) {
+    private String parseFormatDescription(final String pattern, final char[] 
chars, final ParsePosition pos) {
         final int start = pos.getIndex();
-        seekNonWs(pattern, pos);
+        seekNonWs(chars, pos);
         final int text = pos.getIndex();
         int depth = 1;
         while (pos.getIndex() < pattern.length()) {
@@ -393,7 +395,7 @@ private String parseFormatDescription(final String pattern, 
final ParsePosition
                 next(pos);
                 break;
             case QUOTE:
-                getQuotedString(pattern, pos);
+                getQuotedString(chars, pos);
                 break;
             default:
                 next(pos);
@@ -408,18 +410,19 @@ private String parseFormatDescription(final String 
pattern, final ParsePosition
      * Reads the argument index from the current format element
      *
      * @param pattern pattern to parse
+     * @param chars the pattern as a char array created once by the caller
      * @param pos current parse position
      * @return argument index
      */
-    private int readArgumentIndex(final String pattern, final ParsePosition 
pos) {
+    private int readArgumentIndex(final String pattern, final char[] chars, 
final ParsePosition pos) {
         final int start = pos.getIndex();
-        seekNonWs(pattern, pos);
+        seekNonWs(chars, pos);
         final StringBuilder result = new StringBuilder();
         boolean error = false;
         for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
             char c = pattern.charAt(pos.getIndex());
             if (Character.isWhitespace(c)) {
-                seekNonWs(pattern, pos);
+                seekNonWs(chars, pos);
                 if (pos.getIndex() >= pattern.length()) {
                     break;
                 }
@@ -449,11 +452,11 @@ private int readArgumentIndex(final String pattern, final 
ParsePosition pos) {
     /**
      * Consume whitespace from the current parse position.
      *
-     * @param pattern String to read
+     * @param buffer the pattern to read, as a char array created once by the 
caller (avoids
+     *        copying the entire pattern on every call)
      * @param pos current position
      */
-    private void seekNonWs(final String pattern, final ParsePosition pos) {
-        final char[] buffer = pattern.toCharArray();
+    private void seekNonWs(final char[] buffer, final ParsePosition pos) {
         while (pos.getIndex() < buffer.length) {
             final int len = StrMatcher.splitMatcher().isMatch(buffer, 
pos.getIndex());
             if (len == 0) {

Reply via email to