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.
*/