This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-mime4j.git

commit 23a562ac629abf7527034e8e9e051675873e5baf
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 14:52:39 2026 +0100

    [PERF] LenientAddressParser: do not rescan for ']' after each unclosed '['
    
    parseDomain called copyDomainLiteral at every '['. When no ']' follows,
    the scan ran to the next delimiter or the end of the value and was
    repeated for the next '[': O(k*n) for k unclosed brackets, over 16 s for
    a 32 KB To header under the fuzzer.
    
    copyDomainLiteral now returns where a failed scan stopped. Every '['
    before that index is unclosed as well, so parseDomain copies it as
    content without scanning again.
    
    Found by twake-fuzz, scenario 33-header-value-cost-scaling.
---
 .../mime4j/field/address/LenientAddressParser.java     | 17 +++++++++++------
 .../field/address/LenientAddressBuilderTest.java       | 18 ++++++++++++++++++
 2 files changed, 29 insertions(+), 6 deletions(-)

diff --git 
a/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java
 
b/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java
index e184eab6..761b995b 100644
--- 
a/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java
+++ 
b/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java
@@ -90,6 +90,9 @@ public class LenientAddressParser implements AddressParser {
 
     String parseDomain(final ByteSequence buf, final ParserCursor cursor, 
final BitSet delimiters) {
         StringBuilder dst = new StringBuilder();
+        // Every '[' before this index has no ']' before the next delimiter: a 
failed scan for
+        // the closing bracket is not repeated for each of them, which would 
be quadratic.
+        int unclosedUntil = -1;
         while (!cursor.atEnd()) {
             char current = (char) (buf.byteAt(cursor.getPos()) & 0xff);
             if (delimiters != null && delimiters.get(current)) {
@@ -98,7 +101,8 @@ public class LenientAddressParser implements AddressParser {
                 this.parser.skipWhiteSpace(buf, cursor);
             } else if (current == '(') {
                 this.parser.skipComment(buf, cursor);
-            } else if (current == '[' && copyDomainLiteral(buf, cursor, 
delimiters, dst)) {
+            } else if (current == '[' && cursor.getPos() >= unclosedUntil
+                    && (unclosedUntil = copyDomainLiteral(buf, cursor, 
delimiters, dst)) < 0) {
                 continue;
             } else {
                 this.parser.copyContent(buf, cursor, delimiters, dst);
@@ -109,9 +113,10 @@ public class LenientAddressParser implements AddressParser 
{
 
     /**
      * Copies a domain literal up to its closing bracket: '(' and ')' are 
dtext there, not a comment
-     * (RFC 5322 section 3.4.1). Returns false, copying nothing, when no ']' 
comes before a delimiter.
+     * (RFC 5322 section 3.4.1). Returns -1 once copied. When no ']' comes 
before a delimiter, copies
+     * nothing and returns the index where the search stopped: the delimiter, 
or the upper bound.
      */
-    private boolean copyDomainLiteral(final ByteSequence buf, final 
ParserCursor cursor, final BitSet delimiters,
+    private int copyDomainLiteral(final ByteSequence buf, final ParserCursor 
cursor, final BitSet delimiters,
                                       final StringBuilder dst) {
         int from = cursor.getPos();
         int to = cursor.getUpperBound();
@@ -128,13 +133,13 @@ public class LenientAddressParser implements 
AddressParser {
                 // UTF-8, as the other parts of the header
                 dst.append(ContentUtil.decode(StandardCharsets.UTF_8, 
literal));
                 cursor.updatePos(i + 1);
-                return true;
+                return -1;
             }
             if (delimiters != null && delimiters.get(current)) {
-                return false;
+                return i;
             }
         }
-        return false;
+        return to;
     }
 
     DomainList parseRoute(final ByteSequence buf, final ParserCursor cursor, 
final BitSet delimiters) {
diff --git 
a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
 
b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
index d3e21c69..651267ac 100644
--- 
a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
+++ 
b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java
@@ -647,4 +647,22 @@ public class LenientAddressBuilderTest {
         Assert.assertEquals("a", AddressFormatter.DEFAULT.encode(new 
Mailbox("a", ",;")));
         Assert.assertEquals("John <a>", AddressFormatter.DEFAULT.encode(new 
Mailbox("John", "a", "<>")));
     }
+
+    @Test(timeout = 2000)
+    public void parseAddressListShouldBeLinearOnManyUnclosedDomainLiterals() {
+        StringBuilder sb = new StringBuilder("a@b");
+        for (int i = 0; i < 100000; i++) {
+            sb.append("[x ");
+        }
+        AddressList list = parser.parseAddressList(sb);
+        Assert.assertEquals(1, list.size());
+        Assert.assertEquals("b[x[x[x", ((Mailbox) 
list.get(0)).getDomain().substring(0, 7));
+    }
+
+    @Test
+    public void anUnclosedBracketShouldNotHideALaterDomainLiteral() {
+        AddressList list = parser.parseAddressList("a@b[x [y, c@[1.2.3.4]");
+        Assert.assertEquals(2, list.size());
+        Assert.assertEquals("[1.2.3.4]", ((Mailbox) list.get(1)).getDomain());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to