From 1afa356c115ecd9262c2da3cf319d863844e1549 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 25 Jul 2026 16:06:18 +0000 Subject: [PATCH 1/4] FileItemInputIteratorImpl.findNextItem() now uses the max file count. --- .../core/FileItemInputIteratorImpl.java | 44 +++++++++++++------ ...aServletFileUploadGetItemIteratorTest.java | 38 ++++++++++++++++ ...aServletFileUploadGetItemIteratorTest.java | 38 ++++++++++++++++ ...xServletFileUploadGetItemIteratorTest.java | 43 ++++++++++++++++-- 4 files changed, 147 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 79d5f37ea..aced187de 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 @@ private boolean findNextItem() throws FileUploadException, IOException { } 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 @@ private boolean findNextItem() throws FileUploadException, IOException { 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 @@ protected void init(final AbstractFileUpload fileUpload, final RequestC 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 @@ public Iterator unwrap() { // TODO Something better? return (Iterator) 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 2810a927b..39bc06686 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,6 +32,7 @@ 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 jakarta.servlet.http.HttpServletRequest; @@ -126,6 +127,43 @@ void testEmptyMultipartBody() throws Exception { 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. + */ + @Test + void testFileCountLimitExceededThrowsException() throws Exception { + final long maxFileCount = 2; + 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-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. + */ + @Test + void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { + final var body = buildMultiFileParts(2); + final HttpServletRequest request = new JakartaMockHttpServletRequest(body, CONTENT_TYPE); + final var upload = newUpload(); + upload.setMaxFileCount(1L); + final FileItemInputIterator iter = upload.getItemIterator(request); + assertTrue(iter.hasNext()); + iter.next(); // consume the first (allowed) item + assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); + } + /** * A file part: the content read from the item's InputStream must match the uploaded body. */ 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 fd5842512..21fe9d9a0 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,6 +32,7 @@ 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 jakarta.servlet.http.HttpServletRequest; @@ -126,6 +127,43 @@ void testEmptyMultipartBody() throws Exception { 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. + */ + @Test + void testFileCountLimitExceededThrowsException() throws Exception { + final long maxFileCount = 2; + 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-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. + */ + @Test + void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { + final var body = buildMultiFileParts(2); + final HttpServletRequest request = new JakartaMockHttpServletRequest(body, CONTENT_TYPE); + final var upload = newUpload(); + upload.setMaxFileCount(1L); + final FileItemInputIterator iter = upload.getItemIterator(request); + assertTrue(iter.hasNext()); + iter.next(); // consume the first (allowed) item + assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); + } + /** * A file part: the content read from the item's InputStream must match the uploaded body. */ 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 6d73a1fd6..5ee460a1d 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,15 +27,16 @@ 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; - /** * Tests for {@link JakartaServletFileUpload#getItemIterator(HttpServletRequest)}. *

@@ -46,7 +47,6 @@ 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; @@ -126,6 +126,43 @@ void testEmptyMultipartBody() throws Exception { 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. + */ + @Test + void testFileCountLimitExceededThrowsException() throws Exception { + final long maxFileCount = 2; + 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-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. + */ + @Test + void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { + final var body = buildMultiFileParts(2); + final HttpServletRequest request = new JavaxMockHttpServletRequest(body, CONTENT_TYPE); + final var upload = newUpload(); + upload.setMaxFileCount(1L); + final FileItemInputIterator iter = upload.getItemIterator(request); + assertTrue(iter.hasNext()); + iter.next(); // consume the first (allowed) item + assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); + } + /** * A file part: the content read from the item's InputStream must match the uploaded body. */ From 01f472aebcb18c724cee8a884c297d780001eecc Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sun, 26 Jul 2026 09:06:49 -0400 Subject: [PATCH 2/4] Refactor tests to use @ParameterizedTest --- ...aServletFileUploadGetItemIteratorTest.java | 25 +++++-------------- ...aServletFileUploadGetItemIteratorTest.java | 25 +++++-------------- ...xServletFileUploadGetItemIteratorTest.java | 25 +++++-------------- 3 files changed, 18 insertions(+), 57 deletions(-) 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 39bc06686..78aa889ca 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 @@ -34,6 +34,8 @@ 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; @@ -129,11 +131,11 @@ void testEmptyMultipartBody() throws Exception { /** * When the number of uploaded files exceeds {@code maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException} with the expected - * permitted count. + * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8. */ - @Test - void testFileCountLimitExceededThrowsException() throws Exception { - final long maxFileCount = 2; + @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(); @@ -149,21 +151,6 @@ void testFileCountLimitExceededThrowsException() throws Exception { assertEquals(maxFileCount, ex.getPermitted()); } - /** - * A file-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. - */ - @Test - void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { - final var body = buildMultiFileParts(2); - final HttpServletRequest request = new JakartaMockHttpServletRequest(body, CONTENT_TYPE); - final var upload = newUpload(); - upload.setMaxFileCount(1L); - final FileItemInputIterator iter = upload.getItemIterator(request); - assertTrue(iter.hasNext()); - iter.next(); // consume the first (allowed) item - assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); - } - /** * A file part: the content read from the item's InputStream must match the uploaded body. */ 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 21fe9d9a0..93d61cb11 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 @@ -34,6 +34,8 @@ 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; @@ -129,11 +131,11 @@ void testEmptyMultipartBody() throws Exception { /** * When the number of uploaded files exceeds {@code maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException} with the expected - * permitted count. + * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8. */ - @Test - void testFileCountLimitExceededThrowsException() throws Exception { - final long maxFileCount = 2; + @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(); @@ -149,21 +151,6 @@ void testFileCountLimitExceededThrowsException() throws Exception { assertEquals(maxFileCount, ex.getPermitted()); } - /** - * A file-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. - */ - @Test - void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { - final var body = buildMultiFileParts(2); - final HttpServletRequest request = new JakartaMockHttpServletRequest(body, CONTENT_TYPE); - final var upload = newUpload(); - upload.setMaxFileCount(1L); - final FileItemInputIterator iter = upload.getItemIterator(request); - assertTrue(iter.hasNext()); - iter.next(); // consume the first (allowed) item - assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); - } - /** * A file part: the content read from the item's InputStream must match the uploaded body. */ 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 5ee460a1d..e1d3d52f3 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 @@ -36,6 +36,8 @@ 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; /** * Tests for {@link JakartaServletFileUpload#getItemIterator(HttpServletRequest)}. @@ -128,11 +130,11 @@ void testEmptyMultipartBody() throws Exception { /** * When the number of uploaded files exceeds {@code maxFileCount}, iterating must throw a {@link FileUploadFileCountLimitException} with the expected - * permitted count. + * permitted count. Tested for maxFileCount values of 1, 2, 4, and 8. */ - @Test - void testFileCountLimitExceededThrowsException() throws Exception { - final long maxFileCount = 2; + @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(); @@ -148,21 +150,6 @@ void testFileCountLimitExceededThrowsException() throws Exception { assertEquals(maxFileCount, ex.getPermitted()); } - /** - * A file-count limit of 1 allows exactly one file; attempting to move to the second must throw {@link FileUploadFileCountLimitException}. - */ - @Test - void testFileCountLimitOfOneThrowsOnSecondFile() throws Exception { - final var body = buildMultiFileParts(2); - final HttpServletRequest request = new JavaxMockHttpServletRequest(body, CONTENT_TYPE); - final var upload = newUpload(); - upload.setMaxFileCount(1L); - final FileItemInputIterator iter = upload.getItemIterator(request); - assertTrue(iter.hasNext()); - iter.next(); // consume the first (allowed) item - assertThrows(FileUploadFileCountLimitException.class, iter::hasNext); - } - /** * A file part: the content read from the item's InputStream must match the uploaded body. */ From 1a9a4a3d2658d6f43359e540965d9c1afbe04072 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 26 Jul 2026 18:47:01 +0200 Subject: [PATCH 3/4] Add `multipart/related` variant of test --- ...aServletFileUploadGetItemIteratorTest.java | 44 +++++++++++++++++++ ...aServletFileUploadGetItemIteratorTest.java | 44 +++++++++++++++++++ ...xServletFileUploadGetItemIteratorTest.java | 43 ++++++++++++++++++ 3 files changed, 131 insertions(+) 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 78aa889ca..450c964f3 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 @@ -53,6 +53,9 @@ 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; + /** * Builds a complete multipart body that contains {@code fileCount} identical file parts. * @@ -72,6 +75,25 @@ private static byte[] buildMultiFileParts(final int fileCount) { 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: \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. * @@ -353,6 +375,28 @@ void testMixedFormFieldAndFileParts() throws Exception { assertFalse(iter.hasNext()); } + /** + * 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 93d61cb11..7ee17a7b3 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 @@ -53,6 +53,9 @@ 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; + /** * Builds a complete multipart body that contains {@code fileCount} identical file parts. * @@ -72,6 +75,25 @@ private static byte[] buildMultiFileParts(final int fileCount) { 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: \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. * @@ -353,6 +375,28 @@ void testMixedFormFieldAndFileParts() throws Exception { assertFalse(iter.hasNext()); } + /** + * 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 e1d3d52f3..98e9b7aac 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 @@ -51,6 +51,8 @@ class JavaxServletFileUploadGetItemIteratorTest { 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; /** * Builds a complete multipart body that contains {@code fileCount} identical file parts. @@ -71,6 +73,25 @@ private static byte[] buildMultiFileParts(final int fileCount) { 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: \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. * @@ -352,6 +373,28 @@ void testMixedFormFieldAndFileParts() throws Exception { assertFalse(iter.hasNext()); } + /** + * 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. */ From f3f45ec5a0e5a4495149f990d4b7002ba3944f01 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 26 Jul 2026 19:32:25 +0200 Subject: [PATCH 4/4] Add `multipart/mixed` variant of test --- ...aServletFileUploadGetItemIteratorTest.java | 49 +++++++++++++++++++ ...aServletFileUploadGetItemIteratorTest.java | 49 +++++++++++++++++++ ...xServletFileUploadGetItemIteratorTest.java | 48 ++++++++++++++++++ 3 files changed, 146 insertions(+) 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 450c964f3..1a8d4a68d 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 @@ -56,6 +56,33 @@ class JakartaServletFileUploadGetItemIteratorTest { /** 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. * @@ -375,6 +402,28 @@ void testMixedFormFieldAndFileParts() throws Exception { 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. 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 7ee17a7b3..8eaebeb34 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 @@ -56,6 +56,33 @@ class JakartaServletFileUploadGetItemIteratorTest { /** 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. * @@ -375,6 +402,28 @@ void testMixedFormFieldAndFileParts() throws Exception { 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. 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 98e9b7aac..84df9771d 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 @@ -53,6 +53,32 @@ class JavaxServletFileUploadGetItemIteratorTest { 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. @@ -373,6 +399,28 @@ void testMixedFormFieldAndFileParts() throws Exception { 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.