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 726042ff212a9056ce41d620367314976f46398d
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 15:10:13 2026 +0100

    [FIX] MimeBoundaryInputStream: scan the buffered bytes before reading more
    
    The constructor always read the underlying stream first, before looking
    for the delimiter in the bytes already buffered. For a nested multipart
    read in one go, the enclosing part was then already exhausted: in strict
    mode, the extra read of an enclosing part without a closing delimiter
    threw "Unexpected end of stream" before the nested preamble, while small
    reads reported the preamble and the next part first. The constructor now
    scans the buffered bytes first. That read also told, as a side effect,
    whether the source ends right after a delimiter line: skipBoundary now
    reads ahead when nothing is buffered after the delimiter, so that eof()
    stays the same.
    
    Fixes #111
---
 .../james/mime4j/io/MimeBoundaryInputStream.java   | 22 +++++++--
 .../mime4j/stream/StrictMimeTokenStreamTest.java   | 53 ++++++++++++++++++++++
 2 files changed, 71 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java 
b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
index 609a5d9c..6169d39d 100644
--- a/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
+++ b/core/src/main/java/org/apache/james/mime4j/io/MimeBoundaryInputStream.java
@@ -104,7 +104,12 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
             this.boundary[i + 2] = boundaryBytes.byteAt(i);
         }
 
-        fillBuffer();
+        if (inbuffer.hasBufferedData()) {
+            // Scan the bytes already buffered before reading more: the 
enclosing part may hold no more
+            findLimit();
+        } else {
+            fillBuffer();
+        }
         // Read until we know whether the stream is empty, whatever the size 
of the reads
         while (!atBoundary && !eof && !hasData()) {
             fillBuffer();
@@ -266,10 +271,16 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
         } else {
             bytesRead = 0;
         }
-        if (atBoundary) {
-            return bytesRead;
+        if (!atBoundary) {
+            findLimit();
         }
+        return bytesRead;
+    }
 
+    /**
+     * Sets the limit of the readable data from the buffered bytes: up to the 
delimiter if they hold it.
+     */
+    private void findLimit() throws IOException {
         int i = findDelimiter();
         if (i != -1) {
             limit = i;
@@ -288,7 +299,6 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
             }
             limit = end;
         }
-        return bytesRead;
     }
 
     /**
@@ -437,6 +447,10 @@ public class MimeBoundaryInputStream extends 
LineReaderInputStream {
                     fillBuffer();
                 }
             }
+            if (!eof && !buffer.hasBufferedData()) {
+                // Find out whether anything follows the delimiter line
+                fillBuffer();
+            }
         }
     }
 
diff --git 
a/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java
 
b/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java
index 02c3dbf5..b0c8ab68 100644
--- 
a/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java
+++ 
b/core/src/test/java/org/apache/james/mime4j/stream/StrictMimeTokenStreamTest.java
@@ -19,13 +19,19 @@
 
 package org.apache.james.mime4j.stream;
 
+import org.apache.james.mime4j.MimeException;
 import org.apache.james.mime4j.MimeIOException;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 
 public class StrictMimeTokenStreamTest {
 
@@ -94,6 +100,53 @@ public class StrictMimeTokenStreamTest {
         }
     }
 
+    private static final String NESTED_UNTERMINATED =
+        "Content-Type: multipart/mixed; boundary=a\r\n\r\n"
+        + "--a\r\n"
+        + "Content-Type: multipart/mixed; boundary=b\r\n\r\n"
+        + "Th\r\n"
+        + "--b\r\n"
+        + "x";
+
+    private static List<String> events(InputStream in) throws Exception {
+        MimeTokenStream stream = new MimeTokenStream(MimeConfig.STRICT);
+        stream.parse(in);
+        List<String> events = new ArrayList<>();
+        try {
+            for (EntityState s = stream.getState(); s != 
EntityState.T_END_OF_STREAM; s = stream.next()) {
+                if (s == EntityState.T_PREAMBLE || s == EntityState.T_BODY || 
s == EntityState.T_EPILOGUE) {
+                    ByteArrayOutputStream out = new ByteArrayOutputStream();
+                    InputStream body = stream.getInputStream();
+                    byte[] b = new byte[512];
+                    int n;
+                    while ((n = body.read(b)) != -1) {
+                        out.write(b, 0, n);
+                    }
+                    events.add(s + " " + out.toString("US-ASCII"));
+                } else {
+                    events.add(s.toString());
+                }
+            }
+        } catch (MimeException | IOException e) {
+            events.add("EXCEPTION " + e.getMessage());
+        }
+        return events;
+    }
+
+    @Test
+    public void 
nestedMultipartWithoutClosingDelimiterDoesNotDependOnReadSizes() throws 
Exception {
+        byte[] message = NESTED_UNTERMINATED.getBytes("US-ASCII");
+        InputStream oneByteReads = new FilterInputStream(new 
ByteArrayInputStream(message)) {
+            @Override
+            public int read(byte[] b, int off, int len) throws IOException {
+                return super.read(b, off, Math.min(len, 1));
+            }
+        };
+        List<String> whole = events(new ByteArrayInputStream(message));
+        Assert.assertTrue(whole.toString(), whole.contains("T_PREAMBLE Th"));
+        Assert.assertEquals(events(oneByteReads), whole);
+    }
+
     private void checkNextIs(EntityState expected) throws Exception {
         Assert.assertEquals(MimeTokenStream.stateToString(expected), 
MimeTokenStream.stateToString(parser.next()));
     }


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

Reply via email to