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-fileupload.git


The following commit(s) were added to refs/heads/master by this push:
     new 87064e08 FileItemInputIteratorImpl.findNextItem() now uses the max 
file count. (#481)
87064e08 is described below

commit 87064e08caa6f31a8f337a6ba182bf730407b5de
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Jul 26 14:42:38 2026 -0400

    FileItemInputIteratorImpl.findNextItem() now uses the max file count. (#481)
    
    * FileItemInputIteratorImpl.findNextItem() now uses the max file count.
    
    * Refactor tests to use @ParameterizedTest
    
    * Add `multipart/related` variant of test
    
    * Add `multipart/mixed` variant of test
    
    ---------
    
    Co-authored-by: Piotr P. Karwasz <[email protected]>
---
 .../core/FileItemInputIteratorImpl.java            |  44 +++++---
 ...akartaServletFileUploadGetItemIteratorTest.java | 118 ++++++++++++++++++++
 ...akartaServletFileUploadGetItemIteratorTest.java | 118 ++++++++++++++++++++
 .../JavaxServletFileUploadGetItemIteratorTest.java | 121 ++++++++++++++++++++-
 4 files changed, 385 insertions(+), 16 deletions(-)

diff --git 
a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java
 
b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java
index 79d5f37e..aced187d 100644
--- 
a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java
+++ 
b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java
@@ -57,6 +57,16 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
      */
     private long maxFileSize;
 
+    /**
+     * The current number of files.
+     */
+    private long curFileCount;
+
+    /**
+     * The maximum permitted number of files that may be uploaded in a single 
request. A value of -1 indicates no maximum.
+     */
+    private final long maxFileCount;
+
     /**
      * The multi part stream to process.
      */
@@ -114,12 +124,19 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
         this.fileUpload = fileUpload;
         this.maxSize = fileUpload.getMaxSize();
         this.maxFileSize = fileUpload.getMaxFileSize();
+        this.maxFileCount = fileUpload.getMaxFileCount();
         this.requestContext = Objects.requireNonNull(requestContext, 
"requestContext");
         this.multipartRelated = this.requestContext.isMultipartRelated();
         this.skipPreamble = true;
         findNextItem();
     }
 
+    private void checkMaxFileCount() throws FileUploadFileCountLimitException {
+        if (curFileCount == maxFileCount) {
+            throw new FileUploadFileCountLimitException(String.format("Maximum 
file count %,d exceeded.", maxFileCount), maxFileCount, curFileCount);
+        }
+    }
+
     /**
      * Finds the next item, if any.
      *
@@ -155,13 +172,10 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
             }
             final var headers = 
fileUpload.getParsedHeaders(multi.readHeaders());
             if (multipartRelated) {
+                checkMaxFileCount();
                 currentFieldName = "";
-                currentItem = new FileItemInputImpl(
-                        this, null, null, 
headers.getHeader(AbstractFileUpload.CONTENT_TYPE),
-                        false, getContentLength(headers));
-                currentItem.setHeaders(headers);
-                progressNotifier.noteItem();
-                itemValid = true;
+                currentItem = new FileItemInputImpl(this, null, null, 
headers.getHeader(AbstractFileUpload.CONTENT_TYPE), false, 
getContentLength(headers));
+                itemValid(headers);
                 return true;
             }
             if (currentFieldName == null) {
@@ -180,22 +194,20 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
                         skipPreamble = true;
                         continue;
                     }
+                    checkMaxFileCount();
                     final var fileName = fileUpload.getFileName(headers);
                     currentItem = new FileItemInputImpl(this, fileName, 
fieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), fileName == null,
                             getContentLength(headers));
-                    currentItem.setHeaders(headers);
-                    progressNotifier.noteItem();
-                    itemValid = true;
+                    itemValid(headers);
                     return true;
                 }
             } else {
                 final var fileName = fileUpload.getFileName(headers);
                 if (fileName != null) {
+                    checkMaxFileCount();
                     currentItem = new FileItemInputImpl(this, fileName, 
currentFieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), false,
                             getContentLength(headers));
-                    currentItem.setHeaders(headers);
-                    progressNotifier.noteItem();
-                    itemValid = true;
+                    itemValid(headers);
                     return true;
                 }
             }
@@ -304,6 +316,13 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
         multiPartInput.setHeaderCharset(charset);
     }
 
+    private void itemValid(final FileItemHeaders headers) {
+        currentItem.setHeaders(headers);
+        progressNotifier.noteItem();
+        itemValid = true;
+        curFileCount++;
+    }
+
     /**
      * Returns the next available {@link FileItemInput}.
      *
@@ -336,5 +355,4 @@ class FileItemInputIteratorImpl implements 
FileItemInputIterator {
         // TODO Something better?
         return (Iterator<FileItemInput>) this;
     }
-
 }
diff --git 
a/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletFileUploadGetItemIteratorTest.java
 
b/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletFileUploadGetItemIteratorTest.java
index 2810a927..1a8d4a68 100644
--- 
a/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletFileUploadGetItemIteratorTest.java
+++ 
b/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletFileUploadGetItemIteratorTest.java
@@ -32,7 +32,10 @@ import org.apache.commons.fileupload2.core.FileItemInput;
 import org.apache.commons.fileupload2.core.FileItemInputIterator;
 import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadException;
+import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import jakarta.servlet.http.HttpServletRequest;
 
@@ -50,6 +53,36 @@ class JakartaServletFileUploadGetItemIteratorTest {
     /** Content-type header value that matches {@link #BOUNDARY}. */
     private static final String CONTENT_TYPE = "multipart/form-data; 
boundary=" + BOUNDARY;
 
+    /** Content-type header value for multipart/related requests that matches 
{@link #BOUNDARY}. */
+    private static final String CONTENT_TYPE_RELATED = "multipart/related; 
boundary=" + BOUNDARY;
+
+    /** Boundary value used for nested multipart/mixed parts. */
+    private static final String MIXED_BOUNDARY = "---9876";
+
+    /**
+     * Builds a complete multipart body that contains a single form-data part 
holding a nested multipart/mixed part with {@code fileCount} identical files.
+     *
+     * @param fileCount number of nested files to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMixedFileParts(final int fileCount) {
+        final var sb = new StringBuilder();
+        sb.append("--").append(BOUNDARY).append("\r\n");
+        sb.append("Content-Disposition: form-data; name=\"files\"\r\n");
+        sb.append("Content-Type: multipart/mixed; 
boundary=").append(MIXED_BOUNDARY).append("\r\n");
+        sb.append("\r\n");
+        for (int i = 1; i <= fileCount; i++) {
+            sb.append("--").append(MIXED_BOUNDARY).append("\r\n");
+            sb.append("Content-Disposition: attachment; 
filename=\"file").append(i).append(".txt\"\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("\r\n");
+            sb.append("Content of file ").append(i).append("\r\n");
+        }
+        sb.append("--").append(MIXED_BOUNDARY).append("--\r\n");
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
+
     /**
      * Builds a complete multipart body that contains {@code fileCount} 
identical file parts.
      *
@@ -69,6 +102,25 @@ class JakartaServletFileUploadGetItemIteratorTest {
         return sb.toString().getBytes(StandardCharsets.US_ASCII);
     }
 
+    /**
+     * Builds a complete multipart/related body that contains {@code 
partCount} identical parts.
+     *
+     * @param partCount number of parts to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMultiRelatedParts(final int partCount) {
+        final var sb = new StringBuilder();
+        for (int i = 1; i <= partCount; i++) {
+            sb.append("--").append(BOUNDARY).append("\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("Content-ID: 
<part").append(i).append("@example.org>\r\n");
+            sb.append("\r\n");
+            sb.append("Content of part ").append(i).append("\r\n");
+        }
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
+
     /**
      * Builds a complete multipart body that contains exactly one file part.
      *
@@ -126,6 +178,28 @@ class JakartaServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext(), "Expected no items in an empty multipart 
body");
     }
 
+    /**
+     * When the number of uploaded files exceeds {@code maxFileCount}, 
iterating must throw a {@link FileUploadFileCountLimitException} with the 
expected
+     * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testFileCountLimitExceededThrowsException(final long maxFileCount) 
throws Exception {
+        final var body = buildMultiFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * A file part: the content read from the item's InputStream must match 
the uploaded body.
      */
@@ -328,6 +402,50 @@ class JakartaServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext());
     }
 
+    /**
+     * When the number of files in a nested multipart/mixed part exceeds 
{@code maxFileCount}, iterating must throw a {@link 
FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartMixedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMixedFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
+    /**
+     * When the number of parts in a multipart/related request exceeds {@code 
maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartRelatedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMultiRelatedParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE_RELATED);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * Multiple parts: the iterator must return every part in transmission 
order.
      */
diff --git 
a/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletFileUploadGetItemIteratorTest.java
 
b/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletFileUploadGetItemIteratorTest.java
index fd584251..8eaebeb3 100644
--- 
a/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletFileUploadGetItemIteratorTest.java
+++ 
b/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletFileUploadGetItemIteratorTest.java
@@ -32,7 +32,10 @@ import org.apache.commons.fileupload2.core.FileItemInput;
 import org.apache.commons.fileupload2.core.FileItemInputIterator;
 import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadException;
+import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import jakarta.servlet.http.HttpServletRequest;
 
@@ -50,6 +53,36 @@ class JakartaServletFileUploadGetItemIteratorTest {
     /** Content-type header value that matches {@link #BOUNDARY}. */
     private static final String CONTENT_TYPE = "multipart/form-data; 
boundary=" + BOUNDARY;
 
+    /** Content-type header value for multipart/related requests that matches 
{@link #BOUNDARY}. */
+    private static final String CONTENT_TYPE_RELATED = "multipart/related; 
boundary=" + BOUNDARY;
+
+    /** Boundary value used for nested multipart/mixed parts. */
+    private static final String MIXED_BOUNDARY = "---9876";
+
+    /**
+     * Builds a complete multipart body that contains a single form-data part 
holding a nested multipart/mixed part with {@code fileCount} identical files.
+     *
+     * @param fileCount number of nested files to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMixedFileParts(final int fileCount) {
+        final var sb = new StringBuilder();
+        sb.append("--").append(BOUNDARY).append("\r\n");
+        sb.append("Content-Disposition: form-data; name=\"files\"\r\n");
+        sb.append("Content-Type: multipart/mixed; 
boundary=").append(MIXED_BOUNDARY).append("\r\n");
+        sb.append("\r\n");
+        for (int i = 1; i <= fileCount; i++) {
+            sb.append("--").append(MIXED_BOUNDARY).append("\r\n");
+            sb.append("Content-Disposition: attachment; 
filename=\"file").append(i).append(".txt\"\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("\r\n");
+            sb.append("Content of file ").append(i).append("\r\n");
+        }
+        sb.append("--").append(MIXED_BOUNDARY).append("--\r\n");
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
+
     /**
      * Builds a complete multipart body that contains {@code fileCount} 
identical file parts.
      *
@@ -69,6 +102,25 @@ class JakartaServletFileUploadGetItemIteratorTest {
         return sb.toString().getBytes(StandardCharsets.US_ASCII);
     }
 
+    /**
+     * Builds a complete multipart/related body that contains {@code 
partCount} identical parts.
+     *
+     * @param partCount number of parts to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMultiRelatedParts(final int partCount) {
+        final var sb = new StringBuilder();
+        for (int i = 1; i <= partCount; i++) {
+            sb.append("--").append(BOUNDARY).append("\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("Content-ID: 
<part").append(i).append("@example.org>\r\n");
+            sb.append("\r\n");
+            sb.append("Content of part ").append(i).append("\r\n");
+        }
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
+
     /**
      * Builds a complete multipart body that contains exactly one file part.
      *
@@ -126,6 +178,28 @@ class JakartaServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext(), "Expected no items in an empty multipart 
body");
     }
 
+    /**
+     * When the number of uploaded files exceeds {@code maxFileCount}, 
iterating must throw a {@link FileUploadFileCountLimitException} with the 
expected
+     * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testFileCountLimitExceededThrowsException(final long maxFileCount) 
throws Exception {
+        final var body = buildMultiFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * A file part: the content read from the item's InputStream must match 
the uploaded body.
      */
@@ -328,6 +402,50 @@ class JakartaServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext());
     }
 
+    /**
+     * When the number of files in a nested multipart/mixed part exceeds 
{@code maxFileCount}, iterating must throw a {@link 
FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartMixedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMixedFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
+    /**
+     * When the number of parts in a multipart/related request exceeds {@code 
maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartRelatedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMultiRelatedParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JakartaMockHttpServletRequest(body, CONTENT_TYPE_RELATED);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * Multiple parts: the iterator must return every part in transmission 
order.
      */
diff --git 
a/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxServletFileUploadGetItemIteratorTest.java
 
b/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxServletFileUploadGetItemIteratorTest.java
index 6d73a1fd..84df9771 100644
--- 
a/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxServletFileUploadGetItemIteratorTest.java
+++ 
b/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxServletFileUploadGetItemIteratorTest.java
@@ -27,14 +27,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.apache.commons.fileupload2.core.DiskFileItemFactory;
 import org.apache.commons.fileupload2.core.FileItemInput;
 import org.apache.commons.fileupload2.core.FileItemInputIterator;
 import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadException;
+import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
 import org.junit.jupiter.api.Test;
-
-import javax.servlet.http.HttpServletRequest;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * Tests for {@link 
JakartaServletFileUpload#getItemIterator(HttpServletRequest)}.
@@ -46,9 +49,36 @@ class JavaxServletFileUploadGetItemIteratorTest {
 
     /** Boundary value used throughout these tests. */
     private static final String BOUNDARY = "---1234";
-
     /** Content-type header value that matches {@link #BOUNDARY}. */
     private static final String CONTENT_TYPE = "multipart/form-data; 
boundary=" + BOUNDARY;
+    /** Content-type header value for multipart/related requests that matches 
{@link #BOUNDARY}. */
+    private static final String CONTENT_TYPE_RELATED = "multipart/related; 
boundary=" + BOUNDARY;
+    /** Boundary value used for nested multipart/mixed parts. */
+    private static final String MIXED_BOUNDARY = "---9876";
+
+    /**
+     * Builds a complete multipart body that contains a single form-data part 
holding a nested multipart/mixed part with {@code fileCount} identical files.
+     *
+     * @param fileCount number of nested files to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMixedFileParts(final int fileCount) {
+        final var sb = new StringBuilder();
+        sb.append("--").append(BOUNDARY).append("\r\n");
+        sb.append("Content-Disposition: form-data; name=\"files\"\r\n");
+        sb.append("Content-Type: multipart/mixed; 
boundary=").append(MIXED_BOUNDARY).append("\r\n");
+        sb.append("\r\n");
+        for (int i = 1; i <= fileCount; i++) {
+            sb.append("--").append(MIXED_BOUNDARY).append("\r\n");
+            sb.append("Content-Disposition: attachment; 
filename=\"file").append(i).append(".txt\"\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("\r\n");
+            sb.append("Content of file ").append(i).append("\r\n");
+        }
+        sb.append("--").append(MIXED_BOUNDARY).append("--\r\n");
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
 
     /**
      * Builds a complete multipart body that contains {@code fileCount} 
identical file parts.
@@ -69,6 +99,25 @@ class JavaxServletFileUploadGetItemIteratorTest {
         return sb.toString().getBytes(StandardCharsets.US_ASCII);
     }
 
+    /**
+     * Builds a complete multipart/related body that contains {@code 
partCount} identical parts.
+     *
+     * @param partCount number of parts to include
+     * @return raw multipart bytes encoded in US-ASCII
+     */
+    private static byte[] buildMultiRelatedParts(final int partCount) {
+        final var sb = new StringBuilder();
+        for (int i = 1; i <= partCount; i++) {
+            sb.append("--").append(BOUNDARY).append("\r\n");
+            sb.append("Content-Type: text/plain\r\n");
+            sb.append("Content-ID: 
<part").append(i).append("@example.org>\r\n");
+            sb.append("\r\n");
+            sb.append("Content of part ").append(i).append("\r\n");
+        }
+        sb.append("--").append(BOUNDARY).append("--\r\n");
+        return sb.toString().getBytes(StandardCharsets.US_ASCII);
+    }
+
     /**
      * Builds a complete multipart body that contains exactly one file part.
      *
@@ -126,6 +175,28 @@ class JavaxServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext(), "Expected no items in an empty multipart 
body");
     }
 
+    /**
+     * When the number of uploaded files exceeds {@code maxFileCount}, 
iterating must throw a {@link FileUploadFileCountLimitException} with the 
expected
+     * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testFileCountLimitExceededThrowsException(final long maxFileCount) 
throws Exception {
+        final var body = buildMultiFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JavaxMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * A file part: the content read from the item's InputStream must match 
the uploaded body.
      */
@@ -328,6 +399,50 @@ class JavaxServletFileUploadGetItemIteratorTest {
         assertFalse(iter.hasNext());
     }
 
+    /**
+     * When the number of files in a nested multipart/mixed part exceeds 
{@code maxFileCount}, iterating must throw a {@link 
FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartMixedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMixedFileParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JavaxMockHttpServletRequest(body, CONTENT_TYPE);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
+    /**
+     * When the number of parts in a multipart/related request exceeds {@code 
maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException}
+     * with the expected permitted count. Tested for maxFileCount values of 1, 
2, 4, and 8.
+     */
+    @ParameterizedTest
+    @ValueSource(longs = { 1, 2, 4, 8 })
+    void testMultipartRelatedFileCountLimitExceededThrowsException(final long 
maxFileCount) throws Exception {
+        final var body = buildMultiRelatedParts((int) maxFileCount + 1);
+        final HttpServletRequest request = new 
JavaxMockHttpServletRequest(body, CONTENT_TYPE_RELATED);
+        final var upload = newUpload();
+        upload.setMaxFileCount(maxFileCount);
+        final FileItemInputIterator iter = upload.getItemIterator(request);
+        // Consume allowed items first
+        for (int i = 0; i < maxFileCount; i++) {
+            assertTrue(iter.hasNext());
+            iter.next();
+        }
+        // The next hasNext() call triggers the limit check
+        final var ex = assertThrows(FileUploadFileCountLimitException.class, 
iter::hasNext);
+        assertEquals(maxFileCount, ex.getPermitted());
+    }
+
     /**
      * Multiple parts: the iterator must return every part in transmission 
order.
      */

Reply via email to