You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2025/06/03 13:37:59 UTC
(tomcat) branch main updated: Update fork of Commons FileUpload to latest 1.x
This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 81f27a990f Update fork of Commons FileUpload to latest 1.x
81f27a990f is described below
commit 81f27a990fd780c6a4ced79f2f830716f455e695
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Jun 3 14:37:38 2025 +0100
Update fork of Commons FileUpload to latest 1.x
---
MERGE.txt | 2 +-
.../tomcat/util/http/fileupload/FileItem.java | 140 ++-
.../util/http/fileupload/FileItemHeaders.java | 22 +-
.../util/http/fileupload/FileItemStream.java | 49 +-
.../tomcat/util/http/fileupload/FileUpload.java | 12 +-
.../util/http/fileupload/FileUploadBase.java | 472 ++++----
.../util/http/fileupload/FileUploadException.java | 12 +-
.../http/fileupload/InvalidFileNameException.java | 10 +-
.../util/http/fileupload/MultipartStream.java | 1197 ++++++++++----------
.../util/http/fileupload/ParameterParser.java | 291 +++--
.../util/http/fileupload/ProgressListener.java | 8 +-
.../util/http/fileupload/disk/DiskFileItem.java | 652 +++++------
.../http/fileupload/disk/DiskFileItemFactory.java | 115 +-
.../tomcat/util/http/fileupload/package-info.java | 4 +-
.../fileupload/servlet/ServletRequestContext.java | 40 +-
.../http/fileupload/util/FileItemHeadersImpl.java | 37 +-
.../http/fileupload/util/LimitedInputStream.java | 81 +-
.../tomcat/util/http/fileupload/util/Streams.java | 74 +-
.../http/fileupload/util/mime/MimeUtility.java | 18 +-
.../http/fileupload/util/mime/ParseException.java | 2 +
.../util/mime/QuotedPrintableDecoder.java | 53 +-
.../http/fileupload/util/mime/RFC2231Utility.java | 93 +-
webapps/docs/changelog.xml | 4 +
23 files changed, 1650 insertions(+), 1738 deletions(-)
diff --git a/MERGE.txt b/MERGE.txt
index c81293c991..89ba88208a 100644
--- a/MERGE.txt
+++ b/MERGE.txt
@@ -46,7 +46,7 @@ Branch: 1.x
Sub-tree:
src/main/java/org/apache/commons/fileupload
The SHA1 ID / tag for the most recent commit to be merged to Tomcat is:
-7a8c3241cfa8d036452cd4fc3f92d57cff189bca (2023-09-16)
+abe5d94b7ef6ff7164b48b5bb3781979ebbafb0e (2025-06-03)
Note: Tomcat's copy of fileupload also includes classes copied manually from
Commons IO.
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItem.java b/java/org/apache/tomcat/util/http/fileupload/FileItem.java
index e6f035fc7b..00316cc3ed 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileItem.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileItem.java
@@ -20,7 +20,6 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
/**
@@ -47,18 +46,21 @@ import java.io.UnsupportedEncodingException;
*/
public interface FileItem extends FileItemHeadersSupport {
- // ------------------------------- Methods from javax.activation.DataSource
+ /**
+ * Deletes the underlying storage for a file item, including deleting any
+ * associated temporary disk file. Although this storage will be deleted
+ * automatically when the {@code FileItem} instance is garbage
+ * collected, this method can be used to ensure that this is done at an
+ * earlier time, thus preserving system resources.
+ */
+ void delete();
/**
- * Returns an {@link java.io.InputStream InputStream} that can be
- * used to retrieve the contents of the file.
- *
- * @return An {@link java.io.InputStream InputStream} that can be
- * used to retrieve the contents of the file.
+ * Returns the contents of the file item as an array of bytes.
*
- * @throws IOException if an error occurs.
+ * @return The contents of the file item as an array of bytes.
*/
- InputStream getInputStream() throws IOException;
+ byte[] get();
/**
* Returns the content type passed by the browser or {@code null} if
@@ -69,6 +71,25 @@ public interface FileItem extends FileItemHeadersSupport {
*/
String getContentType();
+ /**
+ * Returns the name of the field in the multipart form corresponding to
+ * this file item.
+ *
+ * @return The name of the form field.
+ */
+ String getFieldName();
+
+ /**
+ * Returns an {@link java.io.InputStream InputStream} that can be
+ * used to retrieve the contents of the file.
+ *
+ * @return An {@link java.io.InputStream InputStream} that can be
+ * used to retrieve the contents of the file.
+ *
+ * @throws IOException if an error occurs.
+ */
+ InputStream getInputStream() throws IOException;
+
/**
* Returns the original file name in the client's file system, as provided by
* the browser (or other client software). In most cases, this will be the
@@ -83,16 +104,16 @@ public interface FileItem extends FileItemHeadersSupport {
*/
String getName();
- // ------------------------------------------------------- FileItem methods
-
/**
- * Provides a hint as to whether or not the file contents will be read
- * from memory.
+ * Returns an {@link java.io.OutputStream OutputStream} that can
+ * be used for storing the contents of the file.
*
- * @return {@code true} if the file contents will be read from memory;
- * {@code false} otherwise.
+ * @return An {@link java.io.OutputStream OutputStream} that can be used
+ * for storing the contents of the file.
+ *
+ * @throws IOException if an error occurs.
*/
- boolean isInMemory();
+ OutputStream getOutputStream() throws IOException;
/**
* Returns the size of the file item.
@@ -102,13 +123,13 @@ public interface FileItem extends FileItemHeadersSupport {
long getSize();
/**
- * Returns the contents of the file item as an array of bytes.
- *
- * @return The contents of the file item as an array of bytes.
+ * Returns the contents of the file item as a String, using the default
+ * character encoding. This method uses {@link #get()} to retrieve the
+ * contents of the item.
*
- * @throws UncheckedIOException if an I/O error occurs
+ * @return The contents of the item, as a string.
*/
- byte[] get() throws UncheckedIOException;
+ String getString();
/**
* Returns the contents of the file item as a String, using the specified
@@ -121,53 +142,26 @@ public interface FileItem extends FileItemHeadersSupport {
*
* @throws UnsupportedEncodingException if the requested character
* encoding is not available.
- * @throws IOException if an I/O error occurs
- */
- String getString(String encoding) throws UnsupportedEncodingException, IOException;
-
- /**
- * Returns the contents of the file item as a String, using the default
- * character encoding. This method uses {@link #get()} to retrieve the
- * contents of the item.
- *
- * @return The contents of the item, as a string.
*/
- String getString();
+ String getString(String encoding) throws UnsupportedEncodingException;
/**
- * A convenience method to write an uploaded item to disk. The client code
- * is not concerned with whether or not the item is stored in memory, or on
- * disk in a temporary location. They just want to write the uploaded item
- * to a file.
- * <p>
- * This method is not guaranteed to succeed if called more than once for
- * the same item. This allows a particular implementation to use, for
- * example, file renaming, where possible, rather than copying all of the
- * underlying data, thus gaining a significant performance benefit.
- *
- * @param file The {@code File} into which the uploaded item should
- * be stored.
+ * Determines whether or not a {@code FileItem} instance represents
+ * a simple form field.
*
- * @throws Exception if an error occurs.
- */
- void write(File file) throws Exception;
-
- /**
- * Deletes the underlying storage for a file item, including deleting any
- * associated temporary disk file. Although this storage will be deleted
- * automatically when the {@code FileItem} instance is garbage
- * collected, this method can be used to ensure that this is done at an
- * earlier time, thus preserving system resources.
+ * @return {@code true} if the instance represents a simple form
+ * field; {@code false} if it represents an uploaded file.
*/
- void delete();
+ boolean isFormField();
/**
- * Returns the name of the field in the multipart form corresponding to
- * this file item.
+ * Provides a hint as to whether or not the file contents will be read
+ * from memory.
*
- * @return The name of the form field.
+ * @return {@code true} if the file contents will be read from memory;
+ * {@code false} otherwise.
*/
- String getFieldName();
+ boolean isInMemory();
/**
* Sets the field name used to reference this file item.
@@ -176,15 +170,6 @@ public interface FileItem extends FileItemHeadersSupport {
*/
void setFieldName(String name);
- /**
- * Determines whether or not a {@code FileItem} instance represents
- * a simple form field.
- *
- * @return {@code true} if the instance represents a simple form
- * field; {@code false} if it represents an uploaded file.
- */
- boolean isFormField();
-
/**
* Specifies whether or not a {@code FileItem} instance represents
* a simple form field.
@@ -195,14 +180,21 @@ public interface FileItem extends FileItemHeadersSupport {
void setFormField(boolean state);
/**
- * Returns an {@link java.io.OutputStream OutputStream} that can
- * be used for storing the contents of the file.
+ * A convenience method to write an uploaded item to disk. The client code
+ * is not concerned with whether or not the item is stored in memory, or on
+ * disk in a temporary location. They just want to write the uploaded item
+ * to a file.
+ * <p>
+ * This method is not guaranteed to succeed if called more than once for
+ * the same item. This allows a particular implementation to use, for
+ * example, file renaming, where possible, rather than copying all of the
+ * underlying data, thus gaining a significant performance benefit.
*
- * @return An {@link java.io.OutputStream OutputStream} that can be used
- * for storing the contents of the file.
+ * @param file The {@code File} into which the uploaded item should
+ * be stored.
*
- * @throws IOException if an error occurs.
+ * @throws Exception if an error occurs.
*/
- OutputStream getOutputStream() throws IOException;
+ void write(File file) throws Exception;
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
index 890f4397ae..30b1d40e25 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
@@ -42,6 +42,17 @@ public interface FileItemHeaders {
*/
String getHeader(String name);
+ /**
+ * <p>
+ * Returns an {@code Iterator} of all the header names.
+ * </p>
+ *
+ * @return an {@code Iterator} containing all of the names of
+ * headers provided with this file item. If the item does not have
+ * any headers return an empty {@code Iterator}
+ */
+ Iterator<String> getHeaderNames();
+
/**
* <p>
* Returns all the values of the specified item header as an
@@ -60,15 +71,4 @@ public interface FileItemHeaders {
*/
Iterator<String> getHeaders(String name);
- /**
- * <p>
- * Returns an {@code Iterator} of all the header names.
- * </p>
- *
- * @return an {@code Iterator} containing all of the names of
- * headers provided with this file item. If the item does not have
- * any headers return an empty {@code Iterator}
- */
- Iterator<String> getHeaderNames();
-
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
index c31d9eb13b..e8734e4c93 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
@@ -48,20 +48,14 @@ public interface FileItemStream extends FileItemHeadersSupport {
*/
private static final long serialVersionUID = -7280778431581963740L;
- }
+ /**
+ * Constructs a new instance.
+ */
+ public ItemSkippedException() {
+ // empty
+ }
- /**
- * Creates an {@link InputStream}, which allows to read the
- * items contents.
- *
- * @return The input stream, from which the items data may
- * be read.
- * @throws IllegalStateException The method was already invoked on
- * this item. It is not possible to recreate the data stream.
- * @throws IOException An I/O error occurred.
- * @see ItemSkippedException
- */
- InputStream openStream() throws IOException;
+ }
/**
* Returns the content type passed by the browser or {@code null} if
@@ -72,6 +66,14 @@ public interface FileItemStream extends FileItemHeadersSupport {
*/
String getContentType();
+ /**
+ * Returns the name of the field in the multipart form corresponding to
+ * this file item.
+ *
+ * @return The name of the form field.
+ */
+ String getFieldName();
+
/**
* Returns the original file name in the client's file system, as provided by
* the browser (or other client software). In most cases, this will be the
@@ -82,14 +84,6 @@ public interface FileItemStream extends FileItemHeadersSupport {
*/
String getName();
- /**
- * Returns the name of the field in the multipart form corresponding to
- * this file item.
- *
- * @return The name of the form field.
- */
- String getFieldName();
-
/**
* Determines whether or not a {@code FileItem} instance represents
* a simple form field.
@@ -99,4 +93,17 @@ public interface FileItemStream extends FileItemHeadersSupport {
*/
boolean isFormField();
+ /**
+ * Creates an {@link InputStream}, which allows to read the
+ * items contents.
+ *
+ * @return The input stream, from which the items data may
+ * be read.
+ * @throws IllegalStateException The method was already invoked on
+ * this item. It is not possible to recreate the data stream.
+ * @throws IOException An I/O error occurred.
+ * @see ItemSkippedException
+ */
+ InputStream openStream() throws IOException;
+
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
index 06846f18d6..cd16f07f83 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
@@ -33,15 +33,11 @@ package org.apache.tomcat.util.http.fileupload;
public class FileUpload
extends FileUploadBase {
- // ----------------------------------------------------------- Data members
-
/**
* The factory to use to create new form items.
*/
private FileItemFactory fileItemFactory;
- // ----------------------------------------------------------- Constructors
-
/**
* Constructs an uninitialized instance of this class.
*
@@ -52,8 +48,6 @@ public class FileUpload
public FileUpload() {
}
- // ----------------------------------------------------- Property accessors
-
/**
* Returns the factory class used when creating file items.
*
@@ -67,11 +61,11 @@ public class FileUpload
/**
* Sets the factory class to use when creating file items.
*
- * @param factory The factory class for new file items.
+ * @param fileItemFactory The factory class for new file items.
*/
@Override
- public void setFileItemFactory(final FileItemFactory factory) {
- this.fileItemFactory = factory;
+ public void setFileItemFactory(final FileItemFactory fileItemFactory) {
+ this.fileItemFactory = fileItemFactory;
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
index b9f5cfe174..73e5f1e63e 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
@@ -33,9 +33,10 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
/**
- * <p>High level API for processing file uploads.</p>
+ * High level API for processing file uploads.
*
- * <p>This class handles multiple files per single HTML widget, sent using
+ * <p>
+ * This class handles multiple files per single HTML widget, sent using
* {@code multipart/mixed} encoding type, as specified by
* <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
* #parseRequest(RequestContext)} to acquire a list of {@link
@@ -48,7 +49,15 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
*/
public abstract class FileUploadBase {
- // ----------------------------------------------------- Manifest constants
+ /**
+ * Line feed.
+ */
+ private static final char LF = '\n';
+
+ /**
+ * Carriage return.
+ */
+ private static final char CR = '\r';
/**
* HTTP content type header name.
@@ -90,8 +99,6 @@ public abstract class FileUploadBase {
*/
public static final String MULTIPART_MIXED = "multipart/mixed";
- // ----------------------------------------------------------- Data members
-
/**
* The maximum size permitted for the complete request, as opposed to
* {@link #fileSizeMax}. A value of -1 indicates no maximum.
@@ -120,70 +127,65 @@ public abstract class FileUploadBase {
*/
private ProgressListener listener;
- // ----------------------------------------------------- Property accessors
-
- /**
- * Returns the factory class used when creating file items.
- *
- * @return The factory class for new file items.
- */
- public abstract FileItemFactory getFileItemFactory();
-
/**
- * Sets the factory class to use when creating file items.
- *
- * @param factory The factory class for new file items.
- */
- public abstract void setFileItemFactory(FileItemFactory factory);
-
- /**
- * Returns the maximum allowed size of a complete request, as opposed
- * to {@link #getFileSizeMax()}.
- *
- * @return The maximum allowed size, in bytes. The default value of
- * -1 indicates, that there is no limit.
- *
- * @see #setSizeMax(long)
- *
+ * Constructs a new instance.
*/
- public long getSizeMax() {
- return sizeMax;
+ public FileUploadBase() {
+ // empty
}
/**
- * Sets the maximum allowed size of a complete request, as opposed
- * to {@link #setFileSizeMax(long)}.
+ * Gets the boundary from the {@code Content-type} header.
*
- * @param sizeMax The maximum allowed size, in bytes. The default value of
- * -1 indicates, that there is no limit.
- *
- * @see #getSizeMax()
+ * @param contentType The value of the content type header from which to
+ * extract the boundary value.
*
+ * @return The boundary, as a byte array.
*/
- public void setSizeMax(final long sizeMax) {
- this.sizeMax = sizeMax;
+ public byte[] getBoundary(final String contentType) {
+ final ParameterParser parser = new ParameterParser();
+ parser.setLowerCaseNames(true);
+ // Parameter parser can handle null input
+ final Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
+ final String boundaryStr = params.get("boundary");
+
+ if (boundaryStr == null) {
+ return null;
+ }
+ return boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
}
/**
- * Returns the maximum allowed size of a single uploaded file,
- * as opposed to {@link #getSizeMax()}.
+ * Gets the field name from the {@code Content-disposition}
+ * header.
*
- * @see #setFileSizeMax(long)
- * @return Maximum size of a single uploaded file.
+ * @param headers A {@code Map} containing the HTTP request headers.
+ *
+ * @return The field name for the current {@code encapsulation}.
*/
- public long getFileSizeMax() {
- return fileSizeMax;
+ public String getFieldName(final FileItemHeaders headers) {
+ return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
}
/**
- * Sets the maximum allowed size of a single uploaded file,
- * as opposed to {@link #getSizeMax()}.
- *
- * @see #getFileSizeMax()
- * @param fileSizeMax Maximum size of a single uploaded file.
+ * Returns the field name, which is given by the content-disposition
+ * header.
+ * @param contentDisposition The content-dispositions header value.
+ * @return The field name.
*/
- public void setFileSizeMax(final long fileSizeMax) {
- this.fileSizeMax = fileSizeMax;
+ private String getFieldName(final String contentDisposition) {
+ String fieldName = null;
+ if (contentDisposition != null && contentDisposition.toLowerCase(Locale.ROOT).startsWith(FORM_DATA)) {
+ final ParameterParser parser = new ParameterParser();
+ parser.setLowerCaseNames(true);
+ // Parameter parser can handle null input
+ final Map<String, String> params = parser.parse(contentDisposition, ';');
+ fieldName = params.get("name");
+ if (fieldName != null) {
+ fieldName = fieldName.trim();
+ }
+ }
+ return fieldName;
}
/**
@@ -196,155 +198,14 @@ public abstract class FileUploadBase {
}
/**
- * Sets the maximum number of files allowed per request.
- *
- * @param fileCountMax The new limit. {@code -1} means no limit.
- */
- public void setFileCountMax(final long fileCountMax) {
- this.fileCountMax = fileCountMax;
- }
-
- /**
- * Retrieves the character encoding used when reading the headers of an
- * individual part. When not specified, or {@code null}, the request
- * encoding is used. If that is also not specified, or {@code null},
- * the platform default encoding is used.
- *
- * @return The encoding used to read part headers.
- */
- public String getHeaderEncoding() {
- return headerEncoding;
- }
-
- /**
- * Specifies the character encoding to be used when reading the headers of
- * individual part. When not specified, or {@code null}, the request
- * encoding is used. If that is also not specified, or {@code null},
- * the platform default encoding is used.
- *
- * @param encoding The encoding used to read part headers.
- */
- public void setHeaderEncoding(final String encoding) {
- headerEncoding = encoding;
- }
-
- // --------------------------------------------------------- Public methods
-
- /**
- * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
- * compliant {@code multipart/form-data} stream.
- *
- * @param ctx The context for the request to be parsed.
- *
- * @return An iterator to instances of {@code FileItemStream}
- * parsed from the request, in the order that they were
- * transmitted.
- *
- * @throws FileUploadException if there are problems reading/parsing
- * the request or storing files.
- * @throws IOException An I/O error occurred. This may be a network
- * error while communicating with the client or a problem while
- * storing the uploaded content.
- */
- public FileItemIterator getItemIterator(final RequestContext ctx)
- throws FileUploadException, IOException {
- try {
- return new FileItemIteratorImpl(this, ctx);
- } catch (final FileUploadIOException e) {
- // unwrap encapsulated SizeException
- throw (FileUploadException) e.getCause();
- }
- }
-
- /**
- * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
- * compliant {@code multipart/form-data} stream.
- *
- * @param ctx The context for the request to be parsed.
- *
- * @return A list of {@code FileItem} instances parsed from the
- * request, in the order that they were transmitted.
- *
- * @throws FileUploadException if there are problems reading/parsing
- * the request or storing files.
- */
- public List<FileItem> parseRequest(final RequestContext ctx)
- throws FileUploadException {
- final List<FileItem> items = new ArrayList<>();
- boolean successful = false;
- try {
- final FileItemIterator iter = getItemIterator(ctx);
- final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
- "No FileItemFactory has been set.");
- final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
- while (iter.hasNext()) {
- if (items.size() == fileCountMax) {
- // The next item will exceed the limit.
- throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
- }
- final FileItemStream item = iter.next();
- // Don't use getName() here to prevent an InvalidFileNameException.
- final String fileName = item.getName();
- final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
- item.isFormField(), fileName);
- items.add(fileItem);
- try {
- Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
- } catch (final FileUploadIOException e) {
- throw (FileUploadException) e.getCause();
- } catch (final IOException e) {
- throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
- MULTIPART_FORM_DATA, e.getMessage()), e);
- }
- final FileItemHeaders fih = item.getHeaders();
- fileItem.setHeaders(fih);
- }
- successful = true;
- return items;
- } catch (final FileUploadException e) {
- throw e;
- } catch (final IOException e) {
- throw new FileUploadException(e.getMessage(), e);
- } finally {
- if (!successful) {
- for (final FileItem fileItem : items) {
- try {
- fileItem.delete();
- } catch (final Exception ignored) {
- // ignored TODO perhaps add to tracker delete failure list somehow?
- }
- }
- }
- }
- }
-
- // ------------------------------------------------------ Protected methods
-
- /**
- * Retrieves the boundary from the {@code Content-type} header.
- *
- * @param contentType The value of the content type header from which to
- * extract the boundary value.
+ * Returns the factory class used when creating file items.
*
- * @return The boundary, as a byte array.
+ * @return The factory class for new file items.
*/
- public byte[] getBoundary(final String contentType) {
- final ParameterParser parser = new ParameterParser();
- parser.setLowerCaseNames(true);
- // Parameter parser can handle null input
- final Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
- final String boundaryStr = params.get("boundary");
-
- if (boundaryStr == null) {
- return null;
- }
- final byte[] boundary;
- boundary = boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
- return boundary;
- }
+ public abstract FileItemFactory getFileItemFactory();
/**
- * Retrieves the file name from the {@code Content-disposition}
+ * Gets the file name from the {@code Content-disposition}
* header.
*
* @param headers The HTTP headers object.
@@ -357,18 +218,18 @@ public abstract class FileUploadBase {
/**
* Returns the given content-disposition headers file name.
- * @param pContentDisposition The content-disposition headers value.
+ * @param contentDisposition The content-disposition headers value.
* @return The file name
*/
- private String getFileName(final String pContentDisposition) {
+ private String getFileName(final String contentDisposition) {
String fileName = null;
- if (pContentDisposition != null) {
- final String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH);
+ if (contentDisposition != null) {
+ final String cdl = contentDisposition.toLowerCase(Locale.ROOT);
if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
final ParameterParser parser = new ParameterParser();
parser.setLowerCaseNames(true);
// Parameter parser can handle null input
- final Map<String, String> params = parser.parse(pContentDisposition, ';');
+ final Map<String, String> params = parser.parse(contentDisposition, ';');
if (params.containsKey("filename")) {
fileName = params.get("filename");
if (fileName != null) {
@@ -386,37 +247,52 @@ public abstract class FileUploadBase {
}
/**
- * Retrieves the field name from the {@code Content-disposition}
- * header.
+ * Returns the maximum allowed size of a single uploaded file,
+ * as opposed to {@link #getSizeMax()}.
*
- * @param headers A {@code Map} containing the HTTP request headers.
+ * @see #setFileSizeMax(long)
+ * @return Maximum size of a single uploaded file.
+ */
+ public long getFileSizeMax() {
+ return fileSizeMax;
+ }
+
+ /**
+ * Gets the character encoding used when reading the headers of an
+ * individual part. When not specified, or {@code null}, the request
+ * encoding is used. If that is also not specified, or {@code null},
+ * the platform default encoding is used.
*
- * @return The field name for the current {@code encapsulation}.
+ * @return The encoding used to read part headers.
*/
- public String getFieldName(final FileItemHeaders headers) {
- return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
+ public String getHeaderEncoding() {
+ return headerEncoding;
}
/**
- * Returns the field name, which is given by the content-disposition
- * header.
- * @param pContentDisposition The content-dispositions header value.
- * @return The field jake
+ * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
+ * compliant {@code multipart/form-data} stream.
+ *
+ * @param ctx The context for the request to be parsed.
+ *
+ * @return An iterator to instances of {@code FileItemStream}
+ * parsed from the request, in the order that they were
+ * transmitted.
+ *
+ * @throws FileUploadException if there are problems reading/parsing
+ * the request or storing files.
+ * @throws IOException An I/O error occurred. This may be a network
+ * error while communicating with the client or a problem while
+ * storing the uploaded content.
*/
- private String getFieldName(final String pContentDisposition) {
- String fieldName = null;
- if (pContentDisposition != null
- && pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) {
- final ParameterParser parser = new ParameterParser();
- parser.setLowerCaseNames(true);
- // Parameter parser can handle null input
- final Map<String, String> params = parser.parse(pContentDisposition, ';');
- fieldName = params.get("name");
- if (fieldName != null) {
- fieldName = fieldName.trim();
- }
+ public FileItemIterator getItemIterator(final RequestContext ctx)
+ throws FileUploadException, IOException {
+ try {
+ return new FileItemIteratorImpl(this, ctx);
+ } catch (final FileUploadIOException e) {
+ // unwrap encapsulated SizeException
+ throw (FileUploadException) e.getCause();
}
- return fieldName;
}
/**
@@ -464,6 +340,29 @@ public abstract class FileUploadBase {
return headers;
}
+ /**
+ * Returns the progress listener.
+ *
+ * @return The progress listener, if any, or null.
+ */
+ public ProgressListener getProgressListener() {
+ return listener;
+ }
+
+ /**
+ * Returns the maximum allowed size of a complete request, as opposed
+ * to {@link #getFileSizeMax()}.
+ *
+ * @return The maximum allowed size, in bytes. The default value of
+ * -1 indicates, that there is no limit.
+ *
+ * @see #setSizeMax(long)
+ *
+ */
+ public long getSizeMax() {
+ return sizeMax;
+ }
+
/**
* Creates a new instance of {@link FileItemHeaders}.
* @return The new instance.
@@ -483,12 +382,12 @@ public abstract class FileUploadBase {
private int parseEndOfLine(final String headerPart, final int end) {
int index = end;
for (;;) {
- final int offset = headerPart.indexOf('\r', index);
+ final int offset = headerPart.indexOf(CR, index);
if (offset == -1 || offset + 1 >= headerPart.length()) {
throw new IllegalStateException(
"Expected headers to be terminated by an empty line.");
}
- if (headerPart.charAt(offset + 1) == '\n') {
+ if (headerPart.charAt(offset + 1) == LF) {
return offset;
}
index = offset + 1;
@@ -512,21 +411,126 @@ public abstract class FileUploadBase {
}
/**
- * Returns the progress listener.
+ * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
+ * compliant {@code multipart/form-data} stream.
*
- * @return The progress listener, if any, or null.
+ * @param ctx The context for the request to be parsed.
+ *
+ * @return A list of {@code FileItem} instances parsed from the
+ * request, in the order that they were transmitted.
+ *
+ * @throws FileUploadException if there are problems reading/parsing
+ * the request or storing files.
*/
- public ProgressListener getProgressListener() {
- return listener;
+ public List<FileItem> parseRequest(final RequestContext ctx) throws FileUploadException {
+ final List<FileItem> items = new ArrayList<>();
+ boolean successful = false;
+ try {
+ final FileItemIterator iter = getItemIterator(ctx);
+ final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
+ "No FileItemFactory has been set.");
+ final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
+ while (iter.hasNext()) {
+ if (items.size() == fileCountMax) {
+ // The next item will exceed the limit.
+ throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
+ }
+ final FileItemStream item = iter.next();
+ // Don't use getName() here to prevent an InvalidFileNameException.
+ final String fileName = item.getName();
+ final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
+ item.isFormField(), fileName);
+ items.add(fileItem);
+ try {
+ Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
+ } catch (final FileUploadIOException e) {
+ throw (FileUploadException) e.getCause();
+ } catch (final IOException e) {
+ throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
+ MULTIPART_FORM_DATA, e.getMessage()), e);
+ }
+ final FileItemHeaders fih = item.getHeaders();
+ fileItem.setHeaders(fih);
+ }
+ successful = true;
+ return items;
+ } catch (final FileUploadException e) {
+ throw e;
+ } catch (final IOException e) {
+ throw new FileUploadException(e.getMessage(), e);
+ } finally {
+ if (!successful) {
+ for (final FileItem fileItem : items) {
+ try {
+ fileItem.delete();
+ } catch (final Exception ignored) {
+ // ignored TODO perhaps add to tracker delete failure list somehow?
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the maximum number of files allowed per request.
+ *
+ * @param fileCountMax The new limit. {@code -1} means no limit.
+ */
+ public void setFileCountMax(final long fileCountMax) {
+ this.fileCountMax = fileCountMax;
+ }
+
+ /**
+ * Sets the factory class to use when creating file items.
+ *
+ * @param factory The factory class for new file items.
+ */
+ public abstract void setFileItemFactory(FileItemFactory factory);
+
+ /**
+ * Sets the maximum allowed size of a single uploaded file,
+ * as opposed to {@link #getSizeMax()}.
+ *
+ * @see #getFileSizeMax()
+ * @param fileSizeMax Maximum size of a single uploaded file.
+ */
+ public void setFileSizeMax(final long fileSizeMax) {
+ this.fileSizeMax = fileSizeMax;
+ }
+
+ /**
+ * Specifies the character encoding to be used when reading the headers of
+ * individual part. When not specified, or {@code null}, the request
+ * encoding is used. If that is also not specified, or {@code null},
+ * the platform default encoding is used.
+ *
+ * @param encoding The encoding used to read part headers.
+ */
+ public void setHeaderEncoding(final String encoding) {
+ headerEncoding = encoding;
}
/**
* Sets the progress listener.
*
- * @param pListener The progress listener, if any. Defaults to null.
+ * @param listener The progress listener, if any. Defaults to null.
*/
- public void setProgressListener(final ProgressListener pListener) {
- listener = pListener;
+ public void setProgressListener(final ProgressListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Sets the maximum allowed size of a complete request, as opposed
+ * to {@link #setFileSizeMax(long)}.
+ *
+ * @param sizeMax The maximum allowed size, in bytes. The default value of
+ * -1 indicates, that there is no limit.
+ *
+ * @see #getSizeMax()
+ *
+ */
+ public void setSizeMax(final long sizeMax) {
+ this.sizeMax = sizeMax;
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
index d7a8d11819..ea4226e140 100644
--- a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
+++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
@@ -36,20 +36,20 @@ public class FileUploadException extends IOException {
* Constructs a new {@code FileUploadException} with specified detail
* message.
*
- * @param msg the error message.
+ * @param message the error message.
*/
- public FileUploadException(final String msg) {
- super(msg);
+ public FileUploadException(final String message) {
+ super(message);
}
/**
* Creates a new {@code FileUploadException} with the given
* detail message and cause.
*
- * @param msg The exceptions detail message.
+ * @param message The exceptions detail message.
* @param cause The exceptions cause.
*/
- public FileUploadException(final String msg, final Throwable cause) {
- super(msg, cause);
+ public FileUploadException(final String message, final Throwable cause) {
+ super(message, cause);
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
index 5018620fac..ecc1dce896 100644
--- a/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
+++ b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
@@ -42,12 +42,12 @@ public class InvalidFileNameException extends RuntimeException {
/**
* Creates a new instance.
*
- * @param pName The file name causing the exception.
- * @param pMessage A human readable error message.
+ * @param name The file name causing the exception.
+ * @param message A human readable error message.
*/
- public InvalidFileNameException(final String pName, final String pMessage) {
- super(pMessage);
- name = pName;
+ public InvalidFileNameException(final String name, final String message) {
+ super(message);
+ this.name = name;
}
/**
diff --git a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
index 44e52563e2..97c412da66 100644
--- a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
+++ b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
@@ -27,7 +27,7 @@ import org.apache.tomcat.util.http.fileupload.util.Closeable;
import org.apache.tomcat.util.http.fileupload.util.Streams;
/**
- * <p> Low level API for processing file uploads.
+ * Low level API for processing file uploads.
*
* <p> This class can be used to process data streams conforming to MIME
* 'multipart' format as defined in
@@ -58,7 +58,9 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
* boundary token of the same length as the parent stream (see {@link
* #setBoundary(byte[])}).
*
- * <p>Here is an example of usage of this class.<br>
+ * <p>
+ * Here is an example of usage of this class.
+ * </p>
*
* <pre>
* try {
@@ -82,145 +84,473 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
public class MultipartStream {
/**
- * Internal class, which is used to invoke the
- * {@link ProgressListener}.
+ * Thrown upon attempt of setting an invalid boundary token.
*/
- public static class ProgressNotifier {
+ public static class IllegalBoundaryException extends IOException {
/**
- * The listener to invoke.
+ * The UID to use when serializing this instance.
*/
- private final ProgressListener listener;
+ private static final long serialVersionUID = -161533165102632918L;
/**
- * Number of expected bytes, if known, or -1.
+ * Constructs an {@code IllegalBoundaryException} with no
+ * detail message.
*/
- private final long contentLength;
+ public IllegalBoundaryException() {
+ }
/**
- * Number of bytes, which have been read so far.
+ * Constructs an {@code IllegalBoundaryException} with
+ * the specified detail message.
+ *
+ * @param message The detail message.
*/
- private long bytesRead;
+ public IllegalBoundaryException(final String message) {
+ super(message);
+ }
+
+ }
+
+ /**
+ * An {@link InputStream} for reading an items contents.
+ */
+ public class ItemInputStream extends InputStream implements Closeable {
/**
- * Number of items, which have been read so far.
+ * Offset when converting negative bytes to integers.
*/
- private int items;
+ private static final int BYTE_POSITIVE_OFFSET = 256;
/**
- * Creates a new instance with the given listener
- * and content length.
- *
- * @param pListener The listener to invoke.
- * @param pContentLength The expected content length.
+ * The number of bytes, which have been read so far.
+ */
+ private long total;
+
+ /**
+ * The number of bytes, which must be hold, because
+ * they might be a part of the boundary.
+ */
+ private int pad;
+
+ /**
+ * The current offset in the buffer.
+ */
+ private int pos;
+
+ /**
+ * Whether the stream is already closed.
+ */
+ private boolean closed;
+
+ /**
+ * Creates a new instance.
*/
- public ProgressNotifier(final ProgressListener pListener, final long pContentLength) {
- listener = pListener;
- contentLength = pContentLength;
+ ItemInputStream() {
+ findSeparator();
}
/**
- * Called to indicate that bytes have been read.
+ * Returns the number of bytes, which are currently
+ * available, without blocking.
*
- * @param pBytes Number of bytes, which have been read.
+ * @throws IOException An I/O error occurs.
+ * @return Number of bytes in the buffer.
*/
- void noteBytesRead(final int pBytes) {
- /* Indicates, that the given number of bytes have been read from
- * the input stream.
- */
- bytesRead += pBytes;
- notifyListener();
+ @Override
+ public int available() throws IOException {
+ if (pos == -1) {
+ return tail - head - pad;
+ }
+ return pos - head;
}
/**
- * Called to indicate, that a new file item has been detected.
+ * Closes the input stream.
+ *
+ * @throws IOException An I/O error occurred.
*/
- public void noteItem() {
- ++items;
- notifyListener();
+ @Override
+ public void close() throws IOException {
+ close(false);
}
/**
- * Called for notifying the listener.
+ * Closes the input stream.
+ *
+ * @param closeUnderlying Whether to close the underlying stream (hard close)
+ * @throws IOException An I/O error occurred.
*/
- private void notifyListener() {
- if (listener != null) {
- listener.update(bytesRead, contentLength, items);
+ public void close(final boolean closeUnderlying) throws IOException {
+ if (closed) {
+ return;
}
+ if (closeUnderlying) {
+ closed = true;
+ input.close();
+ } else {
+ for (;;) {
+ int available = available();
+ if (available == 0) {
+ available = makeAvailable();
+ if (available == 0) {
+ break;
+ }
+ }
+ if (skip(available) != available) {
+ // TODO log or throw?
+ }
+ }
+ }
+ closed = true;
}
- }
-
- // ----------------------------------------------------- Manifest constants
-
- /**
- * The Carriage Return ASCII character value.
- */
- public static final byte CR = 0x0D;
-
- /**
- * The Line Feed ASCII character value.
- */
- public static final byte LF = 0x0A;
-
- /**
- * The dash (-) ASCII character value.
- */
- public static final byte DASH = 0x2D;
-
- /**
- * The maximum length of {@code header-part} that will be
- * processed (10 kilobytes = 10240 bytes.).
- */
- public static final int HEADER_PART_SIZE_MAX = 10240;
+ /**
+ * Called for finding the separator.
+ */
+ private void findSeparator() {
+ pos = MultipartStream.this.findSeparator();
+ if (pos == -1) {
+ if (tail - head > keepRegion) {
+ pad = keepRegion;
+ } else {
+ pad = tail - head;
+ }
+ }
+ }
- /**
- * The default length of the buffer used for processing a request.
- */
- protected static final int DEFAULT_BUFSIZE = 4096;
+ /**
+ * Returns the number of bytes, which have been read
+ * by the stream.
+ *
+ * @return Number of bytes, which have been read so far.
+ */
+ public long getBytesRead() {
+ return total;
+ }
- /**
- * A byte sequence that marks the end of {@code header-part}
- * ({@code CRLFCRLF}).
- */
- protected static final byte[] HEADER_SEPARATOR = {CR, LF, CR, LF};
+ /**
+ * Returns, whether the stream is closed.
+ *
+ * @return True, if the stream is closed, otherwise false.
+ */
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
- /**
- * A byte sequence that follows a delimiter that will be
- * followed by an encapsulation ({@code CRLF}).
- */
- protected static final byte[] FIELD_SEPARATOR = {CR, LF};
+ /**
+ * Attempts to read more data.
+ *
+ * @return Number of available bytes
+ * @throws IOException An I/O error occurred.
+ */
+ private int makeAvailable() throws IOException {
+ if (pos != -1) {
+ return 0;
+ }
- /**
- * A byte sequence that follows a delimiter of the last
- * encapsulation in the stream ({@code --}).
- */
- protected static final byte[] STREAM_TERMINATOR = {DASH, DASH};
+ // Move the data to the beginning of the buffer.
+ total += tail - head - pad;
+ System.arraycopy(buffer, tail - pad, buffer, 0, pad);
- /**
- * A byte sequence that precedes a boundary ({@code CRLF--}).
- */
- protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
+ // Refill buffer with new data.
+ head = 0;
+ tail = pad;
- // ----------------------------------------------------------- Data members
+ for (;;) {
+ final int bytesRead = input.read(buffer, tail, bufSize - tail);
+ if (bytesRead == -1) {
+ // The last pad amount is left in the buffer.
+ // Boundary can't be in there so signal an error
+ // condition.
+ final String msg = "Stream ended unexpectedly";
+ throw new MalformedStreamException(msg);
+ }
+ if (notifier != null) {
+ notifier.noteBytesRead(bytesRead);
+ }
+ tail += bytesRead;
- /**
- * The input stream from which data is read.
- */
- private final InputStream input;
+ findSeparator();
+ final int av = available();
- /**
- * The length of the boundary token plus the leading {@code CRLF--}.
- */
- private int boundaryLength;
+ if (av > 0 || pos != -1) {
+ return av;
+ }
+ }
+ }
- /**
- * The amount of data, in bytes, that must be kept in the buffer in order
- * to detect delimiters reliably.
- */
- private final int keepRegion;
+ /**
+ * Returns the next byte in the stream.
+ *
+ * @return The next byte in the stream, as a non-negative
+ * integer, or -1 for EOF.
+ * @throws IOException An I/O error occurred.
+ */
+ @Override
+ public int read() throws IOException {
+ if (closed) {
+ throw new FileItemStream.ItemSkippedException();
+ }
+ if (available() == 0 && makeAvailable() == 0) {
+ return -1;
+ }
+ ++total;
+ final int b = buffer[head++];
+ if (b >= 0) {
+ return b;
+ }
+ return b + BYTE_POSITIVE_OFFSET;
+ }
- /**
+ /**
+ * Reads bytes into the given buffer.
+ *
+ * @param b The destination buffer, where to write to.
+ * @param off Offset of the first byte in the buffer.
+ * @param len Maximum number of bytes to read.
+ * @return Number of bytes, which have been actually read,
+ * or -1 for EOF.
+ * @throws IOException An I/O error occurred.
+ */
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ if (closed) {
+ throw new FileItemStream.ItemSkippedException();
+ }
+ if (len == 0) {
+ return 0;
+ }
+ int res = available();
+ if (res == 0) {
+ res = makeAvailable();
+ if (res == 0) {
+ return -1;
+ }
+ }
+ res = Math.min(res, len);
+ System.arraycopy(buffer, head, b, off, res);
+ head += res;
+ total += res;
+ return res;
+ }
+
+ /**
+ * Skips the given number of bytes.
+ *
+ * @param bytes Number of bytes to skip.
+ * @return The number of bytes, which have actually been
+ * skipped.
+ * @throws IOException An I/O error occurred.
+ */
+ @Override
+ public long skip(final long bytes) throws IOException {
+ if (closed) {
+ throw new FileItemStream.ItemSkippedException();
+ }
+ int av = available();
+ if (av == 0) {
+ av = makeAvailable();
+ if (av == 0) {
+ return 0;
+ }
+ }
+ final long res = Math.min(av, bytes);
+ head += res;
+ return res;
+ }
+
+ }
+
+ /**
+ * Thrown to indicate that the input stream fails to follow the
+ * required syntax.
+ */
+ public static class MalformedStreamException extends IOException {
+
+ /**
+ * The UID to use when serializing this instance.
+ */
+ private static final long serialVersionUID = 6466926458059796677L;
+
+ /**
+ * Constructs a {@code MalformedStreamException} with no
+ * detail message.
+ */
+ public MalformedStreamException() {
+ }
+
+ /**
+ * Constructs an {@code MalformedStreamException} with
+ * the specified detail message.
+ *
+ * @param message The detail message.
+ */
+ public MalformedStreamException(final String message) {
+ super(message);
+ }
+
+ }
+
+ /**
+ * Internal class, which is used to invoke the
+ * {@link ProgressListener}.
+ */
+ public static class ProgressNotifier {
+
+ /**
+ * The listener to invoke.
+ */
+ private final ProgressListener listener;
+
+ /**
+ * Number of expected bytes, if known, or -1.
+ */
+ private final long contentLength;
+
+ /**
+ * Number of bytes, which have been read so far.
+ */
+ private long bytesRead;
+
+ /**
+ * Number of items, which have been read so far.
+ */
+ private int items;
+
+ /**
+ * Creates a new instance with the given listener
+ * and content length.
+ *
+ * @param listener The listener to invoke.
+ * @param contentLength The expected content length.
+ */
+ public ProgressNotifier(final ProgressListener listener, final long contentLength) {
+ this.listener = listener;
+ this.contentLength = contentLength;
+ }
+
+ /**
+ * Called to indicate that bytes have been read.
+ *
+ * @param count Number of bytes, which have been read.
+ */
+ void noteBytesRead(final int count) {
+ /* Indicates, that the given number of bytes have been read from
+ * the input stream.
+ */
+ bytesRead += count;
+ notifyListener();
+ }
+
+ /**
+ * Called to indicate, that a new file item has been detected.
+ */
+ public void noteItem() {
+ ++items;
+ notifyListener();
+ }
+
+ /**
+ * Called for notifying the listener.
+ */
+ private void notifyListener() {
+ if (listener != null) {
+ listener.update(bytesRead, contentLength, items);
+ }
+ }
+
+ }
+
+ /**
+ * The Carriage Return ASCII character value.
+ */
+ public static final byte CR = 0x0D;
+
+ /**
+ * The Line Feed ASCII character value.
+ */
+ public static final byte LF = 0x0A;
+
+ /**
+ * The dash (-) ASCII character value.
+ */
+ public static final byte DASH = 0x2D;
+
+ /**
+ * The maximum length of {@code header-part} that will be
+ * processed (10 kilobytes = 10240 bytes.).
+ */
+ public static final int HEADER_PART_SIZE_MAX = 10240;
+
+ /**
+ * The default length of the buffer used for processing a request.
+ */
+ protected static final int DEFAULT_BUFSIZE = 4096;
+
+ /**
+ * A byte sequence that marks the end of {@code header-part}
+ * ({@code CRLFCRLF}).
+ */
+ protected static final byte[] HEADER_SEPARATOR = {CR, LF, CR, LF};
+
+ /**
+ * A byte sequence that follows a delimiter that will be
+ * followed by an encapsulation ({@code CRLF}).
+ */
+ protected static final byte[] FIELD_SEPARATOR = {CR, LF};
+
+ /**
+ * A byte sequence that follows a delimiter of the last
+ * encapsulation in the stream ({@code --}).
+ */
+ protected static final byte[] STREAM_TERMINATOR = {DASH, DASH};
+
+ /**
+ * A byte sequence that precedes a boundary ({@code CRLF--}).
+ */
+ protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
+
+ /**
+ * Compares {@code count} first bytes in the arrays
+ * {@code a} and {@code b}.
+ *
+ * @param a The first array to compare.
+ * @param b The second array to compare.
+ * @param count How many bytes should be compared.
+ *
+ * @return {@code true} if {@code count} first bytes in arrays
+ * {@code a} and {@code b} are equal.
+ */
+ public static boolean arrayequals(final byte[] a,
+ final byte[] b,
+ final int count) {
+ for (int i = 0; i < count; i++) {
+ if (a[i] != b[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * The input stream from which data is read.
+ */
+ private final InputStream input;
+
+ /**
+ * The length of the boundary token plus the leading {@code CRLF--}.
+ */
+ private int boundaryLength;
+
+ /**
+ * The amount of data, in bytes, that must be kept in the buffer in order
+ * to detect delimiters reliably.
+ */
+ private final int keepRegion;
+
+ /**
* The byte sequence that partitions the stream.
*/
private final byte[] boundary;
@@ -264,31 +594,23 @@ public class MultipartStream {
*/
private final ProgressNotifier notifier;
- // ----------------------------------------------------------- Constructors
-
/**
- * <p> Constructs a {@code MultipartStream} with a custom size buffer.
- *
- * <p> Note that the buffer must be at least big enough to contain the
- * boundary string, plus 4 characters for CR/LF and double dash, plus at
- * least one byte of data. Too small a buffer size setting will degrade
- * performance.
+ * Constructs a {@code MultipartStream} with a custom size buffer.
+ * <p>
+ * Note that the buffer must be at least big enough to contain the boundary string, plus 4 characters for CR/LF and
+ * double dash, plus at least one byte of data. Too small a buffer size setting will degrade performance.
*
* @param input The {@code InputStream} to serve as a data source.
- * @param boundary The token used for dividing the stream into
- * {@code encapsulations}.
+ * @param boundary The token used for dividing the stream into {@code encapsulations}.
* @param bufSize The size of the buffer to be used, in bytes.
- * @param pNotifier The notifier, which is used for calling the
- * progress listener, if any.
+ * @param notifier The notifier, which is used for calling the progress listener, if any.
*
* @throws IllegalArgumentException If the buffer size is too small
*
* @since FileUpload 1.3.1
*/
- public MultipartStream(final InputStream input,
- final byte[] boundary,
- final int bufSize,
- final ProgressNotifier pNotifier) {
+ public MultipartStream(final InputStream input, final byte[] boundary, final int bufSize,
+ final ProgressNotifier notifier) {
if (boundary == null) {
throw new IllegalArgumentException("boundary may not be null");
@@ -297,93 +619,138 @@ public class MultipartStream {
// body-data tokens.
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
if (bufSize < this.boundaryLength + 1) {
- throw new IllegalArgumentException(
- "The buffer size specified for the MultipartStream is too small");
+ throw new IllegalArgumentException("The buffer size specified for the MultipartStream is too small");
}
-
this.input = input;
this.bufSize = Math.max(bufSize, boundaryLength * 2);
this.buffer = new byte[this.bufSize];
- this.notifier = pNotifier;
-
+ this.notifier = notifier;
this.boundary = new byte[this.boundaryLength];
this.boundaryTable = new int[this.boundaryLength + 1];
this.keepRegion = this.boundary.length;
-
- System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
- BOUNDARY_PREFIX.length);
- System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
- boundary.length);
+ System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, BOUNDARY_PREFIX.length);
+ System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length);
computeBoundaryTable();
-
head = 0;
tail = 0;
}
/**
- * <p> Constructs a {@code MultipartStream} with a default size buffer.
+ * Constructs a {@code MultipartStream} with a default size buffer.
*
* @param input The {@code InputStream} to serve as a data source.
* @param boundary The token used for dividing the stream into
* {@code encapsulations}.
- * @param pNotifier An object for calling the progress listener, if any.
+ * @param notifier An object for calling the progress listener, if any.
*
*
* @see #MultipartStream(InputStream, byte[], int, ProgressNotifier)
*/
- public MultipartStream(final InputStream input,
- final byte[] boundary,
- final ProgressNotifier pNotifier) {
- this(input, boundary, DEFAULT_BUFSIZE, pNotifier);
+ public MultipartStream(final InputStream input, final byte[] boundary, final ProgressNotifier notifier) {
+ this(input, boundary, DEFAULT_BUFSIZE, notifier);
}
- // --------------------------------------------------------- Public methods
-
/**
- * Retrieves the character encoding used when reading the headers of an
- * individual part. When not specified, or {@code null}, the platform
- * default encoding is used.
- *
- * @return The encoding used to read part headers.
+ * Compute the table used for Knuth-Morris-Pratt search algorithm.
*/
- public String getHeaderEncoding() {
- return headerEncoding;
- }
+ private void computeBoundaryTable() {
+ int position = 2;
+ int candidate = 0;
+
+ boundaryTable[0] = -1;
+ boundaryTable[1] = 0;
+
+ while (position <= boundaryLength) {
+ if (boundary[position - 1] == boundary[candidate]) {
+ boundaryTable[position] = candidate + 1;
+ candidate++;
+ position++;
+ } else if (candidate > 0) {
+ candidate = boundaryTable[candidate];
+ } else {
+ boundaryTable[position] = 0;
+ position++;
+ }
+ }
+ }
/**
- * Specifies the character encoding to be used when reading the headers of
- * individual parts. When not specified, or {@code null}, the platform
- * default encoding is used.
+ * Reads {@code body-data} from the current {@code encapsulation} and discards it.
+ * <p>
+ * Use this method to skip encapsulations you don't need or don't understand.
*
- * @param encoding The encoding used to read part headers.
+ * @return The amount of data discarded.
+ *
+ * @throws MalformedStreamException if the stream ends unexpectedly.
+ * @throws IOException if an i/o error occurs.
*/
- public void setHeaderEncoding(final String encoding) {
- headerEncoding = encoding;
+ public int discardBodyData() throws MalformedStreamException, IOException {
+ return readBodyData(null);
}
/**
- * Reads a byte from the {@code buffer}, and refills it as
- * necessary.
- *
- * @return The next byte from the input stream.
+ * Searches for the {@code boundary} in the {@code buffer}
+ * region delimited by {@code head} and {@code tail}.
*
- * @throws IOException if there is no more data available.
+ * @return The position of the boundary found, counting from the
+ * beginning of the {@code buffer}, or {@code -1} if
+ * not found.
*/
- public byte readByte() throws IOException {
- // Buffer depleted ?
- if (head == tail) {
- head = 0;
- // Refill.
- tail = input.read(buffer, head, bufSize);
- if (tail == -1) {
- // No more data available.
- throw new IOException("No more data is available");
+ protected int findSeparator() {
+
+ int bufferPos = head;
+ int tablePos = 0;
+
+ while (bufferPos < tail) {
+ while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
+ tablePos = boundaryTable[tablePos];
}
- if (notifier != null) {
- notifier.noteBytesRead(tail);
+ bufferPos++;
+ tablePos++;
+ if (tablePos == boundaryLength) {
+ return bufferPos - boundaryLength;
}
}
- return buffer[head++];
+ return -1;
+ }
+
+ /**
+ * Gets the character encoding used when reading the headers of an
+ * individual part. When not specified, or {@code null}, the platform
+ * default encoding is used.
+ *
+ * @return The encoding used to read part headers.
+ */
+ public String getHeaderEncoding() {
+ return headerEncoding;
+ }
+
+ /**
+ * Creates a new {@link ItemInputStream}.
+ * @return A new instance of {@link ItemInputStream}.
+ */
+ public ItemInputStream newInputStream() {
+ return new ItemInputStream();
+ }
+
+ /**
+ * Reads {@code body-data} from the current {@code encapsulation} and writes its contents into the output
+ * {@code Stream}.
+ * <p>
+ * Arbitrary large amounts of data can be processed by this method using a constant size buffer. (see {@link
+ * #MultipartStream(InputStream,byte[],int, MultipartStream.ProgressNotifier) constructor}).
+ *
+ * @param output The {@code Stream} to write data into. May be null, in which case this method is equivalent to
+ * {@link #discardBodyData()}.
+ *
+ * @return the amount of data written.
+ *
+ * @throws MalformedStreamException if the stream ends unexpectedly.
+ * @throws IOException if an i/o error occurs.
+ */
+ public int readBodyData(final OutputStream output)
+ throws MalformedStreamException, IOException {
+ return (int) Streams.copy(newInputStream(), output, false); // Streams.copy closes the input stream
}
/**
@@ -434,69 +801,37 @@ public class MultipartStream {
}
/**
- * <p>Changes the boundary token used for partitioning the stream.
- *
- * <p>This method allows single pass processing of nested multipart
- * streams.
- *
- * <p>The boundary token of the nested stream is {@code required}
- * to be of the same length as the boundary token in parent stream.
- *
- * <p>Restoring the parent stream boundary token after processing of a
- * nested stream is left to the application.
+ * Reads a byte from the {@code buffer}, and refills it as
+ * necessary.
*
- * @param boundary The boundary to be used for parsing of the nested
- * stream.
+ * @return The next byte from the input stream.
*
- * @throws IllegalBoundaryException if the {@code boundary}
- * has a different length than the one
- * being currently parsed.
- */
- public void setBoundary(final byte[] boundary)
- throws IllegalBoundaryException {
- if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) {
- throw new IllegalBoundaryException(
- "The length of a boundary token cannot be changed");
- }
- System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
- boundary.length);
- computeBoundaryTable();
- }
-
- /**
- * Compute the table used for Knuth-Morris-Pratt search algorithm.
+ * @throws IOException if there is no more data available.
*/
- private void computeBoundaryTable() {
- int position = 2;
- int candidate = 0;
-
- boundaryTable[0] = -1;
- boundaryTable[1] = 0;
-
- while (position <= boundaryLength) {
- if (boundary[position - 1] == boundary[candidate]) {
- boundaryTable[position] = candidate + 1;
- candidate++;
- position++;
- } else if (candidate > 0) {
- candidate = boundaryTable[candidate];
- } else {
- boundaryTable[position] = 0;
- position++;
+ public byte readByte() throws IOException {
+ // Buffer depleted ?
+ if (head == tail) {
+ head = 0;
+ // Refill.
+ tail = input.read(buffer, head, bufSize);
+ if (tail == -1) {
+ // No more data available.
+ throw new IOException("No more data is available");
+ }
+ if (notifier != null) {
+ notifier.noteBytesRead(tail);
}
}
+ return buffer[head++];
}
/**
- * <p>Reads the {@code header-part} of the current
- * {@code encapsulation}.
- *
- * <p>Headers are returned verbatim to the input stream, including the
- * trailing {@code CRLF} marker. Parsing is left to the
- * application.
- *
- * <p><strong>TODO</strong> allow limiting maximum header size to
- * protect against abuse.
+ * Reads the {@code header-part} of the current {@code encapsulation}.
+ * <p>
+ * Headers are returned verbatim to the input stream, including the trailing {@code CRLF} marker. Parsing is left to
+ * the application.
+ * <p>
+ * <strong>TODO</strong> allow limiting maximum header size to protect against abuse.
*
* @return The {@code header-part} of the current encapsulation.
*
@@ -518,7 +853,8 @@ public class MultipartStream {
} catch (final IOException e) {
throw new MalformedStreamException("Stream ended unexpectedly");
}
- if (++size > HEADER_PART_SIZE_MAX) {
+ size++;
+ if (size > HEADER_PART_SIZE_MAX) {
throw new MalformedStreamException(String.format(
"Header section has more than %s bytes (maybe it is not properly terminated)",
Integer.valueOf(HEADER_PART_SIZE_MAX)));
@@ -530,7 +866,6 @@ public class MultipartStream {
}
baos.write(b);
}
-
String headers;
if (headerEncoding != null) {
try {
@@ -543,56 +878,41 @@ public class MultipartStream {
} else {
headers = baos.toString();
}
-
return headers;
}
/**
- * <p>Reads {@code body-data} from the current
- * {@code encapsulation} and writes its contents into the
- * output {@code Stream}.
+ * Changes the boundary token used for partitioning the stream.
+ * <p>
+ * This method allows single pass processing of nested multipart streams.
+ * <p>
+ * The boundary token of the nested stream is {@code required} to be of the same length as the boundary token in
+ * parent stream.
+ * <p>
+ * Restoring the parent stream boundary token after processing of a nested stream is left to the application.
*
- * <p>Arbitrary large amounts of data can be processed by this
- * method using a constant size buffer. (see {@link
- * #MultipartStream(InputStream,byte[],int,
- * MultipartStream.ProgressNotifier) constructor}).
+ * @param boundary The boundary to be used for parsing of the nested stream.
*
- * @param output The {@code Stream} to write data into. May
- * be null, in which case this method is equivalent
- * to {@link #discardBodyData()}.
- *
- * @return the amount of data written.
- *
- * @throws MalformedStreamException if the stream ends unexpectedly.
- * @throws IOException if an i/o error occurs.
- */
- public int readBodyData(final OutputStream output)
- throws MalformedStreamException, IOException {
- return (int) Streams.copy(newInputStream(), output, false); // N.B. Streams.copy closes the input stream
- }
-
- /**
- * Creates a new {@link ItemInputStream}.
- * @return A new instance of {@link ItemInputStream}.
+ * @throws IllegalBoundaryException if the {@code boundary} has a different length than the one being currently
+ * parsed.
*/
- public ItemInputStream newInputStream() {
- return new ItemInputStream();
+ public void setBoundary(final byte[] boundary) throws IllegalBoundaryException {
+ if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) {
+ throw new IllegalBoundaryException("The length of a boundary token cannot be changed");
+ }
+ System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length);
+ computeBoundaryTable();
}
/**
- * <p> Reads {@code body-data} from the current
- * {@code encapsulation} and discards it.
- *
- * <p>Use this method to skip encapsulations you don't need or don't
- * understand.
- *
- * @return The amount of data discarded.
+ * Specifies the character encoding to be used when reading the headers of
+ * individual parts. When not specified, or {@code null}, the platform
+ * default encoding is used.
*
- * @throws MalformedStreamException if the stream ends unexpectedly.
- * @throws IOException if an i/o error occurs.
+ * @param encoding The encoding used to read part headers.
*/
- public int discardBodyData() throws MalformedStreamException, IOException {
- return readBodyData(null);
+ public void setHeaderEncoding(final String encoding) {
+ headerEncoding = encoding;
}
/**
@@ -627,361 +947,4 @@ public class MultipartStream {
}
}
- /**
- * Compares {@code count} first bytes in the arrays
- * {@code a} and {@code b}.
- *
- * @param a The first array to compare.
- * @param b The second array to compare.
- * @param count How many bytes should be compared.
- *
- * @return {@code true} if {@code count} first bytes in arrays
- * {@code a} and {@code b} are equal.
- */
- public static boolean arrayequals(final byte[] a,
- final byte[] b,
- final int count) {
- for (int i = 0; i < count; i++) {
- if (a[i] != b[i]) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Searches for the {@code boundary} in the {@code buffer}
- * region delimited by {@code head} and {@code tail}.
- *
- * @return The position of the boundary found, counting from the
- * beginning of the {@code buffer}, or {@code -1} if
- * not found.
- */
- protected int findSeparator() {
-
- int bufferPos = this.head;
- int tablePos = 0;
-
- while (bufferPos < this.tail) {
- while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
- tablePos = boundaryTable[tablePos];
- }
- bufferPos++;
- tablePos++;
- if (tablePos == boundaryLength) {
- return bufferPos - boundaryLength;
- }
- }
- return -1;
- }
-
- /**
- * Thrown to indicate that the input stream fails to follow the
- * required syntax.
- */
- public static class MalformedStreamException extends IOException {
-
- /**
- * The UID to use when serializing this instance.
- */
- private static final long serialVersionUID = 6466926458059796677L;
-
- /**
- * Constructs a {@code MalformedStreamException} with no
- * detail message.
- */
- public MalformedStreamException() {
- }
-
- /**
- * Constructs an {@code MalformedStreamException} with
- * the specified detail message.
- *
- * @param message The detail message.
- */
- public MalformedStreamException(final String message) {
- super(message);
- }
-
- }
-
- /**
- * Thrown upon attempt of setting an invalid boundary token.
- */
- public static class IllegalBoundaryException extends IOException {
-
- /**
- * The UID to use when serializing this instance.
- */
- private static final long serialVersionUID = -161533165102632918L;
-
- /**
- * Constructs an {@code IllegalBoundaryException} with no
- * detail message.
- */
- public IllegalBoundaryException() {
- }
-
- /**
- * Constructs an {@code IllegalBoundaryException} with
- * the specified detail message.
- *
- * @param message The detail message.
- */
- public IllegalBoundaryException(final String message) {
- super(message);
- }
-
- }
-
- /**
- * An {@link InputStream} for reading an items contents.
- */
- public class ItemInputStream extends InputStream implements Closeable {
-
- /**
- * The number of bytes, which have been read so far.
- */
- private long total;
-
- /**
- * The number of bytes, which must be hold, because
- * they might be a part of the boundary.
- */
- private int pad;
-
- /**
- * The current offset in the buffer.
- */
- private int pos;
-
- /**
- * Whether the stream is already closed.
- */
- private boolean closed;
-
- /**
- * Creates a new instance.
- */
- ItemInputStream() {
- findSeparator();
- }
-
- /**
- * Called for finding the separator.
- */
- private void findSeparator() {
- pos = MultipartStream.this.findSeparator();
- if (pos == -1) {
- if (tail - head > keepRegion) {
- pad = keepRegion;
- } else {
- pad = tail - head;
- }
- }
- }
-
- /**
- * Returns the number of bytes, which have been read
- * by the stream.
- *
- * @return Number of bytes, which have been read so far.
- */
- public long getBytesRead() {
- return total;
- }
-
- /**
- * Returns the number of bytes, which are currently
- * available, without blocking.
- *
- * @throws IOException An I/O error occurs.
- * @return Number of bytes in the buffer.
- */
- @Override
- public int available() throws IOException {
- if (pos == -1) {
- return tail - head - pad;
- }
- return pos - head;
- }
-
- /**
- * Offset when converting negative bytes to integers.
- */
- private static final int BYTE_POSITIVE_OFFSET = 256;
-
- /**
- * Returns the next byte in the stream.
- *
- * @return The next byte in the stream, as a non-negative
- * integer, or -1 for EOF.
- * @throws IOException An I/O error occurred.
- */
- @Override
- public int read() throws IOException {
- if (closed) {
- throw new FileItemStream.ItemSkippedException();
- }
- if (available() == 0 && makeAvailable() == 0) {
- return -1;
- }
- ++total;
- final int b = buffer[head++];
- if (b >= 0) {
- return b;
- }
- return b + BYTE_POSITIVE_OFFSET;
- }
-
- /**
- * Reads bytes into the given buffer.
- *
- * @param b The destination buffer, where to write to.
- * @param off Offset of the first byte in the buffer.
- * @param len Maximum number of bytes to read.
- * @return Number of bytes, which have been actually read,
- * or -1 for EOF.
- * @throws IOException An I/O error occurred.
- */
- @Override
- public int read(final byte[] b, final int off, final int len) throws IOException {
- if (closed) {
- throw new FileItemStream.ItemSkippedException();
- }
- if (len == 0) {
- return 0;
- }
- int res = available();
- if (res == 0) {
- res = makeAvailable();
- if (res == 0) {
- return -1;
- }
- }
- res = Math.min(res, len);
- System.arraycopy(buffer, head, b, off, res);
- head += res;
- total += res;
- return res;
- }
-
- /**
- * Closes the input stream.
- *
- * @throws IOException An I/O error occurred.
- */
- @Override
- public void close() throws IOException {
- close(false);
- }
-
- /**
- * Closes the input stream.
- *
- * @param pCloseUnderlying Whether to close the underlying stream
- * (hard close)
- * @throws IOException An I/O error occurred.
- */
- public void close(final boolean pCloseUnderlying) throws IOException {
- if (closed) {
- return;
- }
- if (pCloseUnderlying) {
- closed = true;
- input.close();
- } else {
- for (;;) {
- int av = available();
- if (av == 0) {
- av = makeAvailable();
- if (av == 0) {
- break;
- }
- }
- skip(av);
- }
- }
- closed = true;
- }
-
- /**
- * Skips the given number of bytes.
- *
- * @param bytes Number of bytes to skip.
- * @return The number of bytes, which have actually been
- * skipped.
- * @throws IOException An I/O error occurred.
- */
- @Override
- public long skip(final long bytes) throws IOException {
- if (closed) {
- throw new FileItemStream.ItemSkippedException();
- }
- int av = available();
- if (av == 0) {
- av = makeAvailable();
- if (av == 0) {
- return 0;
- }
- }
- final long res = Math.min(av, bytes);
- head += res;
- return res;
- }
-
- /**
- * Attempts to read more data.
- *
- * @return Number of available bytes
- * @throws IOException An I/O error occurred.
- */
- private int makeAvailable() throws IOException {
- if (pos != -1) {
- return 0;
- }
-
- // Move the data to the beginning of the buffer.
- total += tail - head - pad;
- System.arraycopy(buffer, tail - pad, buffer, 0, pad);
-
- // Refill buffer with new data.
- head = 0;
- tail = pad;
-
- for (;;) {
- final int bytesRead = input.read(buffer, tail, bufSize - tail);
- if (bytesRead == -1) {
- // The last pad amount is left in the buffer.
- // Boundary can't be in there so signal an error
- // condition.
- final String msg = "Stream ended unexpectedly";
- throw new MalformedStreamException(msg);
- }
- if (notifier != null) {
- notifier.noteBytesRead(bytesRead);
- }
- tail += bytesRead;
-
- findSeparator();
- final int av = available();
-
- if (av > 0 || pos != -1) {
- return av;
- }
- }
- }
-
- /**
- * Returns, whether the stream is closed.
- *
- * @return True, if the stream is closed, otherwise false.
- */
- @Override
- public boolean isClosed() {
- return closed;
- }
-
- }
-
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
index 91a97aeae0..4404d9f719 100644
--- a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
+++ b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
@@ -40,32 +40,32 @@ public class ParameterParser {
/**
* String to be parsed.
*/
- private char[] chars = null;
+ private char[] chars;
/**
* Current position in the string.
*/
- private int pos = 0;
+ private int pos;
/**
* Maximum position in the string.
*/
- private int len = 0;
+ private int len;
/**
* Start of a token.
*/
- private int i1 = 0;
+ private int i1;
/**
* End of a token.
*/
- private int i2 = 0;
+ private int i2;
/**
* Whether names stored in the map should be converted to lower case.
*/
- private boolean lowerCaseNames = false;
+ private boolean lowerCaseNames;
/**
* Default ParameterParser constructor.
@@ -73,16 +73,6 @@ public class ParameterParser {
public ParameterParser() {
}
- /**
- * Are there any characters left to parse?
- *
- * @return {@code true} if there are unparsed characters,
- * {@code false} otherwise.
- */
- private boolean hasChar() {
- return this.pos < this.len;
- }
-
/**
* A helper method to process the parsed token. This method removes
* leading and trailing blanks as well as enclosing quotation marks,
@@ -94,18 +84,18 @@ public class ParameterParser {
*/
private String getToken(final boolean quoted) {
// Trim leading white spaces
- while ((i1 < i2) && (Character.isWhitespace(chars[i1]))) {
+ while (i1 < i2 && Character.isWhitespace(chars[i1])) {
i1++;
}
// Trim trailing white spaces
- while ((i2 > i1) && (Character.isWhitespace(chars[i2 - 1]))) {
+ while (i2 > i1 && Character.isWhitespace(chars[i2 - 1])) {
i2--;
}
// Strip away quotation marks if necessary
if (quoted
- && ((i2 - i1) >= 2)
- && (chars[i1] == '"')
- && (chars[i2 - 1] == '"')) {
+ && i2 - i1 >= 2
+ && chars[i1] == '"'
+ && chars[i2 - 1] == '"') {
i1++;
i2--;
}
@@ -116,6 +106,28 @@ public class ParameterParser {
return result;
}
+ /**
+ * Are there any characters left to parse?
+ *
+ * @return {@code true} if there are unparsed characters,
+ * {@code false} otherwise.
+ */
+ private boolean hasChar() {
+ return pos < len;
+ }
+
+ /**
+ * Returns {@code true} if parameter names are to be converted to lower
+ * case when name/value pairs are parsed.
+ *
+ * @return {@code true} if parameter names are to be
+ * converted to lower case when name/value pairs are parsed.
+ * Otherwise returns {@code false}
+ */
+ public boolean isLowerCaseNames() {
+ return lowerCaseNames;
+ }
+
/**
* Tests if the given character is present in the array of characters.
*
@@ -137,83 +149,83 @@ public class ParameterParser {
}
/**
- * Parses out a token until any of the given terminators
- * is encountered.
+ * Extracts a map of name/value pairs from the given array of
+ * characters. Names are expected to be unique.
*
- * @param terminators the array of terminating characters. Any of these
- * characters when encountered signify the end of the token
+ * @param charArray the array of characters that contains a sequence of
+ * name/value pairs
+ * @param separator the name/value pairs separator
*
- * @return the token
+ * @return a map of name/value pairs
*/
- private String parseToken(final char[] terminators) {
- char ch;
- i1 = pos;
- i2 = pos;
- while (hasChar()) {
- ch = chars[pos];
- if (isOneOf(ch, terminators)) {
- break;
- }
- i2++;
- pos++;
+ public Map<String, String> parse(final char[] charArray, final char separator) {
+ if (charArray == null) {
+ return new HashMap<>();
}
- return getToken(false);
+ return parse(charArray, 0, charArray.length, separator);
}
/**
- * Parses out a token until any of the given terminators
- * is encountered outside the quotation marks.
+ * Extracts a map of name/value pairs from the given array of characters. Names are expected to be unique.
*
- * @param terminators the array of terminating characters. Any of these
- * characters when encountered outside the quotation marks signify the end
- * of the token
+ * @param charArray the array of characters that contains a sequence of name/value pairs
+ * @param offset the initial offset.
+ * @param length the length.
+ * @param separator the name/value pairs separator
*
- * @return the token
+ * @return a map of name/value pairs
*/
- private String parseQuotedToken(final char[] terminators) {
- char ch;
- i1 = pos;
- i2 = pos;
- boolean quoted = false;
- boolean charEscaped = false;
+ public Map<String, String> parse(final char[] charArray, final int offset, final int length, final char separator) {
+ if (charArray == null) {
+ return new HashMap<>();
+ }
+ final HashMap<String, String> params = new HashMap<>();
+ chars = charArray.clone();
+ pos = offset;
+ len = length;
while (hasChar()) {
- ch = chars[pos];
- if (!quoted && isOneOf(ch, terminators)) {
- break;
+ String paramName = parseToken(new char[] { '=', separator });
+ String paramValue = null;
+ if (hasChar() && charArray[pos] == '=') {
+ pos++; // skip '='
+ paramValue = parseQuotedToken(new char[] { separator });
+ if (paramValue != null) {
+ try {
+ paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)
+ : MimeUtility.decodeText(paramValue);
+ } catch (final UnsupportedEncodingException e) {
+ // let's keep the original value in this case
+ }
+ }
}
- if (!charEscaped && ch == '"') {
- quoted = !quoted;
+ if (hasChar() && charArray[pos] == separator) {
+ pos++; // skip separator
+ }
+ if (paramName != null && !paramName.isEmpty()) {
+ paramName = RFC2231Utility.stripDelimiter(paramName);
+ if (lowerCaseNames) {
+ paramName = paramName.toLowerCase(Locale.ROOT);
+ }
+ params.put(paramName, paramValue);
}
- charEscaped = (!charEscaped && ch == '\\');
- i2++;
- pos++;
-
}
- return getToken(true);
+ return params;
}
/**
- * Returns {@code true} if parameter names are to be converted to lower
- * case when name/value pairs are parsed.
+ * Extracts a map of name/value pairs from the given string. Names are
+ * expected to be unique.
*
- * @return {@code true} if parameter names are to be
- * converted to lower case when name/value pairs are parsed.
- * Otherwise returns {@code false}
- */
- public boolean isLowerCaseNames() {
- return this.lowerCaseNames;
- }
-
- /**
- * Sets the flag if parameter names are to be converted to lower case when
- * name/value pairs are parsed.
+ * @param str the string that contains a sequence of name/value pairs
+ * @param separator the name/value pairs separator
*
- * @param b {@code true} if parameter names are to be
- * converted to lower case when name/value pairs are parsed.
- * {@code false} otherwise.
+ * @return a map of name/value pairs
*/
- public void setLowerCaseNames(final boolean b) {
- this.lowerCaseNames = b;
+ public Map<String, String> parse(final String str, final char separator) {
+ if (str == null) {
+ return new HashMap<>();
+ }
+ return parse(str.toCharArray(), separator);
}
/**
@@ -245,96 +257,71 @@ public class ParameterParser {
}
/**
- * Extracts a map of name/value pairs from the given string. Names are
- * expected to be unique.
+ * Parses out a token until any of the given terminators
+ * is encountered outside the quotation marks.
*
- * @param str the string that contains a sequence of name/value pairs
- * @param separator the name/value pairs separator
+ * @param terminators the array of terminating characters. Any of these
+ * characters when encountered outside the quotation marks signify the end
+ * of the token
*
- * @return a map of name/value pairs
+ * @return the token
*/
- public Map<String, String> parse(final String str, final char separator) {
- if (str == null) {
- return new HashMap<>();
+ private String parseQuotedToken(final char[] terminators) {
+ char ch;
+ i1 = pos;
+ i2 = pos;
+ boolean quoted = false;
+ boolean charEscaped = false;
+ while (hasChar()) {
+ ch = chars[pos];
+ if (!quoted && isOneOf(ch, terminators)) {
+ break;
+ }
+ if (!charEscaped && ch == '"') {
+ quoted = !quoted;
+ }
+ charEscaped = !charEscaped && ch == '\\';
+ i2++;
+ pos++;
+
}
- return parse(str.toCharArray(), separator);
+ return getToken(true);
}
/**
- * Extracts a map of name/value pairs from the given array of
- * characters. Names are expected to be unique.
+ * Parses out a token until any of the given terminators
+ * is encountered.
*
- * @param charArray the array of characters that contains a sequence of
- * name/value pairs
- * @param separator the name/value pairs separator
+ * @param terminators the array of terminating characters. Any of these
+ * characters when encountered signify the end of the token
*
- * @return a map of name/value pairs
+ * @return the token
*/
- public Map<String, String> parse(final char[] charArray, final char separator) {
- if (charArray == null) {
- return new HashMap<>();
+ private String parseToken(final char[] terminators) {
+ char ch;
+ i1 = pos;
+ i2 = pos;
+ while (hasChar()) {
+ ch = chars[pos];
+ if (isOneOf(ch, terminators)) {
+ break;
+ }
+ i2++;
+ pos++;
}
- return parse(charArray, 0, charArray.length, separator);
+ return getToken(false);
}
/**
- * Extracts a map of name/value pairs from the given array of
- * characters. Names are expected to be unique.
- *
- * @param charArray the array of characters that contains a sequence of
- * name/value pairs
- * @param offset - the initial offset.
- * @param length - the length.
- * @param separator the name/value pairs separator
+ * Sets the flag if parameter names are to be converted to lower case when
+ * name/value pairs are parsed.
*
- * @return a map of name/value pairs
+ * @param b {@code true} if parameter names are to be
+ * converted to lower case when name/value pairs are parsed.
+ * {@code false} otherwise.
*/
- public Map<String, String> parse(
- final char[] charArray,
- final int offset,
- final int length,
- final char separator) {
-
- if (charArray == null) {
- return new HashMap<>();
- }
- final HashMap<String, String> params = new HashMap<>();
- this.chars = charArray.clone();
- this.pos = offset;
- this.len = length;
-
- String paramName;
- String paramValue;
- while (hasChar()) {
- paramName = parseToken(new char[] {
- '=', separator });
- paramValue = null;
- if (hasChar() && (charArray[pos] == '=')) {
- pos++; // skip '='
- paramValue = parseQuotedToken(new char[] {
- separator });
-
- if (paramValue != null) {
- try {
- paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)
- : MimeUtility.decodeText(paramValue);
- } catch (final UnsupportedEncodingException e) {
- // let's keep the original value in this case
- }
- }
- }
- if (hasChar() && (charArray[pos] == separator)) {
- pos++; // skip separator
- }
- if ((paramName != null) && !paramName.isEmpty()) {
- paramName = RFC2231Utility.stripDelimiter(paramName);
- if (this.lowerCaseNames) {
- paramName = paramName.toLowerCase(Locale.ENGLISH);
- }
- params.put(paramName, paramValue);
- }
- }
- return params;
+ public void setLowerCaseNames(final boolean b) {
+ lowerCaseNames = b;
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
index 931c99d1d8..c7f49c9095 100644
--- a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
+++ b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
@@ -25,13 +25,13 @@ public interface ProgressListener {
/**
* Updates the listeners status information.
*
- * @param pBytesRead The total number of bytes, which have been read
+ * @param bytesRead The total number of bytes, which have been read
* so far.
- * @param pContentLength The total number of bytes, which are being
+ * @param contentLength The total number of bytes, which are being
* read. May be -1, if this number is unknown.
- * @param pItems The number of the field, which is currently being
+ * @param items The number of the field, which is currently being
* read. (0 = no item so far, 1 = first item is being read, ...)
*/
- void update(long pBytesRead, long pContentLength, int pItems);
+ void update(long bytesRead, long contentLength, int items);
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
index e35c1ea237..22fe3c18ab 100644
--- a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
+++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
@@ -37,259 +37,175 @@ import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.FileItemHeaders;
import org.apache.tomcat.util.http.fileupload.FileUploadException;
import org.apache.tomcat.util.http.fileupload.IOUtils;
+import org.apache.tomcat.util.http.fileupload.InvalidFileNameException;
import org.apache.tomcat.util.http.fileupload.ParameterParser;
import org.apache.tomcat.util.http.fileupload.util.Streams;
/**
- * <p> The default implementation of the
- * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface.
- *
- * <p> After retrieving an instance of this class from a {@link
- * org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} instance (see
+ * The default implementation of the {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface.
+ * <p>
+ * After retrieving an instance of this class from a {@link DiskFileItemFactory} instance (see
* {@link org.apache.tomcat.util.http.fileupload.FileUpload
- * #parseRequest(org.apache.tomcat.util.http.fileupload.RequestContext)}), you
- * may either request all contents of file at once using {@link #get()} or
- * request an {@link java.io.InputStream InputStream} with
- * {@link #getInputStream()} and process the file without attempting to load
- * it into memory, which may come handy with large files.
- *
- * <p>Temporary files, which are created for file items, will be deleted when
- * the associated request is recycled.</p>
+ * #parseRequest(org.apache.tomcat.util.http.fileupload.RequestContext)}), you may either request all contents of file
+ * at once using {@link #get()} or request an {@link java.io.InputStream InputStream} with {@link #getInputStream()} and
+ * process the file without attempting to load it into memory, which may come handy with large files.
+ * <p>
+ * Temporary files, which are created for file items, will be deleted when the associated request is recycled.
+ * </p>
*
* @since FileUpload 1.1
*/
-public class DiskFileItem
- implements FileItem {
+public class DiskFileItem implements FileItem {
- // ----------------------------------------------------- Manifest constants
+ /**
+ * Counter used in unique identifier generation.
+ */
+ private static final AtomicInteger COUNTER = new AtomicInteger(0);
/**
- * Default content charset to be used when no explicit charset
- * parameter is provided by the sender. Media subtypes of the
- * "text" type are defined to have a default charset value of
- * "ISO-8859-1" when received via HTTP.
+ * Default content charset to be used when no explicit charset parameter is provided by the sender. Media subtypes
+ * of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP.
*/
public static final String DEFAULT_CHARSET = "ISO-8859-1";
- // ----------------------------------------------------------- Data members
-
/**
* UID used in unique file name generation.
*/
- private static final String UID =
- UUID.randomUUID().toString().replace('-', '_');
+ private static final String UID = UUID.randomUUID().toString().replace('-', '_');
/**
- * Counter used in unique identifier generation.
+ * Returns an identifier that is unique within the class loader used to load this class, but does not have
+ * random-like appearance.
+ *
+ * @return A String with the non-random looking instance identifier.
*/
- private static final AtomicInteger COUNTER = new AtomicInteger(0);
+ private static String getUniqueId() {
+ final int limit = 100000000;
+ final int current = COUNTER.getAndIncrement();
+ String id = Integer.toString(current);
+
+ // If you manage to get more than 100 million of ids, you'll
+ // start getting ids longer than 8 characters.
+ if (current < limit) {
+ id = ("00000000" + id).substring(id.length());
+ }
+ return id;
+ }
/**
- * The name of the form field as provided by the browser.
+ * Cached contents of the file.
*/
- private String fieldName;
+ private byte[] cachedContent;
/**
- * The content type passed by the browser, or {@code null} if
- * not defined.
+ * The content type passed by the browser, or {@code null} if not defined.
*/
private final String contentType;
/**
- * Whether or not this item is a simple form field.
+ * Default content charset to be used when no explicit charset parameter is provided by the sender.
*/
- private boolean isFormField;
+ private String defaultCharset = DEFAULT_CHARSET;
/**
- * The original file name in the user's file system.
+ * Output stream for this item.
*/
- private final String fileName;
+ private transient DeferredFileOutputStream dfos;
/**
- * The size of the item, in bytes. This is used to cache the size when a
- * file item is moved from its original location.
+ * The name of the form field as provided by the browser.
*/
- private long size = -1;
-
+ private String fieldName;
/**
- * The threshold above which uploads will be stored on disk.
+ * The original file name in the user's file system.
*/
- private final int sizeThreshold;
+ private final String fileName;
/**
- * The directory in which uploaded files will be stored, if stored on disk.
+ * The file items headers.
*/
- private final File repository;
+ private FileItemHeaders headers;
/**
- * Cached contents of the file.
+ * Whether or not this item is a simple form field.
*/
- private byte[] cachedContent;
+ private boolean formField;
/**
- * Output stream for this item.
+ * The directory in which uploaded files will be stored, if stored on disk.
*/
- private transient DeferredFileOutputStream dfos;
+ private final File repository;
/**
- * The temporary file to use.
+ * The size of the item, in bytes. This is used to cache the size when a file item is moved from its original
+ * location.
*/
- private transient File tempFile;
+ private long size = -1;
/**
- * The file items headers.
+ * The threshold above which uploads will be stored on disk.
*/
- private FileItemHeaders headers;
+ private final int sizeThreshold;
/**
- * Default content charset to be used when no explicit charset
- * parameter is provided by the sender.
+ * The temporary file to use.
*/
- private String defaultCharset = DEFAULT_CHARSET;
-
- // ----------------------------------------------------------- Constructors
+ private transient File tempFile;
/**
* Constructs a new {@code DiskFileItem} instance.
*
* @param fieldName The name of the form field.
- * @param contentType The content type passed by the browser or
- * {@code null} if not specified.
- * @param isFormField Whether or not this item is a plain form field, as
- * opposed to a file upload.
- * @param fileName The original file name in the user's file system, or
- * {@code null} if not specified.
- * @param sizeThreshold The threshold, in bytes, below which items will be
- * retained in memory and above which they will be
- * stored as a file.
- * @param repository The data repository, which is the directory in
- * which files will be created, should the item size
- * exceed the threshold.
- */
- public DiskFileItem(final String fieldName,
- final String contentType, final boolean isFormField, final String fileName,
- final int sizeThreshold, final File repository) {
+ * @param contentType The content type passed by the browser or {@code null} if not specified.
+ * @param isFormField Whether or not this item is a plain form field, as opposed to a file upload.
+ * @param fileName The original file name in the user's file system, or {@code null} if not specified.
+ * @param sizeThreshold The threshold, in bytes, below which items will be retained in memory and above which they
+ * will be stored as a file.
+ * @param repository The data repository, which is the directory in which files will be created, should the item
+ * size exceed the threshold.
+ */
+ public DiskFileItem(final String fieldName, final String contentType, final boolean isFormField,
+ final String fileName, final int sizeThreshold, final File repository) {
this.fieldName = fieldName;
this.contentType = contentType;
- this.isFormField = isFormField;
+ this.formField = isFormField;
this.fileName = fileName;
this.sizeThreshold = sizeThreshold;
this.repository = repository;
}
- // ------------------------------- Methods from javax.activation.DataSource
-
- /**
- * Returns an {@link java.io.InputStream InputStream} that can be
- * used to retrieve the contents of the file.
- *
- * @return An {@link java.io.InputStream InputStream} that can be
- * used to retrieve the contents of the file.
- *
- * @throws IOException if an error occurs.
- */
- @Override
- public InputStream getInputStream()
- throws IOException {
- if (!isInMemory()) {
- return Files.newInputStream(dfos.getFile().toPath());
- }
-
- if (cachedContent == null) {
- cachedContent = dfos.getData();
- }
- return new ByteArrayInputStream(cachedContent);
- }
-
- /**
- * Returns the content type passed by the agent or {@code null} if
- * not defined.
- *
- * @return The content type passed by the agent or {@code null} if
- * not defined.
- */
- @Override
- public String getContentType() {
- return contentType;
- }
-
- /**
- * Returns the content charset passed by the agent or {@code null} if
- * not defined.
- *
- * @return The content charset passed by the agent or {@code null} if
- * not defined.
- */
- public String getCharSet() {
- final ParameterParser parser = new ParameterParser();
- parser.setLowerCaseNames(true);
- // Parameter parser can handle null input
- final Map<String, String> params = parser.parse(getContentType(), ';');
- return params.get("charset");
- }
-
- /**
- * Returns the original file name in the client's file system.
- *
- * @return The original file name in the client's file system.
- * @throws org.apache.tomcat.util.http.fileupload.InvalidFileNameException
- * The file name contains a NUL character, which might be an indicator of
- * a security attack. If you intend to use the file name anyways, catch
- * the exception and use {@link
- * org.apache.tomcat.util.http.fileupload.InvalidFileNameException#getName()}.
- */
- @Override
- public String getName() {
- return Streams.checkFileName(fileName);
- }
-
- // ------------------------------------------------------- FileItem methods
-
/**
- * Provides a hint as to whether or not the file contents will be read
- * from memory.
- *
- * @return {@code true} if the file contents will be read
- * from memory; {@code false} otherwise.
+ * Clears the cache.
*/
- @Override
- public boolean isInMemory() {
- if (cachedContent != null) {
- return true;
- }
- return dfos.isInMemory();
+ private void clear() {
+ cachedContent = null; // NOPMD
}
/**
- * Returns the size of the file.
- *
- * @return The size of the file, in bytes.
+ * Deletes the underlying storage for a file item, including deleting any associated temporary disk file. This
+ * method can be used to ensure that this is done at an earlier time, thus preserving system resources.
*/
@Override
- public long getSize() {
- if (size >= 0) {
- return size;
- }
- if (cachedContent != null) {
- return cachedContent.length;
- }
- if (dfos.isInMemory()) {
- return dfos.getData().length;
+ public void delete() {
+ clear();
+ final File outputFile = getStoreLocation();
+ if (outputFile != null && !isInMemory() && outputFile.exists()) {
+ if (!outputFile.delete()) {
+ final String desc = "Cannot delete " + outputFile.toString();
+ throw new UncheckedIOException(desc, new IOException(desc));
+ }
}
- return dfos.getFile().length();
}
/**
- * Returns the contents of the file as an array of bytes. If the
- * contents of the file were not yet cached in memory, they will be
- * loaded from the disk storage and cached.
+ * Gets the contents of the file as an array of bytes. If the contents of the file were not yet cached in memory,
+ * they will be loaded from the disk storage and cached.
*
- * @return The contents of the file as an array of bytes
- * or {@code null} if the data cannot be read
+ * @return The contents of the file as an array of bytes or {@code null} if the data cannot be read.
*
* @throws UncheckedIOException if an I/O error occurs
- * @throws ArithmeticException if the file {@code size} overflows an int
+ * @throws ArithmeticException if the file {@code size} overflows an int
*/
@Override
public byte[] get() throws UncheckedIOException {
@@ -311,132 +227,43 @@ public class DiskFileItem
}
/**
- * Returns the contents of the file as a String, using the specified
- * encoding. This method uses {@link #get()} to retrieve the
- * contents of the file.
- *
- * @param charset The charset to use.
+ * Gets the content charset passed by the agent or {@code null} if not defined.
*
- * @return The contents of the file, as a string.
- *
- * @throws UnsupportedEncodingException if the requested character
- * encoding is not available.
+ * @return The content charset passed by the agent or {@code null} if not defined.
*/
- @Override
- public String getString(final String charset)
- throws UnsupportedEncodingException, IOException {
- return new String(get(), charset);
+ public String getCharSet() {
+ final ParameterParser parser = new ParameterParser();
+ parser.setLowerCaseNames(true);
+ // Parameter parser can handle null input
+ final Map<String,String> params = parser.parse(getContentType(), ';');
+ return params.get("charset");
}
/**
- * Returns the contents of the file as a String, using the default
- * character encoding. This method uses {@link #get()} to retrieve the
- * contents of the file.
- *
- * <b>TODO</b> Consider making this method throw UnsupportedEncodingException.
+ * Gets the content type passed by the agent or {@code null} if not defined.
*
- * @return The contents of the file, as a string.
+ * @return The content type passed by the agent or {@code null} if not defined.
*/
@Override
- public String getString() {
- try {
- final byte[] rawData = get();
- String charset = getCharSet();
- if (charset == null) {
- charset = defaultCharset;
- }
- return new String(rawData, charset);
- } catch (final IOException e) {
- return "";
- }
+ public String getContentType() {
+ return contentType;
}
/**
- * A convenience method to write an uploaded item to disk. The client code
- * is not concerned with whether or not the item is stored in memory, or on
- * disk in a temporary location. They just want to write the uploaded item
- * to a file.
- * <p>
- * This implementation first attempts to rename the uploaded item to the
- * specified destination file, if the item was originally written to disk.
- * Otherwise, the data will be copied to the specified file.
- * <p>
- * This method is only guaranteed to work <em>once</em>, the first time it
- * is invoked for a particular item. This is because, in the event that the
- * method renames a temporary file, that file will no longer be available
- * to copy or rename again at a later time.
- *
- * @param file The {@code File} into which the uploaded item should
- * be stored.
+ * Gets the default charset for use when no explicit charset parameter is provided by the sender.
*
- * @throws Exception if an error occurs.
- */
- @Override
- public void write(final File file) throws Exception {
- if (isInMemory()) {
- try (OutputStream fout = Files.newOutputStream(file.toPath())) {
- fout.write(get());
- }
- } else {
- final File outputFile = getStoreLocation();
- if (outputFile == null) {
- /*
- * For whatever reason we cannot write the
- * file to disk.
- */
- throw new FileUploadException(
- "Cannot write uploaded file to disk!");
- }
- // Save the length of the file
- size = outputFile.length();
- /*
- * The uploaded file is being stored on disk
- * in a temporary location so move it to the
- * desired file.
- */
- if (file.exists() && !file.delete()) {
- throw new FileUploadException("Cannot write uploaded file to disk!");
- }
- if (!outputFile.renameTo(file)) {
- BufferedInputStream in = null;
- BufferedOutputStream out = null;
- try {
- in = new BufferedInputStream(new FileInputStream(outputFile));
- out = new BufferedOutputStream(new FileOutputStream(file));
- IOUtils.copy(in, out);
- out.close();
- } finally {
- IOUtils.closeQuietly(in);
- IOUtils.closeQuietly(out);
- }
- }
- }
- }
-
- /**
- * Deletes the underlying storage for a file item, including deleting any associated temporary disk file.
- * This method can be used to ensure that this is done at an earlier time, thus preserving system resources.
+ * @return the default charset
*/
- @Override
- public void delete() {
- cachedContent = null;
- final File outputFile = getStoreLocation();
- if (outputFile != null && !isInMemory() && outputFile.exists()) {
- if (!outputFile.delete()) {
- final String desc = "Cannot delete " + outputFile.toString();
- throw new UncheckedIOException(desc, new IOException(desc));
- }
- }
+ public String getDefaultCharset() {
+ return defaultCharset;
}
/**
- * Returns the name of the field in the multipart form corresponding to
- * this file item.
+ * Gets the name of the field in the multipart form corresponding to this file item.
*
* @return The name of the form field.
*
* @see #setFieldName(String)
- *
*/
@Override
public String getFieldName() {
@@ -444,55 +271,52 @@ public class DiskFileItem
}
/**
- * Sets the field name used to reference this file item.
- *
- * @param fieldName The name of the form field.
- *
- * @see #getFieldName()
+ * Gets the file item headers.
*
+ * @return The file items headers.
*/
@Override
- public void setFieldName(final String fieldName) {
- this.fieldName = fieldName;
+ public FileItemHeaders getHeaders() {
+ return headers;
}
/**
- * Determines whether or not a {@code FileItem} instance represents
- * a simple form field.
- *
- * @return {@code true} if the instance represents a simple form
- * field; {@code false} if it represents an uploaded file.
+ * Gets an {@link java.io.InputStream InputStream} that can be used to retrieve the contents of the file.
*
- * @see #setFormField(boolean)
+ * @return An {@link java.io.InputStream InputStream} that can be used to retrieve the contents of the file.
*
+ * @throws IOException if an error occurs.
*/
@Override
- public boolean isFormField() {
- return isFormField;
+ public InputStream getInputStream() throws IOException {
+ if (!isInMemory()) {
+ return Files.newInputStream(dfos.getFile().toPath());
+ }
+ if (cachedContent == null) {
+ cachedContent = dfos.getData();
+ }
+ return new ByteArrayInputStream(cachedContent);
}
/**
- * Specifies whether or not a {@code FileItem} instance represents
- * a simple form field.
- *
- * @param state {@code true} if the instance represents a simple form
- * field; {@code false} if it represents an uploaded file.
+ * Gets the original file name in the client's file system.
*
- * @see #isFormField()
+ * @return The original file name in the client's file system.
*
+ * @throws InvalidFileNameException The file name contains a NUL character, which might be an indicator of a
+ * security attack. If you intend to use the file name anyways, catch the
+ * exception and use
+ * {@link org.apache.tomcat.util.http.fileupload.InvalidFileNameException#getName()}.
*/
@Override
- public void setFormField(final boolean state) {
- isFormField = state;
+ public String getName() {
+ return Streams.checkFileName(fileName);
}
/**
- * Returns an {@link java.io.OutputStream OutputStream} that can
- * be used for storing the contents of the file.
- *
- * @return An {@link java.io.OutputStream OutputStream} that can be used
- * for storing the contents of the file.
+ * Gets an {@link java.io.OutputStream OutputStream} that can be used for storing the contents of the file.
*
+ * @return An {@link java.io.OutputStream OutputStream} that can be used for storing the contents of the file.
*/
@Override
public OutputStream getOutputStream() {
@@ -503,20 +327,32 @@ public class DiskFileItem
return dfos;
}
- // --------------------------------------------------------- Public methods
+ /**
+ * Gets the size of the file.
+ *
+ * @return The size of the file, in bytes.
+ */
+ @Override
+ public long getSize() {
+ if (size >= 0) {
+ return size;
+ }
+ if (cachedContent != null) {
+ return cachedContent.length;
+ }
+ if (dfos.isInMemory()) {
+ return dfos.getData().length;
+ }
+ return dfos.getFile().length();
+ }
/**
- * Returns the {@link java.io.File} object for the {@code FileItem}'s
- * data's temporary location on the disk. Note that for
- * {@code FileItem}s that have their data stored in memory,
- * this method will return {@code null}. When handling large
- * files, you can use {@link java.io.File#renameTo(java.io.File)} to
- * move the file to new location without copying the data, if the
- * source and destination locations reside within the same logical
- * volume.
+ * Gets the {@link java.io.File} object for the {@code FileItem}'s data's temporary location on the disk. Note
+ * that for {@code FileItem}s that have their data stored in memory, this method will return {@code null}. When
+ * handling large files, you can use {@link java.io.File#renameTo(java.io.File)} to move the file to new location
+ * without copying the data, if the source and destination locations reside within the same logical volume.
*
- * @return The data file, or {@code null} if the data is stored in
- * memory.
+ * @return The data file, or {@code null} if the data is stored in memory.
*/
public File getStoreLocation() {
if (dfos == null) {
@@ -528,16 +364,49 @@ public class DiskFileItem
return dfos.getFile();
}
- // ------------------------------------------------------ Protected methods
+ /**
+ * Gets the contents of the file as a String, using the default character encoding. This method uses
+ * {@link #get()} to retrieve the contents of the file.
+ * <p>
+ * <strong>TODO</strong> Consider making this method throw UnsupportedEncodingException.
+ *
+ * @return The contents of the file, as a string.
+ */
+ @Override
+ public String getString() {
+ final byte[] rawData = get();
+ String charset = getCharSet();
+ if (charset == null) {
+ charset = defaultCharset;
+ }
+ try {
+ return new String(rawData, charset);
+ } catch (final UnsupportedEncodingException e) {
+ return "";
+ }
+ }
/**
- * Creates and returns a {@link java.io.File File} representing a uniquely
- * named temporary file in the configured repository path. The lifetime of
- * the file is tied to the lifetime of the {@code FileItem} instance;
- * the file will be deleted when the instance is garbage collected.
+ * Gets the contents of the file as a String, using the specified encoding. This method uses {@link #get()} to
+ * retrieve the contents of the file.
+ *
+ * @param charset The charset to use.
+ *
+ * @return The contents of the file, as a string.
+ *
+ * @throws UnsupportedEncodingException if the requested character encoding is not available.
+ */
+ @Override
+ public String getString(final String charset) throws UnsupportedEncodingException {
+ return new String(get(), charset);
+ }
+
+ /**
+ * Creates and returns a {@link java.io.File File} representing a uniquely named temporary file in the configured
+ * repository path. The lifetime of the file is tied to the lifetime of the {@code FileItem} instance; the file will
+ * be deleted when the instance is garbage collected.
* <p>
- * <b>Note: Subclasses that override this method must ensure that they return the
- * same File each time.</b>
+ * <b>Note: Subclasses that override this method must ensure that they return the same File each time.</b>
*
* @return The {@link java.io.File File} to be used for temporary storage.
*/
@@ -555,71 +424,138 @@ public class DiskFileItem
return tempFile;
}
- // -------------------------------------------------------- Private methods
-
/**
- * Returns an identifier that is unique within the class loader used to
- * load this class, but does not have random-like appearance.
+ * Tests whether or not a {@code FileItem} instance represents a simple form field.
*
- * @return A String with the non-random looking instance identifier.
+ * @return {@code true} if the instance represents a simple form field; {@code false} if it represents an uploaded
+ * file.
+ *
+ * @see #setFormField(boolean)
*/
- private static String getUniqueId() {
- final int limit = 100000000;
- final int current = COUNTER.getAndIncrement();
- String id = Integer.toString(current);
+ @Override
+ public boolean isFormField() {
+ return formField;
+ }
- // If you manage to get more than 100 million of ids, you'll
- // start getting ids longer than 8 characters.
- if (current < limit) {
- id = ("00000000" + id).substring(id.length());
+ /**
+ * Provides a hint as to whether or not the file contents will be read from memory.
+ *
+ * @return {@code true} if the file contents will be read from memory; {@code false} otherwise.
+ */
+ @Override
+ public boolean isInMemory() {
+ if (cachedContent != null) {
+ return true;
}
- return id;
+ return dfos.isInMemory();
}
/**
- * Returns a string representation of this object.
+ * Sets the default charset for use when no explicit charset parameter is provided by the sender.
*
- * @return a string representation of this object.
+ * @param charset the default charset
+ */
+ public void setDefaultCharset(final String charset) {
+ defaultCharset = charset;
+ }
+
+ /**
+ * Sets the field name used to reference this file item.
+ *
+ * @param fieldName The name of the form field.
+ *
+ * @see #getFieldName()
*/
@Override
- public String toString() {
- return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s",
- getName(), getStoreLocation(), Long.valueOf(getSize()), Boolean.valueOf(isFormField()), getFieldName());
+ public void setFieldName(final String fieldName) {
+ this.fieldName = fieldName;
}
/**
- * Returns the file item headers.
- * @return The file items headers.
+ * Sets whether or not a {@code FileItem} instance represents a simple form field.
+ *
+ * @param formField {@code true} if the instance represents a simple form field; {@code false} if it represents an
+ * uploaded file.
+ *
+ * @see #isFormField()
*/
@Override
- public FileItemHeaders getHeaders() {
- return headers;
+ public void setFormField(final boolean formField) {
+ this.formField = formField;
}
/**
* Sets the file item headers.
- * @param pHeaders The file items headers.
+ *
+ * @param headers The file items headers.
*/
@Override
- public void setHeaders(final FileItemHeaders pHeaders) {
- headers = pHeaders;
+ public void setHeaders(final FileItemHeaders headers) {
+ this.headers = headers;
}
/**
- * Returns the default charset for use when no explicit charset
- * parameter is provided by the sender.
- * @return the default charset
+ * Returns a string representation of this object.
+ *
+ * @return a string representation of this object.
*/
- public String getDefaultCharset() {
- return defaultCharset;
+ @Override
+ public String toString() {
+ return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", getName(),
+ getStoreLocation(), Long.valueOf(getSize()), Boolean.valueOf(isFormField()), getFieldName());
}
/**
- * Sets the default charset for use when no explicit charset
- * parameter is provided by the sender.
- * @param charset the default charset
+ * A convenience method to write an uploaded item to disk. The client code is not concerned with whether or not the
+ * item is stored in memory, or on disk in a temporary location. They just want to write the uploaded item to a
+ * file.
+ * <p>
+ * This implementation first attempts to rename the uploaded item to the specified destination file, if the item was
+ * originally written to disk. Otherwise, the data will be copied to the specified file.
+ * <p>
+ * This method is only guaranteed to work <em>once</em>, the first time it is invoked for a particular item. This is
+ * because, in the event that the method renames a temporary file, that file will no longer be available to copy or
+ * rename again at a later time.
+ *
+ * @param file The {@code File} into which the uploaded item should be stored.
+ *
+ * @throws Exception if an error occurs.
*/
- public void setDefaultCharset(final String charset) {
- defaultCharset = charset;
+ @Override
+ public void write(final File file) throws Exception {
+ if (isInMemory()) {
+ try (OutputStream fout = Files.newOutputStream(file.toPath())) {
+ fout.write(get());
+ }
+ } else {
+ final File outputFile = getStoreLocation();
+ if (outputFile == null) {
+ /*
+ * For whatever reason we cannot write the file to disk.
+ */
+ throw new FileUploadException("Cannot write uploaded file to disk!");
+ }
+ // Save the length of the file
+ size = outputFile.length();
+ /*
+ * The uploaded file is being stored on disk in a temporary location so move it to the desired file.
+ */
+ if (file.exists() && !file.delete()) {
+ throw new FileUploadException("Cannot write uploaded file to disk!");
+ }
+ if (!outputFile.renameTo(file)) {
+ BufferedInputStream in = null;
+ BufferedOutputStream out = null;
+ try {
+ in = new BufferedInputStream(new FileInputStream(outputFile));
+ out = new BufferedOutputStream(new FileOutputStream(file));
+ IOUtils.copy(in, out);
+ out.close();
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(out);
+ }
+ }
+ }
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
index 8b0073da3f..06817f599b 100644
--- a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
+++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
@@ -39,7 +39,7 @@ import org.apache.tomcat.util.http.fileupload.FileItemFactory;
* {@code System.getProperty("java.io.tmpdir")}.</li>
* </ul>
* <p>
- * <b>NOTE</b>: Files are created in the system default temp directory with
+ * <strong>NOTE</strong>: Files are created in the system default temp directory with
* predictable names. This means that a local attacker with write access to that
* directory can perform a TOUTOC attack to replace any uploaded file with a
* file of the attackers choice. The implications of this will depend on how the
@@ -58,15 +58,11 @@ import org.apache.tomcat.util.http.fileupload.FileItemFactory;
*/
public class DiskFileItemFactory implements FileItemFactory {
- // ----------------------------------------------------- Manifest constants
-
/**
* The default threshold above which uploads will be stored on disk.
*/
public static final int DEFAULT_SIZE_THRESHOLD = 10240;
- // ----------------------------------------------------- Instance Variables
-
/**
* The directory in which uploaded files will be stored, if stored on disk.
*/
@@ -83,8 +79,6 @@ public class DiskFileItemFactory implements FileItemFactory {
*/
private String defaultCharset = DiskFileItem.DEFAULT_CHARSET;
- // ----------------------------------------------------------- Constructors
-
/**
* Constructs an unconfigured instance of this class. The resulting factory
* may be configured by calling the appropriate setter methods.
@@ -108,36 +102,53 @@ public class DiskFileItemFactory implements FileItemFactory {
this.repository = repository;
}
- // ------------------------------------------------------------- Properties
-
/**
- * Returns the directory used to temporarily store files that are larger
- * than the configured size threshold.
- *
- * @return The directory in which temporary files will be located.
+ * Create a new {@link DiskFileItem}
+ * instance from the supplied parameters and the local factory
+ * configuration.
*
- * @see #setRepository(java.io.File)
+ * @param fieldName The name of the form field.
+ * @param contentType The content type of the form field.
+ * @param isFormField {@code true} if this is a plain form field;
+ * {@code false} otherwise.
+ * @param fileName The name of the uploaded file, if any, as supplied
+ * by the browser or other client.
*
+ * @return The newly created file item.
*/
- public File getRepository() {
- return repository;
+ @Override
+ public FileItem createItem(final String fieldName, final String contentType,
+ final boolean isFormField, final String fileName) {
+ final DiskFileItem result = new DiskFileItem(fieldName, contentType,
+ isFormField, fileName, sizeThreshold, repository);
+ result.setDefaultCharset(defaultCharset);
+ return result;
}
/**
- * Sets the directory used to temporarily store files that are larger
+ * Gets the default charset for use when no explicit charset
+ * parameter is provided by the sender.
+ * @return the default charset
+ */
+ public String getDefaultCharset() {
+ return defaultCharset;
+ }
+
+ /**
+ * Gets the directory used to temporarily store files that are larger
* than the configured size threshold.
*
- * @param repository The directory in which temporary files will be located.
+ * @return The directory in which temporary files will be located.
*
- * @see #getRepository()
+ * @see #setRepository(java.io.File)
*
*/
- public void setRepository(final File repository) {
- this.repository = repository;
+ public File getRepository() {
+ return repository;
}
/**
- * Returns the size threshold beyond which files are written directly to
+ * Gets the size threshold beyond which files are written directly to
* disk. The default value is 10240 bytes.
*
* @return The size threshold, in bytes.
@@ -149,57 +160,37 @@ public class DiskFileItemFactory implements FileItemFactory {
}
/**
- * Sets the size threshold beyond which files are written directly to disk.
- *
- * @param sizeThreshold The size threshold, in bytes.
- *
- * @see #getSizeThreshold()
+ * Sets the default charset for use when no explicit charset
+ * parameter is provided by the sender.
*
+ * @param charset the default charset
*/
- public void setSizeThreshold(final int sizeThreshold) {
- this.sizeThreshold = sizeThreshold;
+ public void setDefaultCharset(final String charset) {
+ this.defaultCharset = charset;
}
- // --------------------------------------------------------- Public Methods
-
/**
- * Create a new {@link DiskFileItem}
- * instance from the supplied parameters and the local factory
- * configuration.
+ * Sets the directory used to temporarily store files that are larger
+ * than the configured size threshold.
*
- * @param fieldName The name of the form field.
- * @param contentType The content type of the form field.
- * @param isFormField {@code true} if this is a plain form field;
- * {@code false} otherwise.
- * @param fileName The name of the uploaded file, if any, as supplied
- * by the browser or other client.
+ * @param repository The directory in which temporary files will be located.
+ *
+ * @see #getRepository()
*
- * @return The newly created file item.
- */
- @Override
- public FileItem createItem(final String fieldName, final String contentType,
- final boolean isFormField, final String fileName) {
- final DiskFileItem result = new DiskFileItem(fieldName, contentType,
- isFormField, fileName, sizeThreshold, repository);
- result.setDefaultCharset(defaultCharset);
- return result;
- }
-
- /**
- * Returns the default charset for use when no explicit charset
- * parameter is provided by the sender.
- * @return the default charset
*/
- public String getDefaultCharset() {
- return defaultCharset;
+ public void setRepository(final File repository) {
+ this.repository = repository;
}
/**
- * Sets the default charset for use when no explicit charset
- * parameter is provided by the sender.
- * @param pCharset the default charset
+ * Sets the size threshold beyond which files are written directly to disk.
+ *
+ * @param sizeThreshold The size threshold, in bytes.
+ *
+ * @see #getSizeThreshold()
+ *
*/
- public void setDefaultCharset(final String pCharset) {
- defaultCharset = pCharset;
+ public void setSizeThreshold(final int sizeThreshold) {
+ this.sizeThreshold = sizeThreshold;
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/package-info.java b/java/org/apache/tomcat/util/http/fileupload/package-info.java
index dee2bb19fc..91c25bc5f0 100644
--- a/java/org/apache/tomcat/util/http/fileupload/package-info.java
+++ b/java/org/apache/tomcat/util/http/fileupload/package-info.java
@@ -16,8 +16,8 @@
*/
/**
- * <p><b>NOTE:</b> This code has been copied from commons-fileupload trunk
- * 1.3 and commons-io 1.4 and package renamed to avoid clashes with
+ * <p><b>NOTE:</b> This code has been copied from commons-fileupload
+ * 1.x and commons-io 1.4 and package renamed to avoid clashes with
* any web apps that may wish to use these libraries.
* </p>
* <p>
diff --git a/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
index 90f05d1f78..eb6c62a3a1 100644
--- a/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
+++ b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
@@ -32,15 +32,11 @@ import org.apache.tomcat.util.http.fileupload.UploadContext;
*/
public class ServletRequestContext implements UploadContext {
- // ----------------------------------------------------- Instance Variables
-
/**
* The request for which the context is being provided.
*/
private final HttpServletRequest request;
- // ----------------------------------------------------------- Constructors
-
/**
* Construct a context for this request.
*
@@ -50,7 +46,22 @@ public class ServletRequestContext implements UploadContext {
this.request = request;
}
- // --------------------------------------------------------- Public Methods
+ /**
+ * Retrieve the content length of the request.
+ *
+ * @return The content length of the request.
+ * @since FileUpload 1.3
+ */
+ @Override
+ public long contentLength() {
+ long size;
+ try {
+ size = Long.parseLong(request.getHeader(FileUploadBase.CONTENT_LENGTH));
+ } catch (final NumberFormatException e) {
+ size = request.getContentLength();
+ }
+ return size;
+ }
/**
* Retrieve the character encoding for the request.
@@ -72,23 +83,6 @@ public class ServletRequestContext implements UploadContext {
return request.getContentType();
}
- /**
- * Retrieve the content length of the request.
- *
- * @return The content length of the request.
- * @since FileUpload 1.3
- */
- @Override
- public long contentLength() {
- long size;
- try {
- size = Long.parseLong(request.getHeader(FileUploadBase.CONTENT_LENGTH));
- } catch (final NumberFormatException e) {
- size = request.getContentLength();
- }
- return size;
- }
-
/**
* Retrieve the input stream for the request.
*
@@ -109,7 +103,7 @@ public class ServletRequestContext implements UploadContext {
@Override
public String toString() {
return String.format("ContentLength=%s, ContentType=%s",
- Long.valueOf(this.contentLength()), this.getContentType());
+ Long.valueOf(contentLength()), getContentType());
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
index a70345ff43..d4e20ece7f 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
@@ -45,9 +45,29 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
*/
private final Map<String, List<String>> headerNameToValueListMap = new LinkedHashMap<>();
+ /**
+ * Constructs a new instance.
+ */
+ public FileItemHeadersImpl() {
+ // empty
+ }
+
+ /**
+ * Method to add header values to this instance.
+ *
+ * @param name name of this header
+ * @param value value of this header
+ */
+ public synchronized void addHeader(final String name, final String value) {
+ final String nameLower = name.toLowerCase(Locale.ROOT);
+ final List<String> headerValueList = headerNameToValueListMap.
+ computeIfAbsent(nameLower, k -> new ArrayList<>());
+ headerValueList.add(value);
+ }
+
@Override
public String getHeader(final String name) {
- final String nameLower = name.toLowerCase(Locale.ENGLISH);
+ final String nameLower = name.toLowerCase(Locale.ROOT);
final List<String> headerValueList = headerNameToValueListMap.get(nameLower);
if (null == headerValueList) {
return null;
@@ -62,7 +82,7 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
@Override
public Iterator<String> getHeaders(final String name) {
- final String nameLower = name.toLowerCase(Locale.ENGLISH);
+ final String nameLower = name.toLowerCase(Locale.ROOT);
List<String> headerValueList = headerNameToValueListMap.get(nameLower);
if (null == headerValueList) {
headerValueList = Collections.emptyList();
@@ -70,17 +90,4 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
return headerValueList.iterator();
}
- /**
- * Method to add header values to this instance.
- *
- * @param name name of this header
- * @param value value of this header
- */
- public synchronized void addHeader(final String name, final String value) {
- final String nameLower = name.toLowerCase(Locale.ENGLISH);
- final List<String> headerValueList = headerNameToValueListMap.
- computeIfAbsent(nameLower, k -> new ArrayList<>());
- headerValueList.add(value);
- }
-
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
index def25ca125..add304c16a 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
@@ -45,26 +45,14 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
* Creates a new instance.
*
* @param inputStream The input stream, which shall be limited.
- * @param pSizeMax The limit; no more than this number of bytes
+ * @param sizeMax The limit; no more than this number of bytes
* shall be returned by the source stream.
*/
- public LimitedInputStream(final InputStream inputStream, final long pSizeMax) {
+ public LimitedInputStream(final InputStream inputStream, final long sizeMax) {
super(inputStream);
- sizeMax = pSizeMax;
+ this.sizeMax = sizeMax;
}
- /**
- * Called to indicate, that the input streams limit has
- * been exceeded.
- *
- * @param pSizeMax The input streams limit, in bytes.
- * @param pCount The actual number of bytes.
- * @throws IOException The called method is expected
- * to raise an IOException.
- */
- protected abstract void raiseError(long pSizeMax, long pCount)
- throws IOException;
-
/**
* Called to check, whether the input streams
* limit is reached.
@@ -77,6 +65,43 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
}
}
+ /**
+ * Closes this input stream and releases any system resources
+ * associated with the stream.
+ * This
+ * method simply performs {@code in.close()}.
+ *
+ * @throws IOException if an I/O error occurs.
+ * @see java.io.FilterInputStream#in
+ */
+ @Override
+ public void close() throws IOException {
+ closed = true;
+ super.close();
+ }
+
+ /**
+ * Returns, whether this stream is already closed.
+ *
+ * @return True, if the stream is closed, otherwise false.
+ * @throws IOException An I/O error occurred.
+ */
+ @Override
+ public boolean isClosed() throws IOException {
+ return closed;
+ }
+
+ /**
+ * Called to indicate, that the input streams limit has
+ * been exceeded.
+ *
+ * @param sizeMax The input streams limit, in bytes.
+ * @param count The actual number of bytes.
+ * @throws IOException The called method is expected
+ * to raise an IOException.
+ */
+ protected abstract void raiseError(long sizeMax, long count) throws IOException;
+
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an {@code int} in the range
@@ -137,30 +162,4 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
return res;
}
- /**
- * Returns, whether this stream is already closed.
- *
- * @return True, if the stream is closed, otherwise false.
- * @throws IOException An I/O error occurred.
- */
- @Override
- public boolean isClosed() throws IOException {
- return closed;
- }
-
- /**
- * Closes this input stream and releases any system resources
- * associated with the stream.
- * This
- * method simply performs {@code in.close()}.
- *
- * @throws IOException if an I/O error occurs.
- * @see java.io.FilterInputStream#in
- */
- @Override
- public void close() throws IOException {
- closed = true;
- super.close();
- }
-
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/Streams.java b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
index 7289d06665..ecf82c25d0 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
@@ -27,14 +27,6 @@ import org.apache.tomcat.util.http.fileupload.InvalidFileNameException;
*/
public final class Streams {
- /**
- * Private constructor, to prevent instantiation.
- * This class has only static methods.
- */
- private Streams() {
- // Does nothing
- }
-
/**
* Default buffer size for use in
* {@link #copy(InputStream, OutputStream, boolean)}.
@@ -42,10 +34,41 @@ public final class Streams {
public static final int DEFAULT_BUFFER_SIZE = 8192;
/**
+ * Checks, whether the given file name is valid in the sense,
+ * that it doesn't contain any NUL characters. If the file name
+ * is valid, it will be returned without any modifications. Otherwise,
+ * an {@link InvalidFileNameException} is raised.
+ *
+ * @param fileName The file name to check
+ * @return Unmodified file name, if valid.
+ * @throws InvalidFileNameException The file name was found to be invalid.
+ */
+ public static String checkFileName(final String fileName) {
+ if (fileName != null && fileName.indexOf('\u0000') != -1) {
+ // fileName.replace("\u0000", "\\0")
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < fileName.length(); i++) {
+ final char c = fileName.charAt(i);
+ switch (c) {
+ case 0:
+ sb.append("\\0");
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ throw new InvalidFileNameException(fileName,
+ "Invalid file name: " + sb);
+ }
+ return fileName;
+ }
+
+ /**
* Copies the contents of the given {@link InputStream}
* to the given {@link OutputStream}. Shortcut for
* <pre>
- * copy(pInputStream, pOutputStream, new byte[8192]);
+ * copy(pInputStream, outputStream, new byte[8192]);
* </pre>
*
* @param inputStream The input stream, which is being read.
@@ -116,35 +139,12 @@ public final class Streams {
}
}
- /**
- * Checks, whether the given file name is valid in the sense,
- * that it doesn't contain any NUL characters. If the file name
- * is valid, it will be returned without any modifications. Otherwise,
- * an {@link InvalidFileNameException} is raised.
- *
- * @param fileName The file name to check
- * @return Unmodified file name, if valid.
- * @throws InvalidFileNameException The file name was found to be invalid.
+ /**
+ * Private constructor, to prevent instantiation.
+ * This class has only static methods.
*/
- public static String checkFileName(final String fileName) {
- if (fileName != null && fileName.indexOf('\u0000') != -1) {
- // pFileName.replace("\u0000", "\\0")
- final StringBuilder sb = new StringBuilder();
- for (int i = 0; i < fileName.length(); i++) {
- final char c = fileName.charAt(i);
- switch (c) {
- case 0:
- sb.append("\\0");
- break;
- default:
- sb.append(c);
- break;
- }
- }
- throw new InvalidFileNameException(fileName,
- "Invalid file name: " + sb);
- }
- return fileName;
+ private Streams() {
+ // Does nothing
}
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
index 7debaf915e..461b00b354 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
@@ -75,13 +75,6 @@ public final class MimeUtility {
MIME2JAVA.put("x-us-ascii", "ISO-8859-1");
}
- /**
- * Hidden constructor, this class must not be instantiated.
- */
- private MimeUtility() {
- // do nothing
- }
-
/**
* Decode a string of text obtained from a mail header into
* its proper form. The text generally will consist of a
@@ -207,7 +200,7 @@ public final class MimeUtility {
}
// pull out the character set information (this is the MIME name at this point).
- final String charset = word.substring(2, charsetPos).toLowerCase(Locale.ENGLISH);
+ final String charset = word.substring(2, charsetPos).toLowerCase(Locale.ROOT);
// now pull out the encoding token the same way.
final int encodingPos = word.indexOf('?', charsetPos + 1);
@@ -266,7 +259,7 @@ public final class MimeUtility {
return null;
}
- final String mappedCharset = MIME2JAVA.get(charset.toLowerCase(Locale.ENGLISH));
+ final String mappedCharset = MIME2JAVA.get(charset.toLowerCase(Locale.ROOT));
// if there is no mapping, then the original name is used. Many of the MIME character set
// names map directly back into Java. The reverse isn't necessarily true.
if (mappedCharset == null) {
@@ -275,4 +268,11 @@ public final class MimeUtility {
return mappedCharset;
}
+ /**
+ * Hidden constructor, this class must not be instantiated.
+ */
+ private MimeUtility() {
+ // do nothing
+ }
+
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
index 72e9896fe6..a49fe432a7 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
@@ -17,6 +17,8 @@
package org.apache.tomcat.util.http.fileupload.util.mime;
/**
+ * Thrown for a parsing problem.
+ *
* @since FileUpload 1.3
*/
final class ParseException extends Exception {
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
index 6a94f3c202..d1caac1a57 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
@@ -25,17 +25,35 @@ import java.io.OutputStream;
final class QuotedPrintableDecoder {
/**
- * The shift value required to create the upper nibble
- * from the first of 2 byte values converted from ascii hex.
+ * Carriage return character '{@value}'.
*/
- private static final int UPPER_NIBBLE_SHIFT = Byte.SIZE / 2;
+ private static final char CR = '\r';
/**
- * Hidden constructor, this class must not be instantiated.
+ * Equal character '{@value}'.
*/
- private QuotedPrintableDecoder() {
- // do nothing
- }
+ private static final char EQUAL = '=';
+
+ /**
+ * Line feed character '{@value}'.
+ */
+ private static final char LF = '\n';
+
+ /**
+ * Space character '{@value}'.
+ */
+ private static final char SP = ' ';
+
+ /**
+ * Underscore character '{@value}'.
+ */
+ private static final char UNDERSCORE = '_';
+
+ /**
+ * The shift value required to create the upper nibble
+ * from the first of 2 byte values converted from ASCII hex.
+ */
+ private static final int UPPER_NIBBLE_SHIFT = Byte.SIZE / 2;
/**
* Decode the encoded byte data writing it to the given output stream.
@@ -56,9 +74,9 @@ final class QuotedPrintableDecoder {
final byte ch = data[off++];
// space characters were translated to '_' on encode, so we need to translate them back.
- if (ch == '_') {
- out.write(' ');
- } else if (ch == '=') {
+ if (ch == UNDERSCORE) {
+ out.write(SP);
+ } else if (ch == EQUAL) {
// we found an encoded character. Reduce the 3 char sequence to one.
// but first, make sure we have two characters to work with.
if (off + 1 >= endOffset) {
@@ -69,8 +87,8 @@ final class QuotedPrintableDecoder {
final byte b2 = data[off++];
// we've found an encoded carriage return. The next char needs to be a newline
- if (b1 == '\r') {
- if (b2 != '\n') {
+ if (b1 == CR) {
+ if (b2 != LF) {
throw new IOException("Invalid quoted printable encoding; CR must be followed by LF");
}
// this was a soft linebreak inserted by the encoding. We just toss this away
@@ -79,7 +97,7 @@ final class QuotedPrintableDecoder {
// this is a hex pair we need to convert back to a single byte.
final int c1 = hexToBinary(b1);
final int c2 = hexToBinary(b2);
- out.write((c1 << UPPER_NIBBLE_SHIFT) | c2);
+ out.write(c1 << UPPER_NIBBLE_SHIFT | c2);
// 3 bytes in, one byte out
bytesWritten++;
}
@@ -96,7 +114,7 @@ final class QuotedPrintableDecoder {
/**
* Convert a hex digit to the binary value it represents.
*
- * @param b the ascii hex byte to convert (0-0, A-F, a-f)
+ * @param b the ASCII hex byte to convert (0-0, A-F, a-f)
* @return the int value of the hex byte, 0-15
* @throws IOException if the byte is not a valid hex digit.
*/
@@ -109,4 +127,11 @@ final class QuotedPrintableDecoder {
return i;
}
+ /**
+ * Hidden constructor, this class must not be instantiated.
+ */
+ private QuotedPrintableDecoder() {
+ // do nothing
+ }
+
}
diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
index 8a522a313f..c1a7ba0d0c 100644
--- a/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
+++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
@@ -30,6 +30,12 @@ import java.io.UnsupportedEncodingException;
* @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
*/
public final class RFC2231Utility {
+
+ /**
+ * Percent character '{@value}'.
+ */
+ private static final char PERCENT = '%';
+
/**
* The Hexadecimal values char array.
*/
@@ -55,54 +61,19 @@ public final class RFC2231Utility {
}
}
- /**
- * Private constructor so that no instances can be created. This class
- * contains only static utility methods.
- */
- private RFC2231Utility() {
- }
-
- /**
- * Checks if Asterisk (*) at the end of parameter name to indicate,
- * if it has charset and language information to decode the value.
- * @param paramName The parameter, which is being checked.
- * @return {@code true}, if encoded as per RFC 2231, {@code false} otherwise
- */
- public static boolean hasEncodedValue(final String paramName) {
- if (paramName != null) {
- return paramName.lastIndexOf('*') == (paramName.length() - 1);
- }
- return false;
- }
-
- /**
- * If {@code paramName} has Asterisk (*) at the end, it will be stripped off,
- * else the passed value will be returned.
- * @param paramName The parameter, which is being inspected.
- * @return stripped {@code paramName} of Asterisk (*), if RFC2231 encoded
- */
- public static String stripDelimiter(final String paramName) {
- if (hasEncodedValue(paramName)) {
- final StringBuilder paramBuilder = new StringBuilder(paramName);
- paramBuilder.deleteCharAt(paramName.lastIndexOf('*'));
- return paramBuilder.toString();
- }
- return paramName;
- }
-
/**
* Decode a string of text obtained from a HTTP header as per RFC 2231
* <p>
- * <b>Eg 1.</b> {@code us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A}
+ * <strong>Eg 1.</strong> {@code us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A}
* will be decoded to {@code This is ***fun***}
* <p>
- * <b>Eg 2.</b> {@code iso-8859-1'en'%A3%20rate}
+ * <strong>Eg 2.</strong> {@code iso-8859-1'en'%A3%20rate}
* will be decoded to {@code £ rate}
* <p>
- * <b>Eg 3.</b> {@code UTF-8''%c2%a3%20and%20%e2%82%ac%20rates}
+ * <strong>Eg 3.</strong> {@code UTF-8''%c2%a3%20and%20%e2%82%ac%20rates}
* will be decoded to {@code £ and € rates}
*
- * @param encodedText - Text to be decoded has a format of {@code <charset>'<language>'<encoded_value>}
+ * @param encodedText Text to be decoded has a format of {@code <charset>'<language>'<encoded_value>}
* and ASCII only
* @return Decoded text based on charset encoding
* @throws UnsupportedEncodingException The requested character set wasn't found.
@@ -124,8 +95,9 @@ public final class RFC2231Utility {
}
/**
- * Convert {@code text} to their corresponding Hex value.
- * @param text - ASCII text input
+ * Converts {@code text} to their corresponding Hex value.
+ *
+ * @param text ASCII text input
* @return Byte array of characters decoded from ASCII table
*/
private static byte[] fromHex(final String text) {
@@ -133,13 +105,13 @@ public final class RFC2231Utility {
final ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
for (int i = 0; i < text.length();) {
final char c = text.charAt(i++);
- if (c == '%') {
+ if (c == PERCENT) {
if (i > text.length() - 2) {
break; // unterminated sequence
}
final byte b1 = HEX_DECODE[text.charAt(i++) & MASK];
final byte b2 = HEX_DECODE[text.charAt(i++) & MASK];
- out.write((b1 << shift) | b2);
+ out.write(b1 << shift | b2);
} else {
out.write((byte) c);
}
@@ -151,4 +123,39 @@ public final class RFC2231Utility {
// good enough for standard values
return mimeCharset;
}
+
+ /**
+ * Checks if Asterisk (*) at the end of parameter name to indicate,
+ * if it has charset and language information to decode the value.
+ * @param paramName The parameter, which is being checked.
+ * @return {@code true}, if encoded as per RFC 2231, {@code false} otherwise
+ */
+ public static boolean hasEncodedValue(final String paramName) {
+ if (paramName != null) {
+ return paramName.lastIndexOf('*') == paramName.length() - 1;
+ }
+ return false;
+ }
+
+ /**
+ * If {@code paramName} has Asterisk (*) at the end, it will be stripped off,
+ * else the passed value will be returned.
+ * @param paramName The parameter, which is being inspected.
+ * @return stripped {@code paramName} of Asterisk (*), if RFC2231 encoded
+ */
+ public static String stripDelimiter(final String paramName) {
+ if (hasEncodedValue(paramName)) {
+ final StringBuilder paramBuilder = new StringBuilder(paramName);
+ paramBuilder.deleteCharAt(paramName.lastIndexOf('*'));
+ return paramBuilder.toString();
+ }
+ return paramName;
+ }
+
+ /**
+ * Private constructor so that no instances can be created. This class
+ * contains only static utility methods.
+ */
+ private RFC2231Utility() {
+ }
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 9666aa852c..4aa28e1847 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -277,6 +277,10 @@
<update>
Update Tomcat Native to 2.0.9. (markt)
</update>
+ <update>
+ Update the internal fork of Apache Commons FileUpload to 7a8c324
+ (2023-06-03, 1.x-SNAPSHOT). (markt)
+ </update>
</changelog>
</subsection>
</section>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
Re: (tomcat) branch main updated: Update fork of Commons FileUpload to latest 1.x
Posted by Mark Thomas <ma...@apache.org>.
On 03/06/2025 14:37, markt@apache.org wrote:
> This is an automated email from the ASF dual-hosted git repository.
>
> markt pushed a commit to branch main
> in repository https://gitbox.apache.org/repos/asf/tomcat.git
>
>
> The following commit(s) were added to refs/heads/main by this push:
> new 81f27a990f Update fork of Commons FileUpload to latest 1.x
> 81f27a990f is described below
>
> commit 81f27a990fd780c6a4ced79f2f830716f455e695
> Author: Mark Thomas <ma...@apache.org>
> AuthorDate: Tue Jun 3 14:37:38 2025 +0100
>
> Update fork of Commons FileUpload to latest 1.x
I'm currently working on some FileUpload improvements. This merge is an
interim step as I align our fork with Commons FleUpload (we had a small
number of fixes that needed to be ported back). I hope to be able to
merge 1.6 (or at least a 1.6 RC) shortly.
Mark
> ---
> MERGE.txt | 2 +-
> .../tomcat/util/http/fileupload/FileItem.java | 140 ++-
> .../util/http/fileupload/FileItemHeaders.java | 22 +-
> .../util/http/fileupload/FileItemStream.java | 49 +-
> .../tomcat/util/http/fileupload/FileUpload.java | 12 +-
> .../util/http/fileupload/FileUploadBase.java | 472 ++++----
> .../util/http/fileupload/FileUploadException.java | 12 +-
> .../http/fileupload/InvalidFileNameException.java | 10 +-
> .../util/http/fileupload/MultipartStream.java | 1197 ++++++++++----------
> .../util/http/fileupload/ParameterParser.java | 291 +++--
> .../util/http/fileupload/ProgressListener.java | 8 +-
> .../util/http/fileupload/disk/DiskFileItem.java | 652 +++++------
> .../http/fileupload/disk/DiskFileItemFactory.java | 115 +-
> .../tomcat/util/http/fileupload/package-info.java | 4 +-
> .../fileupload/servlet/ServletRequestContext.java | 40 +-
> .../http/fileupload/util/FileItemHeadersImpl.java | 37 +-
> .../http/fileupload/util/LimitedInputStream.java | 81 +-
> .../tomcat/util/http/fileupload/util/Streams.java | 74 +-
> .../http/fileupload/util/mime/MimeUtility.java | 18 +-
> .../http/fileupload/util/mime/ParseException.java | 2 +
> .../util/mime/QuotedPrintableDecoder.java | 53 +-
> .../http/fileupload/util/mime/RFC2231Utility.java | 93 +-
> webapps/docs/changelog.xml | 4 +
> 23 files changed, 1650 insertions(+), 1738 deletions(-)
>
> diff --git a/MERGE.txt b/MERGE.txt
> index c81293c991..89ba88208a 100644
> --- a/MERGE.txt
> +++ b/MERGE.txt
> @@ -46,7 +46,7 @@ Branch: 1.x
> Sub-tree:
> src/main/java/org/apache/commons/fileupload
> The SHA1 ID / tag for the most recent commit to be merged to Tomcat is:
> -7a8c3241cfa8d036452cd4fc3f92d57cff189bca (2023-09-16)
> +abe5d94b7ef6ff7164b48b5bb3781979ebbafb0e (2025-06-03)
>
> Note: Tomcat's copy of fileupload also includes classes copied manually from
> Commons IO.
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItem.java b/java/org/apache/tomcat/util/http/fileupload/FileItem.java
> index e6f035fc7b..00316cc3ed 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileItem.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileItem.java
> @@ -20,7 +20,6 @@ import java.io.File;
> import java.io.IOException;
> import java.io.InputStream;
> import java.io.OutputStream;
> -import java.io.UncheckedIOException;
> import java.io.UnsupportedEncodingException;
>
> /**
> @@ -47,18 +46,21 @@ import java.io.UnsupportedEncodingException;
> */
> public interface FileItem extends FileItemHeadersSupport {
>
> - // ------------------------------- Methods from javax.activation.DataSource
> + /**
> + * Deletes the underlying storage for a file item, including deleting any
> + * associated temporary disk file. Although this storage will be deleted
> + * automatically when the {@code FileItem} instance is garbage
> + * collected, this method can be used to ensure that this is done at an
> + * earlier time, thus preserving system resources.
> + */
> + void delete();
>
> /**
> - * Returns an {@link java.io.InputStream InputStream} that can be
> - * used to retrieve the contents of the file.
> - *
> - * @return An {@link java.io.InputStream InputStream} that can be
> - * used to retrieve the contents of the file.
> + * Returns the contents of the file item as an array of bytes.
> *
> - * @throws IOException if an error occurs.
> + * @return The contents of the file item as an array of bytes.
> */
> - InputStream getInputStream() throws IOException;
> + byte[] get();
>
> /**
> * Returns the content type passed by the browser or {@code null} if
> @@ -69,6 +71,25 @@ public interface FileItem extends FileItemHeadersSupport {
> */
> String getContentType();
>
> + /**
> + * Returns the name of the field in the multipart form corresponding to
> + * this file item.
> + *
> + * @return The name of the form field.
> + */
> + String getFieldName();
> +
> + /**
> + * Returns an {@link java.io.InputStream InputStream} that can be
> + * used to retrieve the contents of the file.
> + *
> + * @return An {@link java.io.InputStream InputStream} that can be
> + * used to retrieve the contents of the file.
> + *
> + * @throws IOException if an error occurs.
> + */
> + InputStream getInputStream() throws IOException;
> +
> /**
> * Returns the original file name in the client's file system, as provided by
> * the browser (or other client software). In most cases, this will be the
> @@ -83,16 +104,16 @@ public interface FileItem extends FileItemHeadersSupport {
> */
> String getName();
>
> - // ------------------------------------------------------- FileItem methods
> -
> /**
> - * Provides a hint as to whether or not the file contents will be read
> - * from memory.
> + * Returns an {@link java.io.OutputStream OutputStream} that can
> + * be used for storing the contents of the file.
> *
> - * @return {@code true} if the file contents will be read from memory;
> - * {@code false} otherwise.
> + * @return An {@link java.io.OutputStream OutputStream} that can be used
> + * for storing the contents of the file.
> + *
> + * @throws IOException if an error occurs.
> */
> - boolean isInMemory();
> + OutputStream getOutputStream() throws IOException;
>
> /**
> * Returns the size of the file item.
> @@ -102,13 +123,13 @@ public interface FileItem extends FileItemHeadersSupport {
> long getSize();
>
> /**
> - * Returns the contents of the file item as an array of bytes.
> - *
> - * @return The contents of the file item as an array of bytes.
> + * Returns the contents of the file item as a String, using the default
> + * character encoding. This method uses {@link #get()} to retrieve the
> + * contents of the item.
> *
> - * @throws UncheckedIOException if an I/O error occurs
> + * @return The contents of the item, as a string.
> */
> - byte[] get() throws UncheckedIOException;
> + String getString();
>
> /**
> * Returns the contents of the file item as a String, using the specified
> @@ -121,53 +142,26 @@ public interface FileItem extends FileItemHeadersSupport {
> *
> * @throws UnsupportedEncodingException if the requested character
> * encoding is not available.
> - * @throws IOException if an I/O error occurs
> - */
> - String getString(String encoding) throws UnsupportedEncodingException, IOException;
> -
> - /**
> - * Returns the contents of the file item as a String, using the default
> - * character encoding. This method uses {@link #get()} to retrieve the
> - * contents of the item.
> - *
> - * @return The contents of the item, as a string.
> */
> - String getString();
> + String getString(String encoding) throws UnsupportedEncodingException;
>
> /**
> - * A convenience method to write an uploaded item to disk. The client code
> - * is not concerned with whether or not the item is stored in memory, or on
> - * disk in a temporary location. They just want to write the uploaded item
> - * to a file.
> - * <p>
> - * This method is not guaranteed to succeed if called more than once for
> - * the same item. This allows a particular implementation to use, for
> - * example, file renaming, where possible, rather than copying all of the
> - * underlying data, thus gaining a significant performance benefit.
> - *
> - * @param file The {@code File} into which the uploaded item should
> - * be stored.
> + * Determines whether or not a {@code FileItem} instance represents
> + * a simple form field.
> *
> - * @throws Exception if an error occurs.
> - */
> - void write(File file) throws Exception;
> -
> - /**
> - * Deletes the underlying storage for a file item, including deleting any
> - * associated temporary disk file. Although this storage will be deleted
> - * automatically when the {@code FileItem} instance is garbage
> - * collected, this method can be used to ensure that this is done at an
> - * earlier time, thus preserving system resources.
> + * @return {@code true} if the instance represents a simple form
> + * field; {@code false} if it represents an uploaded file.
> */
> - void delete();
> + boolean isFormField();
>
> /**
> - * Returns the name of the field in the multipart form corresponding to
> - * this file item.
> + * Provides a hint as to whether or not the file contents will be read
> + * from memory.
> *
> - * @return The name of the form field.
> + * @return {@code true} if the file contents will be read from memory;
> + * {@code false} otherwise.
> */
> - String getFieldName();
> + boolean isInMemory();
>
> /**
> * Sets the field name used to reference this file item.
> @@ -176,15 +170,6 @@ public interface FileItem extends FileItemHeadersSupport {
> */
> void setFieldName(String name);
>
> - /**
> - * Determines whether or not a {@code FileItem} instance represents
> - * a simple form field.
> - *
> - * @return {@code true} if the instance represents a simple form
> - * field; {@code false} if it represents an uploaded file.
> - */
> - boolean isFormField();
> -
> /**
> * Specifies whether or not a {@code FileItem} instance represents
> * a simple form field.
> @@ -195,14 +180,21 @@ public interface FileItem extends FileItemHeadersSupport {
> void setFormField(boolean state);
>
> /**
> - * Returns an {@link java.io.OutputStream OutputStream} that can
> - * be used for storing the contents of the file.
> + * A convenience method to write an uploaded item to disk. The client code
> + * is not concerned with whether or not the item is stored in memory, or on
> + * disk in a temporary location. They just want to write the uploaded item
> + * to a file.
> + * <p>
> + * This method is not guaranteed to succeed if called more than once for
> + * the same item. This allows a particular implementation to use, for
> + * example, file renaming, where possible, rather than copying all of the
> + * underlying data, thus gaining a significant performance benefit.
> *
> - * @return An {@link java.io.OutputStream OutputStream} that can be used
> - * for storing the contents of the file.
> + * @param file The {@code File} into which the uploaded item should
> + * be stored.
> *
> - * @throws IOException if an error occurs.
> + * @throws Exception if an error occurs.
> */
> - OutputStream getOutputStream() throws IOException;
> + void write(File file) throws Exception;
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
> index 890f4397ae..30b1d40e25 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemHeaders.java
> @@ -42,6 +42,17 @@ public interface FileItemHeaders {
> */
> String getHeader(String name);
>
> + /**
> + * <p>
> + * Returns an {@code Iterator} of all the header names.
> + * </p>
> + *
> + * @return an {@code Iterator} containing all of the names of
> + * headers provided with this file item. If the item does not have
> + * any headers return an empty {@code Iterator}
> + */
> + Iterator<String> getHeaderNames();
> +
> /**
> * <p>
> * Returns all the values of the specified item header as an
> @@ -60,15 +71,4 @@ public interface FileItemHeaders {
> */
> Iterator<String> getHeaders(String name);
>
> - /**
> - * <p>
> - * Returns an {@code Iterator} of all the header names.
> - * </p>
> - *
> - * @return an {@code Iterator} containing all of the names of
> - * headers provided with this file item. If the item does not have
> - * any headers return an empty {@code Iterator}
> - */
> - Iterator<String> getHeaderNames();
> -
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
> index c31d9eb13b..e8734e4c93 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileItemStream.java
> @@ -48,20 +48,14 @@ public interface FileItemStream extends FileItemHeadersSupport {
> */
> private static final long serialVersionUID = -7280778431581963740L;
>
> - }
> + /**
> + * Constructs a new instance.
> + */
> + public ItemSkippedException() {
> + // empty
> + }
>
> - /**
> - * Creates an {@link InputStream}, which allows to read the
> - * items contents.
> - *
> - * @return The input stream, from which the items data may
> - * be read.
> - * @throws IllegalStateException The method was already invoked on
> - * this item. It is not possible to recreate the data stream.
> - * @throws IOException An I/O error occurred.
> - * @see ItemSkippedException
> - */
> - InputStream openStream() throws IOException;
> + }
>
> /**
> * Returns the content type passed by the browser or {@code null} if
> @@ -72,6 +66,14 @@ public interface FileItemStream extends FileItemHeadersSupport {
> */
> String getContentType();
>
> + /**
> + * Returns the name of the field in the multipart form corresponding to
> + * this file item.
> + *
> + * @return The name of the form field.
> + */
> + String getFieldName();
> +
> /**
> * Returns the original file name in the client's file system, as provided by
> * the browser (or other client software). In most cases, this will be the
> @@ -82,14 +84,6 @@ public interface FileItemStream extends FileItemHeadersSupport {
> */
> String getName();
>
> - /**
> - * Returns the name of the field in the multipart form corresponding to
> - * this file item.
> - *
> - * @return The name of the form field.
> - */
> - String getFieldName();
> -
> /**
> * Determines whether or not a {@code FileItem} instance represents
> * a simple form field.
> @@ -99,4 +93,17 @@ public interface FileItemStream extends FileItemHeadersSupport {
> */
> boolean isFormField();
>
> + /**
> + * Creates an {@link InputStream}, which allows to read the
> + * items contents.
> + *
> + * @return The input stream, from which the items data may
> + * be read.
> + * @throws IllegalStateException The method was already invoked on
> + * this item. It is not possible to recreate the data stream.
> + * @throws IOException An I/O error occurred.
> + * @see ItemSkippedException
> + */
> + InputStream openStream() throws IOException;
> +
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
> index 06846f18d6..cd16f07f83 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileUpload.java
> @@ -33,15 +33,11 @@ package org.apache.tomcat.util.http.fileupload;
> public class FileUpload
> extends FileUploadBase {
>
> - // ----------------------------------------------------------- Data members
> -
> /**
> * The factory to use to create new form items.
> */
> private FileItemFactory fileItemFactory;
>
> - // ----------------------------------------------------------- Constructors
> -
> /**
> * Constructs an uninitialized instance of this class.
> *
> @@ -52,8 +48,6 @@ public class FileUpload
> public FileUpload() {
> }
>
> - // ----------------------------------------------------- Property accessors
> -
> /**
> * Returns the factory class used when creating file items.
> *
> @@ -67,11 +61,11 @@ public class FileUpload
> /**
> * Sets the factory class to use when creating file items.
> *
> - * @param factory The factory class for new file items.
> + * @param fileItemFactory The factory class for new file items.
> */
> @Override
> - public void setFileItemFactory(final FileItemFactory factory) {
> - this.fileItemFactory = factory;
> + public void setFileItemFactory(final FileItemFactory fileItemFactory) {
> + this.fileItemFactory = fileItemFactory;
> }
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
> index b9f5cfe174..73e5f1e63e 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
> @@ -33,9 +33,10 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
>
>
> /**
> - * <p>High level API for processing file uploads.</p>
> + * High level API for processing file uploads.
> *
> - * <p>This class handles multiple files per single HTML widget, sent using
> + * <p>
> + * This class handles multiple files per single HTML widget, sent using
> * {@code multipart/mixed} encoding type, as specified by
> * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
> * #parseRequest(RequestContext)} to acquire a list of {@link
> @@ -48,7 +49,15 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
> */
> public abstract class FileUploadBase {
>
> - // ----------------------------------------------------- Manifest constants
> + /**
> + * Line feed.
> + */
> + private static final char LF = '\n';
> +
> + /**
> + * Carriage return.
> + */
> + private static final char CR = '\r';
>
> /**
> * HTTP content type header name.
> @@ -90,8 +99,6 @@ public abstract class FileUploadBase {
> */
> public static final String MULTIPART_MIXED = "multipart/mixed";
>
> - // ----------------------------------------------------------- Data members
> -
> /**
> * The maximum size permitted for the complete request, as opposed to
> * {@link #fileSizeMax}. A value of -1 indicates no maximum.
> @@ -120,70 +127,65 @@ public abstract class FileUploadBase {
> */
> private ProgressListener listener;
>
> - // ----------------------------------------------------- Property accessors
> -
> - /**
> - * Returns the factory class used when creating file items.
> - *
> - * @return The factory class for new file items.
> - */
> - public abstract FileItemFactory getFileItemFactory();
> -
> /**
> - * Sets the factory class to use when creating file items.
> - *
> - * @param factory The factory class for new file items.
> - */
> - public abstract void setFileItemFactory(FileItemFactory factory);
> -
> - /**
> - * Returns the maximum allowed size of a complete request, as opposed
> - * to {@link #getFileSizeMax()}.
> - *
> - * @return The maximum allowed size, in bytes. The default value of
> - * -1 indicates, that there is no limit.
> - *
> - * @see #setSizeMax(long)
> - *
> + * Constructs a new instance.
> */
> - public long getSizeMax() {
> - return sizeMax;
> + public FileUploadBase() {
> + // empty
> }
>
> /**
> - * Sets the maximum allowed size of a complete request, as opposed
> - * to {@link #setFileSizeMax(long)}.
> + * Gets the boundary from the {@code Content-type} header.
> *
> - * @param sizeMax The maximum allowed size, in bytes. The default value of
> - * -1 indicates, that there is no limit.
> - *
> - * @see #getSizeMax()
> + * @param contentType The value of the content type header from which to
> + * extract the boundary value.
> *
> + * @return The boundary, as a byte array.
> */
> - public void setSizeMax(final long sizeMax) {
> - this.sizeMax = sizeMax;
> + public byte[] getBoundary(final String contentType) {
> + final ParameterParser parser = new ParameterParser();
> + parser.setLowerCaseNames(true);
> + // Parameter parser can handle null input
> + final Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
> + final String boundaryStr = params.get("boundary");
> +
> + if (boundaryStr == null) {
> + return null;
> + }
> + return boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
> }
>
> /**
> - * Returns the maximum allowed size of a single uploaded file,
> - * as opposed to {@link #getSizeMax()}.
> + * Gets the field name from the {@code Content-disposition}
> + * header.
> *
> - * @see #setFileSizeMax(long)
> - * @return Maximum size of a single uploaded file.
> + * @param headers A {@code Map} containing the HTTP request headers.
> + *
> + * @return The field name for the current {@code encapsulation}.
> */
> - public long getFileSizeMax() {
> - return fileSizeMax;
> + public String getFieldName(final FileItemHeaders headers) {
> + return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
> }
>
> /**
> - * Sets the maximum allowed size of a single uploaded file,
> - * as opposed to {@link #getSizeMax()}.
> - *
> - * @see #getFileSizeMax()
> - * @param fileSizeMax Maximum size of a single uploaded file.
> + * Returns the field name, which is given by the content-disposition
> + * header.
> + * @param contentDisposition The content-dispositions header value.
> + * @return The field name.
> */
> - public void setFileSizeMax(final long fileSizeMax) {
> - this.fileSizeMax = fileSizeMax;
> + private String getFieldName(final String contentDisposition) {
> + String fieldName = null;
> + if (contentDisposition != null && contentDisposition.toLowerCase(Locale.ROOT).startsWith(FORM_DATA)) {
> + final ParameterParser parser = new ParameterParser();
> + parser.setLowerCaseNames(true);
> + // Parameter parser can handle null input
> + final Map<String, String> params = parser.parse(contentDisposition, ';');
> + fieldName = params.get("name");
> + if (fieldName != null) {
> + fieldName = fieldName.trim();
> + }
> + }
> + return fieldName;
> }
>
> /**
> @@ -196,155 +198,14 @@ public abstract class FileUploadBase {
> }
>
> /**
> - * Sets the maximum number of files allowed per request.
> - *
> - * @param fileCountMax The new limit. {@code -1} means no limit.
> - */
> - public void setFileCountMax(final long fileCountMax) {
> - this.fileCountMax = fileCountMax;
> - }
> -
> - /**
> - * Retrieves the character encoding used when reading the headers of an
> - * individual part. When not specified, or {@code null}, the request
> - * encoding is used. If that is also not specified, or {@code null},
> - * the platform default encoding is used.
> - *
> - * @return The encoding used to read part headers.
> - */
> - public String getHeaderEncoding() {
> - return headerEncoding;
> - }
> -
> - /**
> - * Specifies the character encoding to be used when reading the headers of
> - * individual part. When not specified, or {@code null}, the request
> - * encoding is used. If that is also not specified, or {@code null},
> - * the platform default encoding is used.
> - *
> - * @param encoding The encoding used to read part headers.
> - */
> - public void setHeaderEncoding(final String encoding) {
> - headerEncoding = encoding;
> - }
> -
> - // --------------------------------------------------------- Public methods
> -
> - /**
> - * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
> - * compliant {@code multipart/form-data} stream.
> - *
> - * @param ctx The context for the request to be parsed.
> - *
> - * @return An iterator to instances of {@code FileItemStream}
> - * parsed from the request, in the order that they were
> - * transmitted.
> - *
> - * @throws FileUploadException if there are problems reading/parsing
> - * the request or storing files.
> - * @throws IOException An I/O error occurred. This may be a network
> - * error while communicating with the client or a problem while
> - * storing the uploaded content.
> - */
> - public FileItemIterator getItemIterator(final RequestContext ctx)
> - throws FileUploadException, IOException {
> - try {
> - return new FileItemIteratorImpl(this, ctx);
> - } catch (final FileUploadIOException e) {
> - // unwrap encapsulated SizeException
> - throw (FileUploadException) e.getCause();
> - }
> - }
> -
> - /**
> - * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
> - * compliant {@code multipart/form-data} stream.
> - *
> - * @param ctx The context for the request to be parsed.
> - *
> - * @return A list of {@code FileItem} instances parsed from the
> - * request, in the order that they were transmitted.
> - *
> - * @throws FileUploadException if there are problems reading/parsing
> - * the request or storing files.
> - */
> - public List<FileItem> parseRequest(final RequestContext ctx)
> - throws FileUploadException {
> - final List<FileItem> items = new ArrayList<>();
> - boolean successful = false;
> - try {
> - final FileItemIterator iter = getItemIterator(ctx);
> - final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
> - "No FileItemFactory has been set.");
> - final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
> - while (iter.hasNext()) {
> - if (items.size() == fileCountMax) {
> - // The next item will exceed the limit.
> - throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
> - }
> - final FileItemStream item = iter.next();
> - // Don't use getName() here to prevent an InvalidFileNameException.
> - final String fileName = item.getName();
> - final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
> - item.isFormField(), fileName);
> - items.add(fileItem);
> - try {
> - Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
> - } catch (final FileUploadIOException e) {
> - throw (FileUploadException) e.getCause();
> - } catch (final IOException e) {
> - throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
> - MULTIPART_FORM_DATA, e.getMessage()), e);
> - }
> - final FileItemHeaders fih = item.getHeaders();
> - fileItem.setHeaders(fih);
> - }
> - successful = true;
> - return items;
> - } catch (final FileUploadException e) {
> - throw e;
> - } catch (final IOException e) {
> - throw new FileUploadException(e.getMessage(), e);
> - } finally {
> - if (!successful) {
> - for (final FileItem fileItem : items) {
> - try {
> - fileItem.delete();
> - } catch (final Exception ignored) {
> - // ignored TODO perhaps add to tracker delete failure list somehow?
> - }
> - }
> - }
> - }
> - }
> -
> - // ------------------------------------------------------ Protected methods
> -
> - /**
> - * Retrieves the boundary from the {@code Content-type} header.
> - *
> - * @param contentType The value of the content type header from which to
> - * extract the boundary value.
> + * Returns the factory class used when creating file items.
> *
> - * @return The boundary, as a byte array.
> + * @return The factory class for new file items.
> */
> - public byte[] getBoundary(final String contentType) {
> - final ParameterParser parser = new ParameterParser();
> - parser.setLowerCaseNames(true);
> - // Parameter parser can handle null input
> - final Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
> - final String boundaryStr = params.get("boundary");
> -
> - if (boundaryStr == null) {
> - return null;
> - }
> - final byte[] boundary;
> - boundary = boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
> - return boundary;
> - }
> + public abstract FileItemFactory getFileItemFactory();
>
> /**
> - * Retrieves the file name from the {@code Content-disposition}
> + * Gets the file name from the {@code Content-disposition}
> * header.
> *
> * @param headers The HTTP headers object.
> @@ -357,18 +218,18 @@ public abstract class FileUploadBase {
>
> /**
> * Returns the given content-disposition headers file name.
> - * @param pContentDisposition The content-disposition headers value.
> + * @param contentDisposition The content-disposition headers value.
> * @return The file name
> */
> - private String getFileName(final String pContentDisposition) {
> + private String getFileName(final String contentDisposition) {
> String fileName = null;
> - if (pContentDisposition != null) {
> - final String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH);
> + if (contentDisposition != null) {
> + final String cdl = contentDisposition.toLowerCase(Locale.ROOT);
> if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
> final ParameterParser parser = new ParameterParser();
> parser.setLowerCaseNames(true);
> // Parameter parser can handle null input
> - final Map<String, String> params = parser.parse(pContentDisposition, ';');
> + final Map<String, String> params = parser.parse(contentDisposition, ';');
> if (params.containsKey("filename")) {
> fileName = params.get("filename");
> if (fileName != null) {
> @@ -386,37 +247,52 @@ public abstract class FileUploadBase {
> }
>
> /**
> - * Retrieves the field name from the {@code Content-disposition}
> - * header.
> + * Returns the maximum allowed size of a single uploaded file,
> + * as opposed to {@link #getSizeMax()}.
> *
> - * @param headers A {@code Map} containing the HTTP request headers.
> + * @see #setFileSizeMax(long)
> + * @return Maximum size of a single uploaded file.
> + */
> + public long getFileSizeMax() {
> + return fileSizeMax;
> + }
> +
> + /**
> + * Gets the character encoding used when reading the headers of an
> + * individual part. When not specified, or {@code null}, the request
> + * encoding is used. If that is also not specified, or {@code null},
> + * the platform default encoding is used.
> *
> - * @return The field name for the current {@code encapsulation}.
> + * @return The encoding used to read part headers.
> */
> - public String getFieldName(final FileItemHeaders headers) {
> - return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
> + public String getHeaderEncoding() {
> + return headerEncoding;
> }
>
> /**
> - * Returns the field name, which is given by the content-disposition
> - * header.
> - * @param pContentDisposition The content-dispositions header value.
> - * @return The field jake
> + * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
> + * compliant {@code multipart/form-data} stream.
> + *
> + * @param ctx The context for the request to be parsed.
> + *
> + * @return An iterator to instances of {@code FileItemStream}
> + * parsed from the request, in the order that they were
> + * transmitted.
> + *
> + * @throws FileUploadException if there are problems reading/parsing
> + * the request or storing files.
> + * @throws IOException An I/O error occurred. This may be a network
> + * error while communicating with the client or a problem while
> + * storing the uploaded content.
> */
> - private String getFieldName(final String pContentDisposition) {
> - String fieldName = null;
> - if (pContentDisposition != null
> - && pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) {
> - final ParameterParser parser = new ParameterParser();
> - parser.setLowerCaseNames(true);
> - // Parameter parser can handle null input
> - final Map<String, String> params = parser.parse(pContentDisposition, ';');
> - fieldName = params.get("name");
> - if (fieldName != null) {
> - fieldName = fieldName.trim();
> - }
> + public FileItemIterator getItemIterator(final RequestContext ctx)
> + throws FileUploadException, IOException {
> + try {
> + return new FileItemIteratorImpl(this, ctx);
> + } catch (final FileUploadIOException e) {
> + // unwrap encapsulated SizeException
> + throw (FileUploadException) e.getCause();
> }
> - return fieldName;
> }
>
> /**
> @@ -464,6 +340,29 @@ public abstract class FileUploadBase {
> return headers;
> }
>
> + /**
> + * Returns the progress listener.
> + *
> + * @return The progress listener, if any, or null.
> + */
> + public ProgressListener getProgressListener() {
> + return listener;
> + }
> +
> + /**
> + * Returns the maximum allowed size of a complete request, as opposed
> + * to {@link #getFileSizeMax()}.
> + *
> + * @return The maximum allowed size, in bytes. The default value of
> + * -1 indicates, that there is no limit.
> + *
> + * @see #setSizeMax(long)
> + *
> + */
> + public long getSizeMax() {
> + return sizeMax;
> + }
> +
> /**
> * Creates a new instance of {@link FileItemHeaders}.
> * @return The new instance.
> @@ -483,12 +382,12 @@ public abstract class FileUploadBase {
> private int parseEndOfLine(final String headerPart, final int end) {
> int index = end;
> for (;;) {
> - final int offset = headerPart.indexOf('\r', index);
> + final int offset = headerPart.indexOf(CR, index);
> if (offset == -1 || offset + 1 >= headerPart.length()) {
> throw new IllegalStateException(
> "Expected headers to be terminated by an empty line.");
> }
> - if (headerPart.charAt(offset + 1) == '\n') {
> + if (headerPart.charAt(offset + 1) == LF) {
> return offset;
> }
> index = offset + 1;
> @@ -512,21 +411,126 @@ public abstract class FileUploadBase {
> }
>
> /**
> - * Returns the progress listener.
> + * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
> + * compliant {@code multipart/form-data} stream.
> *
> - * @return The progress listener, if any, or null.
> + * @param ctx The context for the request to be parsed.
> + *
> + * @return A list of {@code FileItem} instances parsed from the
> + * request, in the order that they were transmitted.
> + *
> + * @throws FileUploadException if there are problems reading/parsing
> + * the request or storing files.
> */
> - public ProgressListener getProgressListener() {
> - return listener;
> + public List<FileItem> parseRequest(final RequestContext ctx) throws FileUploadException {
> + final List<FileItem> items = new ArrayList<>();
> + boolean successful = false;
> + try {
> + final FileItemIterator iter = getItemIterator(ctx);
> + final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
> + "No FileItemFactory has been set.");
> + final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
> + while (iter.hasNext()) {
> + if (items.size() == fileCountMax) {
> + // The next item will exceed the limit.
> + throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
> + }
> + final FileItemStream item = iter.next();
> + // Don't use getName() here to prevent an InvalidFileNameException.
> + final String fileName = item.getName();
> + final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
> + item.isFormField(), fileName);
> + items.add(fileItem);
> + try {
> + Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
> + } catch (final FileUploadIOException e) {
> + throw (FileUploadException) e.getCause();
> + } catch (final IOException e) {
> + throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
> + MULTIPART_FORM_DATA, e.getMessage()), e);
> + }
> + final FileItemHeaders fih = item.getHeaders();
> + fileItem.setHeaders(fih);
> + }
> + successful = true;
> + return items;
> + } catch (final FileUploadException e) {
> + throw e;
> + } catch (final IOException e) {
> + throw new FileUploadException(e.getMessage(), e);
> + } finally {
> + if (!successful) {
> + for (final FileItem fileItem : items) {
> + try {
> + fileItem.delete();
> + } catch (final Exception ignored) {
> + // ignored TODO perhaps add to tracker delete failure list somehow?
> + }
> + }
> + }
> + }
> + }
> +
> + /**
> + * Sets the maximum number of files allowed per request.
> + *
> + * @param fileCountMax The new limit. {@code -1} means no limit.
> + */
> + public void setFileCountMax(final long fileCountMax) {
> + this.fileCountMax = fileCountMax;
> + }
> +
> + /**
> + * Sets the factory class to use when creating file items.
> + *
> + * @param factory The factory class for new file items.
> + */
> + public abstract void setFileItemFactory(FileItemFactory factory);
> +
> + /**
> + * Sets the maximum allowed size of a single uploaded file,
> + * as opposed to {@link #getSizeMax()}.
> + *
> + * @see #getFileSizeMax()
> + * @param fileSizeMax Maximum size of a single uploaded file.
> + */
> + public void setFileSizeMax(final long fileSizeMax) {
> + this.fileSizeMax = fileSizeMax;
> + }
> +
> + /**
> + * Specifies the character encoding to be used when reading the headers of
> + * individual part. When not specified, or {@code null}, the request
> + * encoding is used. If that is also not specified, or {@code null},
> + * the platform default encoding is used.
> + *
> + * @param encoding The encoding used to read part headers.
> + */
> + public void setHeaderEncoding(final String encoding) {
> + headerEncoding = encoding;
> }
>
> /**
> * Sets the progress listener.
> *
> - * @param pListener The progress listener, if any. Defaults to null.
> + * @param listener The progress listener, if any. Defaults to null.
> */
> - public void setProgressListener(final ProgressListener pListener) {
> - listener = pListener;
> + public void setProgressListener(final ProgressListener listener) {
> + this.listener = listener;
> + }
> +
> + /**
> + * Sets the maximum allowed size of a complete request, as opposed
> + * to {@link #setFileSizeMax(long)}.
> + *
> + * @param sizeMax The maximum allowed size, in bytes. The default value of
> + * -1 indicates, that there is no limit.
> + *
> + * @see #getSizeMax()
> + *
> + */
> + public void setSizeMax(final long sizeMax) {
> + this.sizeMax = sizeMax;
> }
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
> index d7a8d11819..ea4226e140 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/FileUploadException.java
> @@ -36,20 +36,20 @@ public class FileUploadException extends IOException {
> * Constructs a new {@code FileUploadException} with specified detail
> * message.
> *
> - * @param msg the error message.
> + * @param message the error message.
> */
> - public FileUploadException(final String msg) {
> - super(msg);
> + public FileUploadException(final String message) {
> + super(message);
> }
>
> /**
> * Creates a new {@code FileUploadException} with the given
> * detail message and cause.
> *
> - * @param msg The exceptions detail message.
> + * @param message The exceptions detail message.
> * @param cause The exceptions cause.
> */
> - public FileUploadException(final String msg, final Throwable cause) {
> - super(msg, cause);
> + public FileUploadException(final String message, final Throwable cause) {
> + super(message, cause);
> }
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
> index 5018620fac..ecc1dce896 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/InvalidFileNameException.java
> @@ -42,12 +42,12 @@ public class InvalidFileNameException extends RuntimeException {
> /**
> * Creates a new instance.
> *
> - * @param pName The file name causing the exception.
> - * @param pMessage A human readable error message.
> + * @param name The file name causing the exception.
> + * @param message A human readable error message.
> */
> - public InvalidFileNameException(final String pName, final String pMessage) {
> - super(pMessage);
> - name = pName;
> + public InvalidFileNameException(final String name, final String message) {
> + super(message);
> + this.name = name;
> }
>
> /**
> diff --git a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
> index 44e52563e2..97c412da66 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/MultipartStream.java
> @@ -27,7 +27,7 @@ import org.apache.tomcat.util.http.fileupload.util.Closeable;
> import org.apache.tomcat.util.http.fileupload.util.Streams;
>
> /**
> - * <p> Low level API for processing file uploads.
> + * Low level API for processing file uploads.
> *
> * <p> This class can be used to process data streams conforming to MIME
> * 'multipart' format as defined in
> @@ -58,7 +58,9 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
> * boundary token of the same length as the parent stream (see {@link
> * #setBoundary(byte[])}).
> *
> - * <p>Here is an example of usage of this class.<br>
> + * <p>
> + * Here is an example of usage of this class.
> + * </p>
> *
> * <pre>
> * try {
> @@ -82,145 +84,473 @@ import org.apache.tomcat.util.http.fileupload.util.Streams;
> public class MultipartStream {
>
> /**
> - * Internal class, which is used to invoke the
> - * {@link ProgressListener}.
> + * Thrown upon attempt of setting an invalid boundary token.
> */
> - public static class ProgressNotifier {
> + public static class IllegalBoundaryException extends IOException {
>
> /**
> - * The listener to invoke.
> + * The UID to use when serializing this instance.
> */
> - private final ProgressListener listener;
> + private static final long serialVersionUID = -161533165102632918L;
>
> /**
> - * Number of expected bytes, if known, or -1.
> + * Constructs an {@code IllegalBoundaryException} with no
> + * detail message.
> */
> - private final long contentLength;
> + public IllegalBoundaryException() {
> + }
>
> /**
> - * Number of bytes, which have been read so far.
> + * Constructs an {@code IllegalBoundaryException} with
> + * the specified detail message.
> + *
> + * @param message The detail message.
> */
> - private long bytesRead;
> + public IllegalBoundaryException(final String message) {
> + super(message);
> + }
> +
> + }
> +
> + /**
> + * An {@link InputStream} for reading an items contents.
> + */
> + public class ItemInputStream extends InputStream implements Closeable {
>
> /**
> - * Number of items, which have been read so far.
> + * Offset when converting negative bytes to integers.
> */
> - private int items;
> + private static final int BYTE_POSITIVE_OFFSET = 256;
>
> /**
> - * Creates a new instance with the given listener
> - * and content length.
> - *
> - * @param pListener The listener to invoke.
> - * @param pContentLength The expected content length.
> + * The number of bytes, which have been read so far.
> + */
> + private long total;
> +
> + /**
> + * The number of bytes, which must be hold, because
> + * they might be a part of the boundary.
> + */
> + private int pad;
> +
> + /**
> + * The current offset in the buffer.
> + */
> + private int pos;
> +
> + /**
> + * Whether the stream is already closed.
> + */
> + private boolean closed;
> +
> + /**
> + * Creates a new instance.
> */
> - public ProgressNotifier(final ProgressListener pListener, final long pContentLength) {
> - listener = pListener;
> - contentLength = pContentLength;
> + ItemInputStream() {
> + findSeparator();
> }
>
> /**
> - * Called to indicate that bytes have been read.
> + * Returns the number of bytes, which are currently
> + * available, without blocking.
> *
> - * @param pBytes Number of bytes, which have been read.
> + * @throws IOException An I/O error occurs.
> + * @return Number of bytes in the buffer.
> */
> - void noteBytesRead(final int pBytes) {
> - /* Indicates, that the given number of bytes have been read from
> - * the input stream.
> - */
> - bytesRead += pBytes;
> - notifyListener();
> + @Override
> + public int available() throws IOException {
> + if (pos == -1) {
> + return tail - head - pad;
> + }
> + return pos - head;
> }
>
> /**
> - * Called to indicate, that a new file item has been detected.
> + * Closes the input stream.
> + *
> + * @throws IOException An I/O error occurred.
> */
> - public void noteItem() {
> - ++items;
> - notifyListener();
> + @Override
> + public void close() throws IOException {
> + close(false);
> }
>
> /**
> - * Called for notifying the listener.
> + * Closes the input stream.
> + *
> + * @param closeUnderlying Whether to close the underlying stream (hard close)
> + * @throws IOException An I/O error occurred.
> */
> - private void notifyListener() {
> - if (listener != null) {
> - listener.update(bytesRead, contentLength, items);
> + public void close(final boolean closeUnderlying) throws IOException {
> + if (closed) {
> + return;
> }
> + if (closeUnderlying) {
> + closed = true;
> + input.close();
> + } else {
> + for (;;) {
> + int available = available();
> + if (available == 0) {
> + available = makeAvailable();
> + if (available == 0) {
> + break;
> + }
> + }
> + if (skip(available) != available) {
> + // TODO log or throw?
> + }
> + }
> + }
> + closed = true;
> }
>
> - }
> -
> - // ----------------------------------------------------- Manifest constants
> -
> - /**
> - * The Carriage Return ASCII character value.
> - */
> - public static final byte CR = 0x0D;
> -
> - /**
> - * The Line Feed ASCII character value.
> - */
> - public static final byte LF = 0x0A;
> -
> - /**
> - * The dash (-) ASCII character value.
> - */
> - public static final byte DASH = 0x2D;
> -
> - /**
> - * The maximum length of {@code header-part} that will be
> - * processed (10 kilobytes = 10240 bytes.).
> - */
> - public static final int HEADER_PART_SIZE_MAX = 10240;
> + /**
> + * Called for finding the separator.
> + */
> + private void findSeparator() {
> + pos = MultipartStream.this.findSeparator();
> + if (pos == -1) {
> + if (tail - head > keepRegion) {
> + pad = keepRegion;
> + } else {
> + pad = tail - head;
> + }
> + }
> + }
>
> - /**
> - * The default length of the buffer used for processing a request.
> - */
> - protected static final int DEFAULT_BUFSIZE = 4096;
> + /**
> + * Returns the number of bytes, which have been read
> + * by the stream.
> + *
> + * @return Number of bytes, which have been read so far.
> + */
> + public long getBytesRead() {
> + return total;
> + }
>
> - /**
> - * A byte sequence that marks the end of {@code header-part}
> - * ({@code CRLFCRLF}).
> - */
> - protected static final byte[] HEADER_SEPARATOR = {CR, LF, CR, LF};
> + /**
> + * Returns, whether the stream is closed.
> + *
> + * @return True, if the stream is closed, otherwise false.
> + */
> + @Override
> + public boolean isClosed() {
> + return closed;
> + }
>
> - /**
> - * A byte sequence that follows a delimiter that will be
> - * followed by an encapsulation ({@code CRLF}).
> - */
> - protected static final byte[] FIELD_SEPARATOR = {CR, LF};
> + /**
> + * Attempts to read more data.
> + *
> + * @return Number of available bytes
> + * @throws IOException An I/O error occurred.
> + */
> + private int makeAvailable() throws IOException {
> + if (pos != -1) {
> + return 0;
> + }
>
> - /**
> - * A byte sequence that follows a delimiter of the last
> - * encapsulation in the stream ({@code --}).
> - */
> - protected static final byte[] STREAM_TERMINATOR = {DASH, DASH};
> + // Move the data to the beginning of the buffer.
> + total += tail - head - pad;
> + System.arraycopy(buffer, tail - pad, buffer, 0, pad);
>
> - /**
> - * A byte sequence that precedes a boundary ({@code CRLF--}).
> - */
> - protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
> + // Refill buffer with new data.
> + head = 0;
> + tail = pad;
>
> - // ----------------------------------------------------------- Data members
> + for (;;) {
> + final int bytesRead = input.read(buffer, tail, bufSize - tail);
> + if (bytesRead == -1) {
> + // The last pad amount is left in the buffer.
> + // Boundary can't be in there so signal an error
> + // condition.
> + final String msg = "Stream ended unexpectedly";
> + throw new MalformedStreamException(msg);
> + }
> + if (notifier != null) {
> + notifier.noteBytesRead(bytesRead);
> + }
> + tail += bytesRead;
>
> - /**
> - * The input stream from which data is read.
> - */
> - private final InputStream input;
> + findSeparator();
> + final int av = available();
>
> - /**
> - * The length of the boundary token plus the leading {@code CRLF--}.
> - */
> - private int boundaryLength;
> + if (av > 0 || pos != -1) {
> + return av;
> + }
> + }
> + }
>
> - /**
> - * The amount of data, in bytes, that must be kept in the buffer in order
> - * to detect delimiters reliably.
> - */
> - private final int keepRegion;
> + /**
> + * Returns the next byte in the stream.
> + *
> + * @return The next byte in the stream, as a non-negative
> + * integer, or -1 for EOF.
> + * @throws IOException An I/O error occurred.
> + */
> + @Override
> + public int read() throws IOException {
> + if (closed) {
> + throw new FileItemStream.ItemSkippedException();
> + }
> + if (available() == 0 && makeAvailable() == 0) {
> + return -1;
> + }
> + ++total;
> + final int b = buffer[head++];
> + if (b >= 0) {
> + return b;
> + }
> + return b + BYTE_POSITIVE_OFFSET;
> + }
>
> - /**
> + /**
> + * Reads bytes into the given buffer.
> + *
> + * @param b The destination buffer, where to write to.
> + * @param off Offset of the first byte in the buffer.
> + * @param len Maximum number of bytes to read.
> + * @return Number of bytes, which have been actually read,
> + * or -1 for EOF.
> + * @throws IOException An I/O error occurred.
> + */
> + @Override
> + public int read(final byte[] b, final int off, final int len) throws IOException {
> + if (closed) {
> + throw new FileItemStream.ItemSkippedException();
> + }
> + if (len == 0) {
> + return 0;
> + }
> + int res = available();
> + if (res == 0) {
> + res = makeAvailable();
> + if (res == 0) {
> + return -1;
> + }
> + }
> + res = Math.min(res, len);
> + System.arraycopy(buffer, head, b, off, res);
> + head += res;
> + total += res;
> + return res;
> + }
> +
> + /**
> + * Skips the given number of bytes.
> + *
> + * @param bytes Number of bytes to skip.
> + * @return The number of bytes, which have actually been
> + * skipped.
> + * @throws IOException An I/O error occurred.
> + */
> + @Override
> + public long skip(final long bytes) throws IOException {
> + if (closed) {
> + throw new FileItemStream.ItemSkippedException();
> + }
> + int av = available();
> + if (av == 0) {
> + av = makeAvailable();
> + if (av == 0) {
> + return 0;
> + }
> + }
> + final long res = Math.min(av, bytes);
> + head += res;
> + return res;
> + }
> +
> + }
> +
> + /**
> + * Thrown to indicate that the input stream fails to follow the
> + * required syntax.
> + */
> + public static class MalformedStreamException extends IOException {
> +
> + /**
> + * The UID to use when serializing this instance.
> + */
> + private static final long serialVersionUID = 6466926458059796677L;
> +
> + /**
> + * Constructs a {@code MalformedStreamException} with no
> + * detail message.
> + */
> + public MalformedStreamException() {
> + }
> +
> + /**
> + * Constructs an {@code MalformedStreamException} with
> + * the specified detail message.
> + *
> + * @param message The detail message.
> + */
> + public MalformedStreamException(final String message) {
> + super(message);
> + }
> +
> + }
> +
> + /**
> + * Internal class, which is used to invoke the
> + * {@link ProgressListener}.
> + */
> + public static class ProgressNotifier {
> +
> + /**
> + * The listener to invoke.
> + */
> + private final ProgressListener listener;
> +
> + /**
> + * Number of expected bytes, if known, or -1.
> + */
> + private final long contentLength;
> +
> + /**
> + * Number of bytes, which have been read so far.
> + */
> + private long bytesRead;
> +
> + /**
> + * Number of items, which have been read so far.
> + */
> + private int items;
> +
> + /**
> + * Creates a new instance with the given listener
> + * and content length.
> + *
> + * @param listener The listener to invoke.
> + * @param contentLength The expected content length.
> + */
> + public ProgressNotifier(final ProgressListener listener, final long contentLength) {
> + this.listener = listener;
> + this.contentLength = contentLength;
> + }
> +
> + /**
> + * Called to indicate that bytes have been read.
> + *
> + * @param count Number of bytes, which have been read.
> + */
> + void noteBytesRead(final int count) {
> + /* Indicates, that the given number of bytes have been read from
> + * the input stream.
> + */
> + bytesRead += count;
> + notifyListener();
> + }
> +
> + /**
> + * Called to indicate, that a new file item has been detected.
> + */
> + public void noteItem() {
> + ++items;
> + notifyListener();
> + }
> +
> + /**
> + * Called for notifying the listener.
> + */
> + private void notifyListener() {
> + if (listener != null) {
> + listener.update(bytesRead, contentLength, items);
> + }
> + }
> +
> + }
> +
> + /**
> + * The Carriage Return ASCII character value.
> + */
> + public static final byte CR = 0x0D;
> +
> + /**
> + * The Line Feed ASCII character value.
> + */
> + public static final byte LF = 0x0A;
> +
> + /**
> + * The dash (-) ASCII character value.
> + */
> + public static final byte DASH = 0x2D;
> +
> + /**
> + * The maximum length of {@code header-part} that will be
> + * processed (10 kilobytes = 10240 bytes.).
> + */
> + public static final int HEADER_PART_SIZE_MAX = 10240;
> +
> + /**
> + * The default length of the buffer used for processing a request.
> + */
> + protected static final int DEFAULT_BUFSIZE = 4096;
> +
> + /**
> + * A byte sequence that marks the end of {@code header-part}
> + * ({@code CRLFCRLF}).
> + */
> + protected static final byte[] HEADER_SEPARATOR = {CR, LF, CR, LF};
> +
> + /**
> + * A byte sequence that follows a delimiter that will be
> + * followed by an encapsulation ({@code CRLF}).
> + */
> + protected static final byte[] FIELD_SEPARATOR = {CR, LF};
> +
> + /**
> + * A byte sequence that follows a delimiter of the last
> + * encapsulation in the stream ({@code --}).
> + */
> + protected static final byte[] STREAM_TERMINATOR = {DASH, DASH};
> +
> + /**
> + * A byte sequence that precedes a boundary ({@code CRLF--}).
> + */
> + protected static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH};
> +
> + /**
> + * Compares {@code count} first bytes in the arrays
> + * {@code a} and {@code b}.
> + *
> + * @param a The first array to compare.
> + * @param b The second array to compare.
> + * @param count How many bytes should be compared.
> + *
> + * @return {@code true} if {@code count} first bytes in arrays
> + * {@code a} and {@code b} are equal.
> + */
> + public static boolean arrayequals(final byte[] a,
> + final byte[] b,
> + final int count) {
> + for (int i = 0; i < count; i++) {
> + if (a[i] != b[i]) {
> + return false;
> + }
> + }
> + return true;
> + }
> +
> + /**
> + * The input stream from which data is read.
> + */
> + private final InputStream input;
> +
> + /**
> + * The length of the boundary token plus the leading {@code CRLF--}.
> + */
> + private int boundaryLength;
> +
> + /**
> + * The amount of data, in bytes, that must be kept in the buffer in order
> + * to detect delimiters reliably.
> + */
> + private final int keepRegion;
> +
> + /**
> * The byte sequence that partitions the stream.
> */
> private final byte[] boundary;
> @@ -264,31 +594,23 @@ public class MultipartStream {
> */
> private final ProgressNotifier notifier;
>
> - // ----------------------------------------------------------- Constructors
> -
> /**
> - * <p> Constructs a {@code MultipartStream} with a custom size buffer.
> - *
> - * <p> Note that the buffer must be at least big enough to contain the
> - * boundary string, plus 4 characters for CR/LF and double dash, plus at
> - * least one byte of data. Too small a buffer size setting will degrade
> - * performance.
> + * Constructs a {@code MultipartStream} with a custom size buffer.
> + * <p>
> + * Note that the buffer must be at least big enough to contain the boundary string, plus 4 characters for CR/LF and
> + * double dash, plus at least one byte of data. Too small a buffer size setting will degrade performance.
> *
> * @param input The {@code InputStream} to serve as a data source.
> - * @param boundary The token used for dividing the stream into
> - * {@code encapsulations}.
> + * @param boundary The token used for dividing the stream into {@code encapsulations}.
> * @param bufSize The size of the buffer to be used, in bytes.
> - * @param pNotifier The notifier, which is used for calling the
> - * progress listener, if any.
> + * @param notifier The notifier, which is used for calling the progress listener, if any.
> *
> * @throws IllegalArgumentException If the buffer size is too small
> *
> * @since FileUpload 1.3.1
> */
> - public MultipartStream(final InputStream input,
> - final byte[] boundary,
> - final int bufSize,
> - final ProgressNotifier pNotifier) {
> + public MultipartStream(final InputStream input, final byte[] boundary, final int bufSize,
> + final ProgressNotifier notifier) {
>
> if (boundary == null) {
> throw new IllegalArgumentException("boundary may not be null");
> @@ -297,93 +619,138 @@ public class MultipartStream {
> // body-data tokens.
> this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
> if (bufSize < this.boundaryLength + 1) {
> - throw new IllegalArgumentException(
> - "The buffer size specified for the MultipartStream is too small");
> + throw new IllegalArgumentException("The buffer size specified for the MultipartStream is too small");
> }
> -
> this.input = input;
> this.bufSize = Math.max(bufSize, boundaryLength * 2);
> this.buffer = new byte[this.bufSize];
> - this.notifier = pNotifier;
> -
> + this.notifier = notifier;
> this.boundary = new byte[this.boundaryLength];
> this.boundaryTable = new int[this.boundaryLength + 1];
> this.keepRegion = this.boundary.length;
> -
> - System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
> - BOUNDARY_PREFIX.length);
> - System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
> - boundary.length);
> + System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, BOUNDARY_PREFIX.length);
> + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length);
> computeBoundaryTable();
> -
> head = 0;
> tail = 0;
> }
>
> /**
> - * <p> Constructs a {@code MultipartStream} with a default size buffer.
> + * Constructs a {@code MultipartStream} with a default size buffer.
> *
> * @param input The {@code InputStream} to serve as a data source.
> * @param boundary The token used for dividing the stream into
> * {@code encapsulations}.
> - * @param pNotifier An object for calling the progress listener, if any.
> + * @param notifier An object for calling the progress listener, if any.
> *
> *
> * @see #MultipartStream(InputStream, byte[], int, ProgressNotifier)
> */
> - public MultipartStream(final InputStream input,
> - final byte[] boundary,
> - final ProgressNotifier pNotifier) {
> - this(input, boundary, DEFAULT_BUFSIZE, pNotifier);
> + public MultipartStream(final InputStream input, final byte[] boundary, final ProgressNotifier notifier) {
> + this(input, boundary, DEFAULT_BUFSIZE, notifier);
> }
>
> - // --------------------------------------------------------- Public methods
> -
> /**
> - * Retrieves the character encoding used when reading the headers of an
> - * individual part. When not specified, or {@code null}, the platform
> - * default encoding is used.
> - *
> - * @return The encoding used to read part headers.
> + * Compute the table used for Knuth-Morris-Pratt search algorithm.
> */
> - public String getHeaderEncoding() {
> - return headerEncoding;
> - }
> + private void computeBoundaryTable() {
> + int position = 2;
> + int candidate = 0;
> +
> + boundaryTable[0] = -1;
> + boundaryTable[1] = 0;
> +
> + while (position <= boundaryLength) {
> + if (boundary[position - 1] == boundary[candidate]) {
> + boundaryTable[position] = candidate + 1;
> + candidate++;
> + position++;
> + } else if (candidate > 0) {
> + candidate = boundaryTable[candidate];
> + } else {
> + boundaryTable[position] = 0;
> + position++;
> + }
> + }
> + }
>
> /**
> - * Specifies the character encoding to be used when reading the headers of
> - * individual parts. When not specified, or {@code null}, the platform
> - * default encoding is used.
> + * Reads {@code body-data} from the current {@code encapsulation} and discards it.
> + * <p>
> + * Use this method to skip encapsulations you don't need or don't understand.
> *
> - * @param encoding The encoding used to read part headers.
> + * @return The amount of data discarded.
> + *
> + * @throws MalformedStreamException if the stream ends unexpectedly.
> + * @throws IOException if an i/o error occurs.
> */
> - public void setHeaderEncoding(final String encoding) {
> - headerEncoding = encoding;
> + public int discardBodyData() throws MalformedStreamException, IOException {
> + return readBodyData(null);
> }
>
> /**
> - * Reads a byte from the {@code buffer}, and refills it as
> - * necessary.
> - *
> - * @return The next byte from the input stream.
> + * Searches for the {@code boundary} in the {@code buffer}
> + * region delimited by {@code head} and {@code tail}.
> *
> - * @throws IOException if there is no more data available.
> + * @return The position of the boundary found, counting from the
> + * beginning of the {@code buffer}, or {@code -1} if
> + * not found.
> */
> - public byte readByte() throws IOException {
> - // Buffer depleted ?
> - if (head == tail) {
> - head = 0;
> - // Refill.
> - tail = input.read(buffer, head, bufSize);
> - if (tail == -1) {
> - // No more data available.
> - throw new IOException("No more data is available");
> + protected int findSeparator() {
> +
> + int bufferPos = head;
> + int tablePos = 0;
> +
> + while (bufferPos < tail) {
> + while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
> + tablePos = boundaryTable[tablePos];
> }
> - if (notifier != null) {
> - notifier.noteBytesRead(tail);
> + bufferPos++;
> + tablePos++;
> + if (tablePos == boundaryLength) {
> + return bufferPos - boundaryLength;
> }
> }
> - return buffer[head++];
> + return -1;
> + }
> +
> + /**
> + * Gets the character encoding used when reading the headers of an
> + * individual part. When not specified, or {@code null}, the platform
> + * default encoding is used.
> + *
> + * @return The encoding used to read part headers.
> + */
> + public String getHeaderEncoding() {
> + return headerEncoding;
> + }
> +
> + /**
> + * Creates a new {@link ItemInputStream}.
> + * @return A new instance of {@link ItemInputStream}.
> + */
> + public ItemInputStream newInputStream() {
> + return new ItemInputStream();
> + }
> +
> + /**
> + * Reads {@code body-data} from the current {@code encapsulation} and writes its contents into the output
> + * {@code Stream}.
> + * <p>
> + * Arbitrary large amounts of data can be processed by this method using a constant size buffer. (see {@link
> + * #MultipartStream(InputStream,byte[],int, MultipartStream.ProgressNotifier) constructor}).
> + *
> + * @param output The {@code Stream} to write data into. May be null, in which case this method is equivalent to
> + * {@link #discardBodyData()}.
> + *
> + * @return the amount of data written.
> + *
> + * @throws MalformedStreamException if the stream ends unexpectedly.
> + * @throws IOException if an i/o error occurs.
> + */
> + public int readBodyData(final OutputStream output)
> + throws MalformedStreamException, IOException {
> + return (int) Streams.copy(newInputStream(), output, false); // Streams.copy closes the input stream
> }
>
> /**
> @@ -434,69 +801,37 @@ public class MultipartStream {
> }
>
> /**
> - * <p>Changes the boundary token used for partitioning the stream.
> - *
> - * <p>This method allows single pass processing of nested multipart
> - * streams.
> - *
> - * <p>The boundary token of the nested stream is {@code required}
> - * to be of the same length as the boundary token in parent stream.
> - *
> - * <p>Restoring the parent stream boundary token after processing of a
> - * nested stream is left to the application.
> + * Reads a byte from the {@code buffer}, and refills it as
> + * necessary.
> *
> - * @param boundary The boundary to be used for parsing of the nested
> - * stream.
> + * @return The next byte from the input stream.
> *
> - * @throws IllegalBoundaryException if the {@code boundary}
> - * has a different length than the one
> - * being currently parsed.
> - */
> - public void setBoundary(final byte[] boundary)
> - throws IllegalBoundaryException {
> - if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) {
> - throw new IllegalBoundaryException(
> - "The length of a boundary token cannot be changed");
> - }
> - System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
> - boundary.length);
> - computeBoundaryTable();
> - }
> -
> - /**
> - * Compute the table used for Knuth-Morris-Pratt search algorithm.
> + * @throws IOException if there is no more data available.
> */
> - private void computeBoundaryTable() {
> - int position = 2;
> - int candidate = 0;
> -
> - boundaryTable[0] = -1;
> - boundaryTable[1] = 0;
> -
> - while (position <= boundaryLength) {
> - if (boundary[position - 1] == boundary[candidate]) {
> - boundaryTable[position] = candidate + 1;
> - candidate++;
> - position++;
> - } else if (candidate > 0) {
> - candidate = boundaryTable[candidate];
> - } else {
> - boundaryTable[position] = 0;
> - position++;
> + public byte readByte() throws IOException {
> + // Buffer depleted ?
> + if (head == tail) {
> + head = 0;
> + // Refill.
> + tail = input.read(buffer, head, bufSize);
> + if (tail == -1) {
> + // No more data available.
> + throw new IOException("No more data is available");
> + }
> + if (notifier != null) {
> + notifier.noteBytesRead(tail);
> }
> }
> + return buffer[head++];
> }
>
> /**
> - * <p>Reads the {@code header-part} of the current
> - * {@code encapsulation}.
> - *
> - * <p>Headers are returned verbatim to the input stream, including the
> - * trailing {@code CRLF} marker. Parsing is left to the
> - * application.
> - *
> - * <p><strong>TODO</strong> allow limiting maximum header size to
> - * protect against abuse.
> + * Reads the {@code header-part} of the current {@code encapsulation}.
> + * <p>
> + * Headers are returned verbatim to the input stream, including the trailing {@code CRLF} marker. Parsing is left to
> + * the application.
> + * <p>
> + * <strong>TODO</strong> allow limiting maximum header size to protect against abuse.
> *
> * @return The {@code header-part} of the current encapsulation.
> *
> @@ -518,7 +853,8 @@ public class MultipartStream {
> } catch (final IOException e) {
> throw new MalformedStreamException("Stream ended unexpectedly");
> }
> - if (++size > HEADER_PART_SIZE_MAX) {
> + size++;
> + if (size > HEADER_PART_SIZE_MAX) {
> throw new MalformedStreamException(String.format(
> "Header section has more than %s bytes (maybe it is not properly terminated)",
> Integer.valueOf(HEADER_PART_SIZE_MAX)));
> @@ -530,7 +866,6 @@ public class MultipartStream {
> }
> baos.write(b);
> }
> -
> String headers;
> if (headerEncoding != null) {
> try {
> @@ -543,56 +878,41 @@ public class MultipartStream {
> } else {
> headers = baos.toString();
> }
> -
> return headers;
> }
>
> /**
> - * <p>Reads {@code body-data} from the current
> - * {@code encapsulation} and writes its contents into the
> - * output {@code Stream}.
> + * Changes the boundary token used for partitioning the stream.
> + * <p>
> + * This method allows single pass processing of nested multipart streams.
> + * <p>
> + * The boundary token of the nested stream is {@code required} to be of the same length as the boundary token in
> + * parent stream.
> + * <p>
> + * Restoring the parent stream boundary token after processing of a nested stream is left to the application.
> *
> - * <p>Arbitrary large amounts of data can be processed by this
> - * method using a constant size buffer. (see {@link
> - * #MultipartStream(InputStream,byte[],int,
> - * MultipartStream.ProgressNotifier) constructor}).
> + * @param boundary The boundary to be used for parsing of the nested stream.
> *
> - * @param output The {@code Stream} to write data into. May
> - * be null, in which case this method is equivalent
> - * to {@link #discardBodyData()}.
> - *
> - * @return the amount of data written.
> - *
> - * @throws MalformedStreamException if the stream ends unexpectedly.
> - * @throws IOException if an i/o error occurs.
> - */
> - public int readBodyData(final OutputStream output)
> - throws MalformedStreamException, IOException {
> - return (int) Streams.copy(newInputStream(), output, false); // N.B. Streams.copy closes the input stream
> - }
> -
> - /**
> - * Creates a new {@link ItemInputStream}.
> - * @return A new instance of {@link ItemInputStream}.
> + * @throws IllegalBoundaryException if the {@code boundary} has a different length than the one being currently
> + * parsed.
> */
> - public ItemInputStream newInputStream() {
> - return new ItemInputStream();
> + public void setBoundary(final byte[] boundary) throws IllegalBoundaryException {
> + if (boundary.length != boundaryLength - BOUNDARY_PREFIX.length) {
> + throw new IllegalBoundaryException("The length of a boundary token cannot be changed");
> + }
> + System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length);
> + computeBoundaryTable();
> }
>
> /**
> - * <p> Reads {@code body-data} from the current
> - * {@code encapsulation} and discards it.
> - *
> - * <p>Use this method to skip encapsulations you don't need or don't
> - * understand.
> - *
> - * @return The amount of data discarded.
> + * Specifies the character encoding to be used when reading the headers of
> + * individual parts. When not specified, or {@code null}, the platform
> + * default encoding is used.
> *
> - * @throws MalformedStreamException if the stream ends unexpectedly.
> - * @throws IOException if an i/o error occurs.
> + * @param encoding The encoding used to read part headers.
> */
> - public int discardBodyData() throws MalformedStreamException, IOException {
> - return readBodyData(null);
> + public void setHeaderEncoding(final String encoding) {
> + headerEncoding = encoding;
> }
>
> /**
> @@ -627,361 +947,4 @@ public class MultipartStream {
> }
> }
>
> - /**
> - * Compares {@code count} first bytes in the arrays
> - * {@code a} and {@code b}.
> - *
> - * @param a The first array to compare.
> - * @param b The second array to compare.
> - * @param count How many bytes should be compared.
> - *
> - * @return {@code true} if {@code count} first bytes in arrays
> - * {@code a} and {@code b} are equal.
> - */
> - public static boolean arrayequals(final byte[] a,
> - final byte[] b,
> - final int count) {
> - for (int i = 0; i < count; i++) {
> - if (a[i] != b[i]) {
> - return false;
> - }
> - }
> - return true;
> - }
> -
> - /**
> - * Searches for the {@code boundary} in the {@code buffer}
> - * region delimited by {@code head} and {@code tail}.
> - *
> - * @return The position of the boundary found, counting from the
> - * beginning of the {@code buffer}, or {@code -1} if
> - * not found.
> - */
> - protected int findSeparator() {
> -
> - int bufferPos = this.head;
> - int tablePos = 0;
> -
> - while (bufferPos < this.tail) {
> - while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
> - tablePos = boundaryTable[tablePos];
> - }
> - bufferPos++;
> - tablePos++;
> - if (tablePos == boundaryLength) {
> - return bufferPos - boundaryLength;
> - }
> - }
> - return -1;
> - }
> -
> - /**
> - * Thrown to indicate that the input stream fails to follow the
> - * required syntax.
> - */
> - public static class MalformedStreamException extends IOException {
> -
> - /**
> - * The UID to use when serializing this instance.
> - */
> - private static final long serialVersionUID = 6466926458059796677L;
> -
> - /**
> - * Constructs a {@code MalformedStreamException} with no
> - * detail message.
> - */
> - public MalformedStreamException() {
> - }
> -
> - /**
> - * Constructs an {@code MalformedStreamException} with
> - * the specified detail message.
> - *
> - * @param message The detail message.
> - */
> - public MalformedStreamException(final String message) {
> - super(message);
> - }
> -
> - }
> -
> - /**
> - * Thrown upon attempt of setting an invalid boundary token.
> - */
> - public static class IllegalBoundaryException extends IOException {
> -
> - /**
> - * The UID to use when serializing this instance.
> - */
> - private static final long serialVersionUID = -161533165102632918L;
> -
> - /**
> - * Constructs an {@code IllegalBoundaryException} with no
> - * detail message.
> - */
> - public IllegalBoundaryException() {
> - }
> -
> - /**
> - * Constructs an {@code IllegalBoundaryException} with
> - * the specified detail message.
> - *
> - * @param message The detail message.
> - */
> - public IllegalBoundaryException(final String message) {
> - super(message);
> - }
> -
> - }
> -
> - /**
> - * An {@link InputStream} for reading an items contents.
> - */
> - public class ItemInputStream extends InputStream implements Closeable {
> -
> - /**
> - * The number of bytes, which have been read so far.
> - */
> - private long total;
> -
> - /**
> - * The number of bytes, which must be hold, because
> - * they might be a part of the boundary.
> - */
> - private int pad;
> -
> - /**
> - * The current offset in the buffer.
> - */
> - private int pos;
> -
> - /**
> - * Whether the stream is already closed.
> - */
> - private boolean closed;
> -
> - /**
> - * Creates a new instance.
> - */
> - ItemInputStream() {
> - findSeparator();
> - }
> -
> - /**
> - * Called for finding the separator.
> - */
> - private void findSeparator() {
> - pos = MultipartStream.this.findSeparator();
> - if (pos == -1) {
> - if (tail - head > keepRegion) {
> - pad = keepRegion;
> - } else {
> - pad = tail - head;
> - }
> - }
> - }
> -
> - /**
> - * Returns the number of bytes, which have been read
> - * by the stream.
> - *
> - * @return Number of bytes, which have been read so far.
> - */
> - public long getBytesRead() {
> - return total;
> - }
> -
> - /**
> - * Returns the number of bytes, which are currently
> - * available, without blocking.
> - *
> - * @throws IOException An I/O error occurs.
> - * @return Number of bytes in the buffer.
> - */
> - @Override
> - public int available() throws IOException {
> - if (pos == -1) {
> - return tail - head - pad;
> - }
> - return pos - head;
> - }
> -
> - /**
> - * Offset when converting negative bytes to integers.
> - */
> - private static final int BYTE_POSITIVE_OFFSET = 256;
> -
> - /**
> - * Returns the next byte in the stream.
> - *
> - * @return The next byte in the stream, as a non-negative
> - * integer, or -1 for EOF.
> - * @throws IOException An I/O error occurred.
> - */
> - @Override
> - public int read() throws IOException {
> - if (closed) {
> - throw new FileItemStream.ItemSkippedException();
> - }
> - if (available() == 0 && makeAvailable() == 0) {
> - return -1;
> - }
> - ++total;
> - final int b = buffer[head++];
> - if (b >= 0) {
> - return b;
> - }
> - return b + BYTE_POSITIVE_OFFSET;
> - }
> -
> - /**
> - * Reads bytes into the given buffer.
> - *
> - * @param b The destination buffer, where to write to.
> - * @param off Offset of the first byte in the buffer.
> - * @param len Maximum number of bytes to read.
> - * @return Number of bytes, which have been actually read,
> - * or -1 for EOF.
> - * @throws IOException An I/O error occurred.
> - */
> - @Override
> - public int read(final byte[] b, final int off, final int len) throws IOException {
> - if (closed) {
> - throw new FileItemStream.ItemSkippedException();
> - }
> - if (len == 0) {
> - return 0;
> - }
> - int res = available();
> - if (res == 0) {
> - res = makeAvailable();
> - if (res == 0) {
> - return -1;
> - }
> - }
> - res = Math.min(res, len);
> - System.arraycopy(buffer, head, b, off, res);
> - head += res;
> - total += res;
> - return res;
> - }
> -
> - /**
> - * Closes the input stream.
> - *
> - * @throws IOException An I/O error occurred.
> - */
> - @Override
> - public void close() throws IOException {
> - close(false);
> - }
> -
> - /**
> - * Closes the input stream.
> - *
> - * @param pCloseUnderlying Whether to close the underlying stream
> - * (hard close)
> - * @throws IOException An I/O error occurred.
> - */
> - public void close(final boolean pCloseUnderlying) throws IOException {
> - if (closed) {
> - return;
> - }
> - if (pCloseUnderlying) {
> - closed = true;
> - input.close();
> - } else {
> - for (;;) {
> - int av = available();
> - if (av == 0) {
> - av = makeAvailable();
> - if (av == 0) {
> - break;
> - }
> - }
> - skip(av);
> - }
> - }
> - closed = true;
> - }
> -
> - /**
> - * Skips the given number of bytes.
> - *
> - * @param bytes Number of bytes to skip.
> - * @return The number of bytes, which have actually been
> - * skipped.
> - * @throws IOException An I/O error occurred.
> - */
> - @Override
> - public long skip(final long bytes) throws IOException {
> - if (closed) {
> - throw new FileItemStream.ItemSkippedException();
> - }
> - int av = available();
> - if (av == 0) {
> - av = makeAvailable();
> - if (av == 0) {
> - return 0;
> - }
> - }
> - final long res = Math.min(av, bytes);
> - head += res;
> - return res;
> - }
> -
> - /**
> - * Attempts to read more data.
> - *
> - * @return Number of available bytes
> - * @throws IOException An I/O error occurred.
> - */
> - private int makeAvailable() throws IOException {
> - if (pos != -1) {
> - return 0;
> - }
> -
> - // Move the data to the beginning of the buffer.
> - total += tail - head - pad;
> - System.arraycopy(buffer, tail - pad, buffer, 0, pad);
> -
> - // Refill buffer with new data.
> - head = 0;
> - tail = pad;
> -
> - for (;;) {
> - final int bytesRead = input.read(buffer, tail, bufSize - tail);
> - if (bytesRead == -1) {
> - // The last pad amount is left in the buffer.
> - // Boundary can't be in there so signal an error
> - // condition.
> - final String msg = "Stream ended unexpectedly";
> - throw new MalformedStreamException(msg);
> - }
> - if (notifier != null) {
> - notifier.noteBytesRead(bytesRead);
> - }
> - tail += bytesRead;
> -
> - findSeparator();
> - final int av = available();
> -
> - if (av > 0 || pos != -1) {
> - return av;
> - }
> - }
> - }
> -
> - /**
> - * Returns, whether the stream is closed.
> - *
> - * @return True, if the stream is closed, otherwise false.
> - */
> - @Override
> - public boolean isClosed() {
> - return closed;
> - }
> -
> - }
> -
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
> index 91a97aeae0..4404d9f719 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/ParameterParser.java
> @@ -40,32 +40,32 @@ public class ParameterParser {
> /**
> * String to be parsed.
> */
> - private char[] chars = null;
> + private char[] chars;
>
> /**
> * Current position in the string.
> */
> - private int pos = 0;
> + private int pos;
>
> /**
> * Maximum position in the string.
> */
> - private int len = 0;
> + private int len;
>
> /**
> * Start of a token.
> */
> - private int i1 = 0;
> + private int i1;
>
> /**
> * End of a token.
> */
> - private int i2 = 0;
> + private int i2;
>
> /**
> * Whether names stored in the map should be converted to lower case.
> */
> - private boolean lowerCaseNames = false;
> + private boolean lowerCaseNames;
>
> /**
> * Default ParameterParser constructor.
> @@ -73,16 +73,6 @@ public class ParameterParser {
> public ParameterParser() {
> }
>
> - /**
> - * Are there any characters left to parse?
> - *
> - * @return {@code true} if there are unparsed characters,
> - * {@code false} otherwise.
> - */
> - private boolean hasChar() {
> - return this.pos < this.len;
> - }
> -
> /**
> * A helper method to process the parsed token. This method removes
> * leading and trailing blanks as well as enclosing quotation marks,
> @@ -94,18 +84,18 @@ public class ParameterParser {
> */
> private String getToken(final boolean quoted) {
> // Trim leading white spaces
> - while ((i1 < i2) && (Character.isWhitespace(chars[i1]))) {
> + while (i1 < i2 && Character.isWhitespace(chars[i1])) {
> i1++;
> }
> // Trim trailing white spaces
> - while ((i2 > i1) && (Character.isWhitespace(chars[i2 - 1]))) {
> + while (i2 > i1 && Character.isWhitespace(chars[i2 - 1])) {
> i2--;
> }
> // Strip away quotation marks if necessary
> if (quoted
> - && ((i2 - i1) >= 2)
> - && (chars[i1] == '"')
> - && (chars[i2 - 1] == '"')) {
> + && i2 - i1 >= 2
> + && chars[i1] == '"'
> + && chars[i2 - 1] == '"') {
> i1++;
> i2--;
> }
> @@ -116,6 +106,28 @@ public class ParameterParser {
> return result;
> }
>
> + /**
> + * Are there any characters left to parse?
> + *
> + * @return {@code true} if there are unparsed characters,
> + * {@code false} otherwise.
> + */
> + private boolean hasChar() {
> + return pos < len;
> + }
> +
> + /**
> + * Returns {@code true} if parameter names are to be converted to lower
> + * case when name/value pairs are parsed.
> + *
> + * @return {@code true} if parameter names are to be
> + * converted to lower case when name/value pairs are parsed.
> + * Otherwise returns {@code false}
> + */
> + public boolean isLowerCaseNames() {
> + return lowerCaseNames;
> + }
> +
> /**
> * Tests if the given character is present in the array of characters.
> *
> @@ -137,83 +149,83 @@ public class ParameterParser {
> }
>
> /**
> - * Parses out a token until any of the given terminators
> - * is encountered.
> + * Extracts a map of name/value pairs from the given array of
> + * characters. Names are expected to be unique.
> *
> - * @param terminators the array of terminating characters. Any of these
> - * characters when encountered signify the end of the token
> + * @param charArray the array of characters that contains a sequence of
> + * name/value pairs
> + * @param separator the name/value pairs separator
> *
> - * @return the token
> + * @return a map of name/value pairs
> */
> - private String parseToken(final char[] terminators) {
> - char ch;
> - i1 = pos;
> - i2 = pos;
> - while (hasChar()) {
> - ch = chars[pos];
> - if (isOneOf(ch, terminators)) {
> - break;
> - }
> - i2++;
> - pos++;
> + public Map<String, String> parse(final char[] charArray, final char separator) {
> + if (charArray == null) {
> + return new HashMap<>();
> }
> - return getToken(false);
> + return parse(charArray, 0, charArray.length, separator);
> }
>
> /**
> - * Parses out a token until any of the given terminators
> - * is encountered outside the quotation marks.
> + * Extracts a map of name/value pairs from the given array of characters. Names are expected to be unique.
> *
> - * @param terminators the array of terminating characters. Any of these
> - * characters when encountered outside the quotation marks signify the end
> - * of the token
> + * @param charArray the array of characters that contains a sequence of name/value pairs
> + * @param offset the initial offset.
> + * @param length the length.
> + * @param separator the name/value pairs separator
> *
> - * @return the token
> + * @return a map of name/value pairs
> */
> - private String parseQuotedToken(final char[] terminators) {
> - char ch;
> - i1 = pos;
> - i2 = pos;
> - boolean quoted = false;
> - boolean charEscaped = false;
> + public Map<String, String> parse(final char[] charArray, final int offset, final int length, final char separator) {
> + if (charArray == null) {
> + return new HashMap<>();
> + }
> + final HashMap<String, String> params = new HashMap<>();
> + chars = charArray.clone();
> + pos = offset;
> + len = length;
> while (hasChar()) {
> - ch = chars[pos];
> - if (!quoted && isOneOf(ch, terminators)) {
> - break;
> + String paramName = parseToken(new char[] { '=', separator });
> + String paramValue = null;
> + if (hasChar() && charArray[pos] == '=') {
> + pos++; // skip '='
> + paramValue = parseQuotedToken(new char[] { separator });
> + if (paramValue != null) {
> + try {
> + paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)
> + : MimeUtility.decodeText(paramValue);
> + } catch (final UnsupportedEncodingException e) {
> + // let's keep the original value in this case
> + }
> + }
> }
> - if (!charEscaped && ch == '"') {
> - quoted = !quoted;
> + if (hasChar() && charArray[pos] == separator) {
> + pos++; // skip separator
> + }
> + if (paramName != null && !paramName.isEmpty()) {
> + paramName = RFC2231Utility.stripDelimiter(paramName);
> + if (lowerCaseNames) {
> + paramName = paramName.toLowerCase(Locale.ROOT);
> + }
> + params.put(paramName, paramValue);
> }
> - charEscaped = (!charEscaped && ch == '\\');
> - i2++;
> - pos++;
> -
> }
> - return getToken(true);
> + return params;
> }
>
> /**
> - * Returns {@code true} if parameter names are to be converted to lower
> - * case when name/value pairs are parsed.
> + * Extracts a map of name/value pairs from the given string. Names are
> + * expected to be unique.
> *
> - * @return {@code true} if parameter names are to be
> - * converted to lower case when name/value pairs are parsed.
> - * Otherwise returns {@code false}
> - */
> - public boolean isLowerCaseNames() {
> - return this.lowerCaseNames;
> - }
> -
> - /**
> - * Sets the flag if parameter names are to be converted to lower case when
> - * name/value pairs are parsed.
> + * @param str the string that contains a sequence of name/value pairs
> + * @param separator the name/value pairs separator
> *
> - * @param b {@code true} if parameter names are to be
> - * converted to lower case when name/value pairs are parsed.
> - * {@code false} otherwise.
> + * @return a map of name/value pairs
> */
> - public void setLowerCaseNames(final boolean b) {
> - this.lowerCaseNames = b;
> + public Map<String, String> parse(final String str, final char separator) {
> + if (str == null) {
> + return new HashMap<>();
> + }
> + return parse(str.toCharArray(), separator);
> }
>
> /**
> @@ -245,96 +257,71 @@ public class ParameterParser {
> }
>
> /**
> - * Extracts a map of name/value pairs from the given string. Names are
> - * expected to be unique.
> + * Parses out a token until any of the given terminators
> + * is encountered outside the quotation marks.
> *
> - * @param str the string that contains a sequence of name/value pairs
> - * @param separator the name/value pairs separator
> + * @param terminators the array of terminating characters. Any of these
> + * characters when encountered outside the quotation marks signify the end
> + * of the token
> *
> - * @return a map of name/value pairs
> + * @return the token
> */
> - public Map<String, String> parse(final String str, final char separator) {
> - if (str == null) {
> - return new HashMap<>();
> + private String parseQuotedToken(final char[] terminators) {
> + char ch;
> + i1 = pos;
> + i2 = pos;
> + boolean quoted = false;
> + boolean charEscaped = false;
> + while (hasChar()) {
> + ch = chars[pos];
> + if (!quoted && isOneOf(ch, terminators)) {
> + break;
> + }
> + if (!charEscaped && ch == '"') {
> + quoted = !quoted;
> + }
> + charEscaped = !charEscaped && ch == '\\';
> + i2++;
> + pos++;
> +
> }
> - return parse(str.toCharArray(), separator);
> + return getToken(true);
> }
>
> /**
> - * Extracts a map of name/value pairs from the given array of
> - * characters. Names are expected to be unique.
> + * Parses out a token until any of the given terminators
> + * is encountered.
> *
> - * @param charArray the array of characters that contains a sequence of
> - * name/value pairs
> - * @param separator the name/value pairs separator
> + * @param terminators the array of terminating characters. Any of these
> + * characters when encountered signify the end of the token
> *
> - * @return a map of name/value pairs
> + * @return the token
> */
> - public Map<String, String> parse(final char[] charArray, final char separator) {
> - if (charArray == null) {
> - return new HashMap<>();
> + private String parseToken(final char[] terminators) {
> + char ch;
> + i1 = pos;
> + i2 = pos;
> + while (hasChar()) {
> + ch = chars[pos];
> + if (isOneOf(ch, terminators)) {
> + break;
> + }
> + i2++;
> + pos++;
> }
> - return parse(charArray, 0, charArray.length, separator);
> + return getToken(false);
> }
>
> /**
> - * Extracts a map of name/value pairs from the given array of
> - * characters. Names are expected to be unique.
> - *
> - * @param charArray the array of characters that contains a sequence of
> - * name/value pairs
> - * @param offset - the initial offset.
> - * @param length - the length.
> - * @param separator the name/value pairs separator
> + * Sets the flag if parameter names are to be converted to lower case when
> + * name/value pairs are parsed.
> *
> - * @return a map of name/value pairs
> + * @param b {@code true} if parameter names are to be
> + * converted to lower case when name/value pairs are parsed.
> + * {@code false} otherwise.
> */
> - public Map<String, String> parse(
> - final char[] charArray,
> - final int offset,
> - final int length,
> - final char separator) {
> -
> - if (charArray == null) {
> - return new HashMap<>();
> - }
> - final HashMap<String, String> params = new HashMap<>();
> - this.chars = charArray.clone();
> - this.pos = offset;
> - this.len = length;
> -
> - String paramName;
> - String paramValue;
> - while (hasChar()) {
> - paramName = parseToken(new char[] {
> - '=', separator });
> - paramValue = null;
> - if (hasChar() && (charArray[pos] == '=')) {
> - pos++; // skip '='
> - paramValue = parseQuotedToken(new char[] {
> - separator });
> -
> - if (paramValue != null) {
> - try {
> - paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)
> - : MimeUtility.decodeText(paramValue);
> - } catch (final UnsupportedEncodingException e) {
> - // let's keep the original value in this case
> - }
> - }
> - }
> - if (hasChar() && (charArray[pos] == separator)) {
> - pos++; // skip separator
> - }
> - if ((paramName != null) && !paramName.isEmpty()) {
> - paramName = RFC2231Utility.stripDelimiter(paramName);
> - if (this.lowerCaseNames) {
> - paramName = paramName.toLowerCase(Locale.ENGLISH);
> - }
> - params.put(paramName, paramValue);
> - }
> - }
> - return params;
> + public void setLowerCaseNames(final boolean b) {
> + lowerCaseNames = b;
> }
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
> index 931c99d1d8..c7f49c9095 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/ProgressListener.java
> @@ -25,13 +25,13 @@ public interface ProgressListener {
> /**
> * Updates the listeners status information.
> *
> - * @param pBytesRead The total number of bytes, which have been read
> + * @param bytesRead The total number of bytes, which have been read
> * so far.
> - * @param pContentLength The total number of bytes, which are being
> + * @param contentLength The total number of bytes, which are being
> * read. May be -1, if this number is unknown.
> - * @param pItems The number of the field, which is currently being
> + * @param items The number of the field, which is currently being
> * read. (0 = no item so far, 1 = first item is being read, ...)
> */
> - void update(long pBytesRead, long pContentLength, int pItems);
> + void update(long bytesRead, long contentLength, int items);
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
> index e35c1ea237..22fe3c18ab 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java
> @@ -37,259 +37,175 @@ import org.apache.tomcat.util.http.fileupload.FileItem;
> import org.apache.tomcat.util.http.fileupload.FileItemHeaders;
> import org.apache.tomcat.util.http.fileupload.FileUploadException;
> import org.apache.tomcat.util.http.fileupload.IOUtils;
> +import org.apache.tomcat.util.http.fileupload.InvalidFileNameException;
> import org.apache.tomcat.util.http.fileupload.ParameterParser;
> import org.apache.tomcat.util.http.fileupload.util.Streams;
>
> /**
> - * <p> The default implementation of the
> - * {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface.
> - *
> - * <p> After retrieving an instance of this class from a {@link
> - * org.apache.tomcat.util.http.fileupload.FileUpload FileUpload} instance (see
> + * The default implementation of the {@link org.apache.tomcat.util.http.fileupload.FileItem FileItem} interface.
> + * <p>
> + * After retrieving an instance of this class from a {@link DiskFileItemFactory} instance (see
> * {@link org.apache.tomcat.util.http.fileupload.FileUpload
> - * #parseRequest(org.apache.tomcat.util.http.fileupload.RequestContext)}), you
> - * may either request all contents of file at once using {@link #get()} or
> - * request an {@link java.io.InputStream InputStream} with
> - * {@link #getInputStream()} and process the file without attempting to load
> - * it into memory, which may come handy with large files.
> - *
> - * <p>Temporary files, which are created for file items, will be deleted when
> - * the associated request is recycled.</p>
> + * #parseRequest(org.apache.tomcat.util.http.fileupload.RequestContext)}), you may either request all contents of file
> + * at once using {@link #get()} or request an {@link java.io.InputStream InputStream} with {@link #getInputStream()} and
> + * process the file without attempting to load it into memory, which may come handy with large files.
> + * <p>
> + * Temporary files, which are created for file items, will be deleted when the associated request is recycled.
> + * </p>
> *
> * @since FileUpload 1.1
> */
> -public class DiskFileItem
> - implements FileItem {
> +public class DiskFileItem implements FileItem {
>
> - // ----------------------------------------------------- Manifest constants
> + /**
> + * Counter used in unique identifier generation.
> + */
> + private static final AtomicInteger COUNTER = new AtomicInteger(0);
>
> /**
> - * Default content charset to be used when no explicit charset
> - * parameter is provided by the sender. Media subtypes of the
> - * "text" type are defined to have a default charset value of
> - * "ISO-8859-1" when received via HTTP.
> + * Default content charset to be used when no explicit charset parameter is provided by the sender. Media subtypes
> + * of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP.
> */
> public static final String DEFAULT_CHARSET = "ISO-8859-1";
>
> - // ----------------------------------------------------------- Data members
> -
> /**
> * UID used in unique file name generation.
> */
> - private static final String UID =
> - UUID.randomUUID().toString().replace('-', '_');
> + private static final String UID = UUID.randomUUID().toString().replace('-', '_');
>
> /**
> - * Counter used in unique identifier generation.
> + * Returns an identifier that is unique within the class loader used to load this class, but does not have
> + * random-like appearance.
> + *
> + * @return A String with the non-random looking instance identifier.
> */
> - private static final AtomicInteger COUNTER = new AtomicInteger(0);
> + private static String getUniqueId() {
> + final int limit = 100000000;
> + final int current = COUNTER.getAndIncrement();
> + String id = Integer.toString(current);
> +
> + // If you manage to get more than 100 million of ids, you'll
> + // start getting ids longer than 8 characters.
> + if (current < limit) {
> + id = ("00000000" + id).substring(id.length());
> + }
> + return id;
> + }
>
> /**
> - * The name of the form field as provided by the browser.
> + * Cached contents of the file.
> */
> - private String fieldName;
> + private byte[] cachedContent;
>
> /**
> - * The content type passed by the browser, or {@code null} if
> - * not defined.
> + * The content type passed by the browser, or {@code null} if not defined.
> */
> private final String contentType;
>
> /**
> - * Whether or not this item is a simple form field.
> + * Default content charset to be used when no explicit charset parameter is provided by the sender.
> */
> - private boolean isFormField;
> + private String defaultCharset = DEFAULT_CHARSET;
>
> /**
> - * The original file name in the user's file system.
> + * Output stream for this item.
> */
> - private final String fileName;
> + private transient DeferredFileOutputStream dfos;
>
> /**
> - * The size of the item, in bytes. This is used to cache the size when a
> - * file item is moved from its original location.
> + * The name of the form field as provided by the browser.
> */
> - private long size = -1;
> -
> + private String fieldName;
>
> /**
> - * The threshold above which uploads will be stored on disk.
> + * The original file name in the user's file system.
> */
> - private final int sizeThreshold;
> + private final String fileName;
>
> /**
> - * The directory in which uploaded files will be stored, if stored on disk.
> + * The file items headers.
> */
> - private final File repository;
> + private FileItemHeaders headers;
>
> /**
> - * Cached contents of the file.
> + * Whether or not this item is a simple form field.
> */
> - private byte[] cachedContent;
> + private boolean formField;
>
> /**
> - * Output stream for this item.
> + * The directory in which uploaded files will be stored, if stored on disk.
> */
> - private transient DeferredFileOutputStream dfos;
> + private final File repository;
>
> /**
> - * The temporary file to use.
> + * The size of the item, in bytes. This is used to cache the size when a file item is moved from its original
> + * location.
> */
> - private transient File tempFile;
> + private long size = -1;
>
> /**
> - * The file items headers.
> + * The threshold above which uploads will be stored on disk.
> */
> - private FileItemHeaders headers;
> + private final int sizeThreshold;
>
> /**
> - * Default content charset to be used when no explicit charset
> - * parameter is provided by the sender.
> + * The temporary file to use.
> */
> - private String defaultCharset = DEFAULT_CHARSET;
> -
> - // ----------------------------------------------------------- Constructors
> + private transient File tempFile;
>
> /**
> * Constructs a new {@code DiskFileItem} instance.
> *
> * @param fieldName The name of the form field.
> - * @param contentType The content type passed by the browser or
> - * {@code null} if not specified.
> - * @param isFormField Whether or not this item is a plain form field, as
> - * opposed to a file upload.
> - * @param fileName The original file name in the user's file system, or
> - * {@code null} if not specified.
> - * @param sizeThreshold The threshold, in bytes, below which items will be
> - * retained in memory and above which they will be
> - * stored as a file.
> - * @param repository The data repository, which is the directory in
> - * which files will be created, should the item size
> - * exceed the threshold.
> - */
> - public DiskFileItem(final String fieldName,
> - final String contentType, final boolean isFormField, final String fileName,
> - final int sizeThreshold, final File repository) {
> + * @param contentType The content type passed by the browser or {@code null} if not specified.
> + * @param isFormField Whether or not this item is a plain form field, as opposed to a file upload.
> + * @param fileName The original file name in the user's file system, or {@code null} if not specified.
> + * @param sizeThreshold The threshold, in bytes, below which items will be retained in memory and above which they
> + * will be stored as a file.
> + * @param repository The data repository, which is the directory in which files will be created, should the item
> + * size exceed the threshold.
> + */
> + public DiskFileItem(final String fieldName, final String contentType, final boolean isFormField,
> + final String fileName, final int sizeThreshold, final File repository) {
> this.fieldName = fieldName;
> this.contentType = contentType;
> - this.isFormField = isFormField;
> + this.formField = isFormField;
> this.fileName = fileName;
> this.sizeThreshold = sizeThreshold;
> this.repository = repository;
> }
>
> - // ------------------------------- Methods from javax.activation.DataSource
> -
> - /**
> - * Returns an {@link java.io.InputStream InputStream} that can be
> - * used to retrieve the contents of the file.
> - *
> - * @return An {@link java.io.InputStream InputStream} that can be
> - * used to retrieve the contents of the file.
> - *
> - * @throws IOException if an error occurs.
> - */
> - @Override
> - public InputStream getInputStream()
> - throws IOException {
> - if (!isInMemory()) {
> - return Files.newInputStream(dfos.getFile().toPath());
> - }
> -
> - if (cachedContent == null) {
> - cachedContent = dfos.getData();
> - }
> - return new ByteArrayInputStream(cachedContent);
> - }
> -
> - /**
> - * Returns the content type passed by the agent or {@code null} if
> - * not defined.
> - *
> - * @return The content type passed by the agent or {@code null} if
> - * not defined.
> - */
> - @Override
> - public String getContentType() {
> - return contentType;
> - }
> -
> - /**
> - * Returns the content charset passed by the agent or {@code null} if
> - * not defined.
> - *
> - * @return The content charset passed by the agent or {@code null} if
> - * not defined.
> - */
> - public String getCharSet() {
> - final ParameterParser parser = new ParameterParser();
> - parser.setLowerCaseNames(true);
> - // Parameter parser can handle null input
> - final Map<String, String> params = parser.parse(getContentType(), ';');
> - return params.get("charset");
> - }
> -
> - /**
> - * Returns the original file name in the client's file system.
> - *
> - * @return The original file name in the client's file system.
> - * @throws org.apache.tomcat.util.http.fileupload.InvalidFileNameException
> - * The file name contains a NUL character, which might be an indicator of
> - * a security attack. If you intend to use the file name anyways, catch
> - * the exception and use {@link
> - * org.apache.tomcat.util.http.fileupload.InvalidFileNameException#getName()}.
> - */
> - @Override
> - public String getName() {
> - return Streams.checkFileName(fileName);
> - }
> -
> - // ------------------------------------------------------- FileItem methods
> -
> /**
> - * Provides a hint as to whether or not the file contents will be read
> - * from memory.
> - *
> - * @return {@code true} if the file contents will be read
> - * from memory; {@code false} otherwise.
> + * Clears the cache.
> */
> - @Override
> - public boolean isInMemory() {
> - if (cachedContent != null) {
> - return true;
> - }
> - return dfos.isInMemory();
> + private void clear() {
> + cachedContent = null; // NOPMD
> }
>
> /**
> - * Returns the size of the file.
> - *
> - * @return The size of the file, in bytes.
> + * Deletes the underlying storage for a file item, including deleting any associated temporary disk file. This
> + * method can be used to ensure that this is done at an earlier time, thus preserving system resources.
> */
> @Override
> - public long getSize() {
> - if (size >= 0) {
> - return size;
> - }
> - if (cachedContent != null) {
> - return cachedContent.length;
> - }
> - if (dfos.isInMemory()) {
> - return dfos.getData().length;
> + public void delete() {
> + clear();
> + final File outputFile = getStoreLocation();
> + if (outputFile != null && !isInMemory() && outputFile.exists()) {
> + if (!outputFile.delete()) {
> + final String desc = "Cannot delete " + outputFile.toString();
> + throw new UncheckedIOException(desc, new IOException(desc));
> + }
> }
> - return dfos.getFile().length();
> }
>
> /**
> - * Returns the contents of the file as an array of bytes. If the
> - * contents of the file were not yet cached in memory, they will be
> - * loaded from the disk storage and cached.
> + * Gets the contents of the file as an array of bytes. If the contents of the file were not yet cached in memory,
> + * they will be loaded from the disk storage and cached.
> *
> - * @return The contents of the file as an array of bytes
> - * or {@code null} if the data cannot be read
> + * @return The contents of the file as an array of bytes or {@code null} if the data cannot be read.
> *
> * @throws UncheckedIOException if an I/O error occurs
> - * @throws ArithmeticException if the file {@code size} overflows an int
> + * @throws ArithmeticException if the file {@code size} overflows an int
> */
> @Override
> public byte[] get() throws UncheckedIOException {
> @@ -311,132 +227,43 @@ public class DiskFileItem
> }
>
> /**
> - * Returns the contents of the file as a String, using the specified
> - * encoding. This method uses {@link #get()} to retrieve the
> - * contents of the file.
> - *
> - * @param charset The charset to use.
> + * Gets the content charset passed by the agent or {@code null} if not defined.
> *
> - * @return The contents of the file, as a string.
> - *
> - * @throws UnsupportedEncodingException if the requested character
> - * encoding is not available.
> + * @return The content charset passed by the agent or {@code null} if not defined.
> */
> - @Override
> - public String getString(final String charset)
> - throws UnsupportedEncodingException, IOException {
> - return new String(get(), charset);
> + public String getCharSet() {
> + final ParameterParser parser = new ParameterParser();
> + parser.setLowerCaseNames(true);
> + // Parameter parser can handle null input
> + final Map<String,String> params = parser.parse(getContentType(), ';');
> + return params.get("charset");
> }
>
> /**
> - * Returns the contents of the file as a String, using the default
> - * character encoding. This method uses {@link #get()} to retrieve the
> - * contents of the file.
> - *
> - * <b>TODO</b> Consider making this method throw UnsupportedEncodingException.
> + * Gets the content type passed by the agent or {@code null} if not defined.
> *
> - * @return The contents of the file, as a string.
> + * @return The content type passed by the agent or {@code null} if not defined.
> */
> @Override
> - public String getString() {
> - try {
> - final byte[] rawData = get();
> - String charset = getCharSet();
> - if (charset == null) {
> - charset = defaultCharset;
> - }
> - return new String(rawData, charset);
> - } catch (final IOException e) {
> - return "";
> - }
> + public String getContentType() {
> + return contentType;
> }
>
> /**
> - * A convenience method to write an uploaded item to disk. The client code
> - * is not concerned with whether or not the item is stored in memory, or on
> - * disk in a temporary location. They just want to write the uploaded item
> - * to a file.
> - * <p>
> - * This implementation first attempts to rename the uploaded item to the
> - * specified destination file, if the item was originally written to disk.
> - * Otherwise, the data will be copied to the specified file.
> - * <p>
> - * This method is only guaranteed to work <em>once</em>, the first time it
> - * is invoked for a particular item. This is because, in the event that the
> - * method renames a temporary file, that file will no longer be available
> - * to copy or rename again at a later time.
> - *
> - * @param file The {@code File} into which the uploaded item should
> - * be stored.
> + * Gets the default charset for use when no explicit charset parameter is provided by the sender.
> *
> - * @throws Exception if an error occurs.
> - */
> - @Override
> - public void write(final File file) throws Exception {
> - if (isInMemory()) {
> - try (OutputStream fout = Files.newOutputStream(file.toPath())) {
> - fout.write(get());
> - }
> - } else {
> - final File outputFile = getStoreLocation();
> - if (outputFile == null) {
> - /*
> - * For whatever reason we cannot write the
> - * file to disk.
> - */
> - throw new FileUploadException(
> - "Cannot write uploaded file to disk!");
> - }
> - // Save the length of the file
> - size = outputFile.length();
> - /*
> - * The uploaded file is being stored on disk
> - * in a temporary location so move it to the
> - * desired file.
> - */
> - if (file.exists() && !file.delete()) {
> - throw new FileUploadException("Cannot write uploaded file to disk!");
> - }
> - if (!outputFile.renameTo(file)) {
> - BufferedInputStream in = null;
> - BufferedOutputStream out = null;
> - try {
> - in = new BufferedInputStream(new FileInputStream(outputFile));
> - out = new BufferedOutputStream(new FileOutputStream(file));
> - IOUtils.copy(in, out);
> - out.close();
> - } finally {
> - IOUtils.closeQuietly(in);
> - IOUtils.closeQuietly(out);
> - }
> - }
> - }
> - }
> -
> - /**
> - * Deletes the underlying storage for a file item, including deleting any associated temporary disk file.
> - * This method can be used to ensure that this is done at an earlier time, thus preserving system resources.
> + * @return the default charset
> */
> - @Override
> - public void delete() {
> - cachedContent = null;
> - final File outputFile = getStoreLocation();
> - if (outputFile != null && !isInMemory() && outputFile.exists()) {
> - if (!outputFile.delete()) {
> - final String desc = "Cannot delete " + outputFile.toString();
> - throw new UncheckedIOException(desc, new IOException(desc));
> - }
> - }
> + public String getDefaultCharset() {
> + return defaultCharset;
> }
>
> /**
> - * Returns the name of the field in the multipart form corresponding to
> - * this file item.
> + * Gets the name of the field in the multipart form corresponding to this file item.
> *
> * @return The name of the form field.
> *
> * @see #setFieldName(String)
> - *
> */
> @Override
> public String getFieldName() {
> @@ -444,55 +271,52 @@ public class DiskFileItem
> }
>
> /**
> - * Sets the field name used to reference this file item.
> - *
> - * @param fieldName The name of the form field.
> - *
> - * @see #getFieldName()
> + * Gets the file item headers.
> *
> + * @return The file items headers.
> */
> @Override
> - public void setFieldName(final String fieldName) {
> - this.fieldName = fieldName;
> + public FileItemHeaders getHeaders() {
> + return headers;
> }
>
> /**
> - * Determines whether or not a {@code FileItem} instance represents
> - * a simple form field.
> - *
> - * @return {@code true} if the instance represents a simple form
> - * field; {@code false} if it represents an uploaded file.
> + * Gets an {@link java.io.InputStream InputStream} that can be used to retrieve the contents of the file.
> *
> - * @see #setFormField(boolean)
> + * @return An {@link java.io.InputStream InputStream} that can be used to retrieve the contents of the file.
> *
> + * @throws IOException if an error occurs.
> */
> @Override
> - public boolean isFormField() {
> - return isFormField;
> + public InputStream getInputStream() throws IOException {
> + if (!isInMemory()) {
> + return Files.newInputStream(dfos.getFile().toPath());
> + }
> + if (cachedContent == null) {
> + cachedContent = dfos.getData();
> + }
> + return new ByteArrayInputStream(cachedContent);
> }
>
> /**
> - * Specifies whether or not a {@code FileItem} instance represents
> - * a simple form field.
> - *
> - * @param state {@code true} if the instance represents a simple form
> - * field; {@code false} if it represents an uploaded file.
> + * Gets the original file name in the client's file system.
> *
> - * @see #isFormField()
> + * @return The original file name in the client's file system.
> *
> + * @throws InvalidFileNameException The file name contains a NUL character, which might be an indicator of a
> + * security attack. If you intend to use the file name anyways, catch the
> + * exception and use
> + * {@link org.apache.tomcat.util.http.fileupload.InvalidFileNameException#getName()}.
> */
> @Override
> - public void setFormField(final boolean state) {
> - isFormField = state;
> + public String getName() {
> + return Streams.checkFileName(fileName);
> }
>
> /**
> - * Returns an {@link java.io.OutputStream OutputStream} that can
> - * be used for storing the contents of the file.
> - *
> - * @return An {@link java.io.OutputStream OutputStream} that can be used
> - * for storing the contents of the file.
> + * Gets an {@link java.io.OutputStream OutputStream} that can be used for storing the contents of the file.
> *
> + * @return An {@link java.io.OutputStream OutputStream} that can be used for storing the contents of the file.
> */
> @Override
> public OutputStream getOutputStream() {
> @@ -503,20 +327,32 @@ public class DiskFileItem
> return dfos;
> }
>
> - // --------------------------------------------------------- Public methods
> + /**
> + * Gets the size of the file.
> + *
> + * @return The size of the file, in bytes.
> + */
> + @Override
> + public long getSize() {
> + if (size >= 0) {
> + return size;
> + }
> + if (cachedContent != null) {
> + return cachedContent.length;
> + }
> + if (dfos.isInMemory()) {
> + return dfos.getData().length;
> + }
> + return dfos.getFile().length();
> + }
>
> /**
> - * Returns the {@link java.io.File} object for the {@code FileItem}'s
> - * data's temporary location on the disk. Note that for
> - * {@code FileItem}s that have their data stored in memory,
> - * this method will return {@code null}. When handling large
> - * files, you can use {@link java.io.File#renameTo(java.io.File)} to
> - * move the file to new location without copying the data, if the
> - * source and destination locations reside within the same logical
> - * volume.
> + * Gets the {@link java.io.File} object for the {@code FileItem}'s data's temporary location on the disk. Note
> + * that for {@code FileItem}s that have their data stored in memory, this method will return {@code null}. When
> + * handling large files, you can use {@link java.io.File#renameTo(java.io.File)} to move the file to new location
> + * without copying the data, if the source and destination locations reside within the same logical volume.
> *
> - * @return The data file, or {@code null} if the data is stored in
> - * memory.
> + * @return The data file, or {@code null} if the data is stored in memory.
> */
> public File getStoreLocation() {
> if (dfos == null) {
> @@ -528,16 +364,49 @@ public class DiskFileItem
> return dfos.getFile();
> }
>
> - // ------------------------------------------------------ Protected methods
> + /**
> + * Gets the contents of the file as a String, using the default character encoding. This method uses
> + * {@link #get()} to retrieve the contents of the file.
> + * <p>
> + * <strong>TODO</strong> Consider making this method throw UnsupportedEncodingException.
> + *
> + * @return The contents of the file, as a string.
> + */
> + @Override
> + public String getString() {
> + final byte[] rawData = get();
> + String charset = getCharSet();
> + if (charset == null) {
> + charset = defaultCharset;
> + }
> + try {
> + return new String(rawData, charset);
> + } catch (final UnsupportedEncodingException e) {
> + return "";
> + }
> + }
>
> /**
> - * Creates and returns a {@link java.io.File File} representing a uniquely
> - * named temporary file in the configured repository path. The lifetime of
> - * the file is tied to the lifetime of the {@code FileItem} instance;
> - * the file will be deleted when the instance is garbage collected.
> + * Gets the contents of the file as a String, using the specified encoding. This method uses {@link #get()} to
> + * retrieve the contents of the file.
> + *
> + * @param charset The charset to use.
> + *
> + * @return The contents of the file, as a string.
> + *
> + * @throws UnsupportedEncodingException if the requested character encoding is not available.
> + */
> + @Override
> + public String getString(final String charset) throws UnsupportedEncodingException {
> + return new String(get(), charset);
> + }
> +
> + /**
> + * Creates and returns a {@link java.io.File File} representing a uniquely named temporary file in the configured
> + * repository path. The lifetime of the file is tied to the lifetime of the {@code FileItem} instance; the file will
> + * be deleted when the instance is garbage collected.
> * <p>
> - * <b>Note: Subclasses that override this method must ensure that they return the
> - * same File each time.</b>
> + * <b>Note: Subclasses that override this method must ensure that they return the same File each time.</b>
> *
> * @return The {@link java.io.File File} to be used for temporary storage.
> */
> @@ -555,71 +424,138 @@ public class DiskFileItem
> return tempFile;
> }
>
> - // -------------------------------------------------------- Private methods
> -
> /**
> - * Returns an identifier that is unique within the class loader used to
> - * load this class, but does not have random-like appearance.
> + * Tests whether or not a {@code FileItem} instance represents a simple form field.
> *
> - * @return A String with the non-random looking instance identifier.
> + * @return {@code true} if the instance represents a simple form field; {@code false} if it represents an uploaded
> + * file.
> + *
> + * @see #setFormField(boolean)
> */
> - private static String getUniqueId() {
> - final int limit = 100000000;
> - final int current = COUNTER.getAndIncrement();
> - String id = Integer.toString(current);
> + @Override
> + public boolean isFormField() {
> + return formField;
> + }
>
> - // If you manage to get more than 100 million of ids, you'll
> - // start getting ids longer than 8 characters.
> - if (current < limit) {
> - id = ("00000000" + id).substring(id.length());
> + /**
> + * Provides a hint as to whether or not the file contents will be read from memory.
> + *
> + * @return {@code true} if the file contents will be read from memory; {@code false} otherwise.
> + */
> + @Override
> + public boolean isInMemory() {
> + if (cachedContent != null) {
> + return true;
> }
> - return id;
> + return dfos.isInMemory();
> }
>
> /**
> - * Returns a string representation of this object.
> + * Sets the default charset for use when no explicit charset parameter is provided by the sender.
> *
> - * @return a string representation of this object.
> + * @param charset the default charset
> + */
> + public void setDefaultCharset(final String charset) {
> + defaultCharset = charset;
> + }
> +
> + /**
> + * Sets the field name used to reference this file item.
> + *
> + * @param fieldName The name of the form field.
> + *
> + * @see #getFieldName()
> */
> @Override
> - public String toString() {
> - return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s",
> - getName(), getStoreLocation(), Long.valueOf(getSize()), Boolean.valueOf(isFormField()), getFieldName());
> + public void setFieldName(final String fieldName) {
> + this.fieldName = fieldName;
> }
>
> /**
> - * Returns the file item headers.
> - * @return The file items headers.
> + * Sets whether or not a {@code FileItem} instance represents a simple form field.
> + *
> + * @param formField {@code true} if the instance represents a simple form field; {@code false} if it represents an
> + * uploaded file.
> + *
> + * @see #isFormField()
> */
> @Override
> - public FileItemHeaders getHeaders() {
> - return headers;
> + public void setFormField(final boolean formField) {
> + this.formField = formField;
> }
>
> /**
> * Sets the file item headers.
> - * @param pHeaders The file items headers.
> + *
> + * @param headers The file items headers.
> */
> @Override
> - public void setHeaders(final FileItemHeaders pHeaders) {
> - headers = pHeaders;
> + public void setHeaders(final FileItemHeaders headers) {
> + this.headers = headers;
> }
>
> /**
> - * Returns the default charset for use when no explicit charset
> - * parameter is provided by the sender.
> - * @return the default charset
> + * Returns a string representation of this object.
> + *
> + * @return a string representation of this object.
> */
> - public String getDefaultCharset() {
> - return defaultCharset;
> + @Override
> + public String toString() {
> + return String.format("name=%s, StoreLocation=%s, size=%s bytes, isFormField=%s, FieldName=%s", getName(),
> + getStoreLocation(), Long.valueOf(getSize()), Boolean.valueOf(isFormField()), getFieldName());
> }
>
> /**
> - * Sets the default charset for use when no explicit charset
> - * parameter is provided by the sender.
> - * @param charset the default charset
> + * A convenience method to write an uploaded item to disk. The client code is not concerned with whether or not the
> + * item is stored in memory, or on disk in a temporary location. They just want to write the uploaded item to a
> + * file.
> + * <p>
> + * This implementation first attempts to rename the uploaded item to the specified destination file, if the item was
> + * originally written to disk. Otherwise, the data will be copied to the specified file.
> + * <p>
> + * This method is only guaranteed to work <em>once</em>, the first time it is invoked for a particular item. This is
> + * because, in the event that the method renames a temporary file, that file will no longer be available to copy or
> + * rename again at a later time.
> + *
> + * @param file The {@code File} into which the uploaded item should be stored.
> + *
> + * @throws Exception if an error occurs.
> */
> - public void setDefaultCharset(final String charset) {
> - defaultCharset = charset;
> + @Override
> + public void write(final File file) throws Exception {
> + if (isInMemory()) {
> + try (OutputStream fout = Files.newOutputStream(file.toPath())) {
> + fout.write(get());
> + }
> + } else {
> + final File outputFile = getStoreLocation();
> + if (outputFile == null) {
> + /*
> + * For whatever reason we cannot write the file to disk.
> + */
> + throw new FileUploadException("Cannot write uploaded file to disk!");
> + }
> + // Save the length of the file
> + size = outputFile.length();
> + /*
> + * The uploaded file is being stored on disk in a temporary location so move it to the desired file.
> + */
> + if (file.exists() && !file.delete()) {
> + throw new FileUploadException("Cannot write uploaded file to disk!");
> + }
> + if (!outputFile.renameTo(file)) {
> + BufferedInputStream in = null;
> + BufferedOutputStream out = null;
> + try {
> + in = new BufferedInputStream(new FileInputStream(outputFile));
> + out = new BufferedOutputStream(new FileOutputStream(file));
> + IOUtils.copy(in, out);
> + out.close();
> + } finally {
> + IOUtils.closeQuietly(in);
> + IOUtils.closeQuietly(out);
> + }
> + }
> + }
> }
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
> index 8b0073da3f..06817f599b 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItemFactory.java
> @@ -39,7 +39,7 @@ import org.apache.tomcat.util.http.fileupload.FileItemFactory;
> * {@code System.getProperty("java.io.tmpdir")}.</li>
> * </ul>
> * <p>
> - * <b>NOTE</b>: Files are created in the system default temp directory with
> + * <strong>NOTE</strong>: Files are created in the system default temp directory with
> * predictable names. This means that a local attacker with write access to that
> * directory can perform a TOUTOC attack to replace any uploaded file with a
> * file of the attackers choice. The implications of this will depend on how the
> @@ -58,15 +58,11 @@ import org.apache.tomcat.util.http.fileupload.FileItemFactory;
> */
> public class DiskFileItemFactory implements FileItemFactory {
>
> - // ----------------------------------------------------- Manifest constants
> -
> /**
> * The default threshold above which uploads will be stored on disk.
> */
> public static final int DEFAULT_SIZE_THRESHOLD = 10240;
>
> - // ----------------------------------------------------- Instance Variables
> -
> /**
> * The directory in which uploaded files will be stored, if stored on disk.
> */
> @@ -83,8 +79,6 @@ public class DiskFileItemFactory implements FileItemFactory {
> */
> private String defaultCharset = DiskFileItem.DEFAULT_CHARSET;
>
> - // ----------------------------------------------------------- Constructors
> -
> /**
> * Constructs an unconfigured instance of this class. The resulting factory
> * may be configured by calling the appropriate setter methods.
> @@ -108,36 +102,53 @@ public class DiskFileItemFactory implements FileItemFactory {
> this.repository = repository;
> }
>
> - // ------------------------------------------------------------- Properties
> -
> /**
> - * Returns the directory used to temporarily store files that are larger
> - * than the configured size threshold.
> - *
> - * @return The directory in which temporary files will be located.
> + * Create a new {@link DiskFileItem}
> + * instance from the supplied parameters and the local factory
> + * configuration.
> *
> - * @see #setRepository(java.io.File)
> + * @param fieldName The name of the form field.
> + * @param contentType The content type of the form field.
> + * @param isFormField {@code true} if this is a plain form field;
> + * {@code false} otherwise.
> + * @param fileName The name of the uploaded file, if any, as supplied
> + * by the browser or other client.
> *
> + * @return The newly created file item.
> */
> - public File getRepository() {
> - return repository;
> + @Override
> + public FileItem createItem(final String fieldName, final String contentType,
> + final boolean isFormField, final String fileName) {
> + final DiskFileItem result = new DiskFileItem(fieldName, contentType,
> + isFormField, fileName, sizeThreshold, repository);
> + result.setDefaultCharset(defaultCharset);
> + return result;
> }
>
> /**
> - * Sets the directory used to temporarily store files that are larger
> + * Gets the default charset for use when no explicit charset
> + * parameter is provided by the sender.
> + * @return the default charset
> + */
> + public String getDefaultCharset() {
> + return defaultCharset;
> + }
> +
> + /**
> + * Gets the directory used to temporarily store files that are larger
> * than the configured size threshold.
> *
> - * @param repository The directory in which temporary files will be located.
> + * @return The directory in which temporary files will be located.
> *
> - * @see #getRepository()
> + * @see #setRepository(java.io.File)
> *
> */
> - public void setRepository(final File repository) {
> - this.repository = repository;
> + public File getRepository() {
> + return repository;
> }
>
> /**
> - * Returns the size threshold beyond which files are written directly to
> + * Gets the size threshold beyond which files are written directly to
> * disk. The default value is 10240 bytes.
> *
> * @return The size threshold, in bytes.
> @@ -149,57 +160,37 @@ public class DiskFileItemFactory implements FileItemFactory {
> }
>
> /**
> - * Sets the size threshold beyond which files are written directly to disk.
> - *
> - * @param sizeThreshold The size threshold, in bytes.
> - *
> - * @see #getSizeThreshold()
> + * Sets the default charset for use when no explicit charset
> + * parameter is provided by the sender.
> *
> + * @param charset the default charset
> */
> - public void setSizeThreshold(final int sizeThreshold) {
> - this.sizeThreshold = sizeThreshold;
> + public void setDefaultCharset(final String charset) {
> + this.defaultCharset = charset;
> }
>
> - // --------------------------------------------------------- Public Methods
> -
> /**
> - * Create a new {@link DiskFileItem}
> - * instance from the supplied parameters and the local factory
> - * configuration.
> + * Sets the directory used to temporarily store files that are larger
> + * than the configured size threshold.
> *
> - * @param fieldName The name of the form field.
> - * @param contentType The content type of the form field.
> - * @param isFormField {@code true} if this is a plain form field;
> - * {@code false} otherwise.
> - * @param fileName The name of the uploaded file, if any, as supplied
> - * by the browser or other client.
> + * @param repository The directory in which temporary files will be located.
> + *
> + * @see #getRepository()
> *
> - * @return The newly created file item.
> - */
> - @Override
> - public FileItem createItem(final String fieldName, final String contentType,
> - final boolean isFormField, final String fileName) {
> - final DiskFileItem result = new DiskFileItem(fieldName, contentType,
> - isFormField, fileName, sizeThreshold, repository);
> - result.setDefaultCharset(defaultCharset);
> - return result;
> - }
> -
> - /**
> - * Returns the default charset for use when no explicit charset
> - * parameter is provided by the sender.
> - * @return the default charset
> */
> - public String getDefaultCharset() {
> - return defaultCharset;
> + public void setRepository(final File repository) {
> + this.repository = repository;
> }
>
> /**
> - * Sets the default charset for use when no explicit charset
> - * parameter is provided by the sender.
> - * @param pCharset the default charset
> + * Sets the size threshold beyond which files are written directly to disk.
> + *
> + * @param sizeThreshold The size threshold, in bytes.
> + *
> + * @see #getSizeThreshold()
> + *
> */
> - public void setDefaultCharset(final String pCharset) {
> - defaultCharset = pCharset;
> + public void setSizeThreshold(final int sizeThreshold) {
> + this.sizeThreshold = sizeThreshold;
> }
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/package-info.java b/java/org/apache/tomcat/util/http/fileupload/package-info.java
> index dee2bb19fc..91c25bc5f0 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/package-info.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/package-info.java
> @@ -16,8 +16,8 @@
> */
>
> /**
> - * <p><b>NOTE:</b> This code has been copied from commons-fileupload trunk
> - * 1.3 and commons-io 1.4 and package renamed to avoid clashes with
> + * <p><b>NOTE:</b> This code has been copied from commons-fileupload
> + * 1.x and commons-io 1.4 and package renamed to avoid clashes with
> * any web apps that may wish to use these libraries.
> * </p>
> * <p>
> diff --git a/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
> index 90f05d1f78..eb6c62a3a1 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/servlet/ServletRequestContext.java
> @@ -32,15 +32,11 @@ import org.apache.tomcat.util.http.fileupload.UploadContext;
> */
> public class ServletRequestContext implements UploadContext {
>
> - // ----------------------------------------------------- Instance Variables
> -
> /**
> * The request for which the context is being provided.
> */
> private final HttpServletRequest request;
>
> - // ----------------------------------------------------------- Constructors
> -
> /**
> * Construct a context for this request.
> *
> @@ -50,7 +46,22 @@ public class ServletRequestContext implements UploadContext {
> this.request = request;
> }
>
> - // --------------------------------------------------------- Public Methods
> + /**
> + * Retrieve the content length of the request.
> + *
> + * @return The content length of the request.
> + * @since FileUpload 1.3
> + */
> + @Override
> + public long contentLength() {
> + long size;
> + try {
> + size = Long.parseLong(request.getHeader(FileUploadBase.CONTENT_LENGTH));
> + } catch (final NumberFormatException e) {
> + size = request.getContentLength();
> + }
> + return size;
> + }
>
> /**
> * Retrieve the character encoding for the request.
> @@ -72,23 +83,6 @@ public class ServletRequestContext implements UploadContext {
> return request.getContentType();
> }
>
> - /**
> - * Retrieve the content length of the request.
> - *
> - * @return The content length of the request.
> - * @since FileUpload 1.3
> - */
> - @Override
> - public long contentLength() {
> - long size;
> - try {
> - size = Long.parseLong(request.getHeader(FileUploadBase.CONTENT_LENGTH));
> - } catch (final NumberFormatException e) {
> - size = request.getContentLength();
> - }
> - return size;
> - }
> -
> /**
> * Retrieve the input stream for the request.
> *
> @@ -109,7 +103,7 @@ public class ServletRequestContext implements UploadContext {
> @Override
> public String toString() {
> return String.format("ContentLength=%s, ContentType=%s",
> - Long.valueOf(this.contentLength()), this.getContentType());
> + Long.valueOf(contentLength()), getContentType());
> }
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
> index a70345ff43..d4e20ece7f 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/FileItemHeadersImpl.java
> @@ -45,9 +45,29 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
> */
> private final Map<String, List<String>> headerNameToValueListMap = new LinkedHashMap<>();
>
> + /**
> + * Constructs a new instance.
> + */
> + public FileItemHeadersImpl() {
> + // empty
> + }
> +
> + /**
> + * Method to add header values to this instance.
> + *
> + * @param name name of this header
> + * @param value value of this header
> + */
> + public synchronized void addHeader(final String name, final String value) {
> + final String nameLower = name.toLowerCase(Locale.ROOT);
> + final List<String> headerValueList = headerNameToValueListMap.
> + computeIfAbsent(nameLower, k -> new ArrayList<>());
> + headerValueList.add(value);
> + }
> +
> @Override
> public String getHeader(final String name) {
> - final String nameLower = name.toLowerCase(Locale.ENGLISH);
> + final String nameLower = name.toLowerCase(Locale.ROOT);
> final List<String> headerValueList = headerNameToValueListMap.get(nameLower);
> if (null == headerValueList) {
> return null;
> @@ -62,7 +82,7 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
>
> @Override
> public Iterator<String> getHeaders(final String name) {
> - final String nameLower = name.toLowerCase(Locale.ENGLISH);
> + final String nameLower = name.toLowerCase(Locale.ROOT);
> List<String> headerValueList = headerNameToValueListMap.get(nameLower);
> if (null == headerValueList) {
> headerValueList = Collections.emptyList();
> @@ -70,17 +90,4 @@ public class FileItemHeadersImpl implements FileItemHeaders, Serializable {
> return headerValueList.iterator();
> }
>
> - /**
> - * Method to add header values to this instance.
> - *
> - * @param name name of this header
> - * @param value value of this header
> - */
> - public synchronized void addHeader(final String name, final String value) {
> - final String nameLower = name.toLowerCase(Locale.ENGLISH);
> - final List<String> headerValueList = headerNameToValueListMap.
> - computeIfAbsent(nameLower, k -> new ArrayList<>());
> - headerValueList.add(value);
> - }
> -
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
> index def25ca125..add304c16a 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/LimitedInputStream.java
> @@ -45,26 +45,14 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
> * Creates a new instance.
> *
> * @param inputStream The input stream, which shall be limited.
> - * @param pSizeMax The limit; no more than this number of bytes
> + * @param sizeMax The limit; no more than this number of bytes
> * shall be returned by the source stream.
> */
> - public LimitedInputStream(final InputStream inputStream, final long pSizeMax) {
> + public LimitedInputStream(final InputStream inputStream, final long sizeMax) {
> super(inputStream);
> - sizeMax = pSizeMax;
> + this.sizeMax = sizeMax;
> }
>
> - /**
> - * Called to indicate, that the input streams limit has
> - * been exceeded.
> - *
> - * @param pSizeMax The input streams limit, in bytes.
> - * @param pCount The actual number of bytes.
> - * @throws IOException The called method is expected
> - * to raise an IOException.
> - */
> - protected abstract void raiseError(long pSizeMax, long pCount)
> - throws IOException;
> -
> /**
> * Called to check, whether the input streams
> * limit is reached.
> @@ -77,6 +65,43 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
> }
> }
>
> + /**
> + * Closes this input stream and releases any system resources
> + * associated with the stream.
> + * This
> + * method simply performs {@code in.close()}.
> + *
> + * @throws IOException if an I/O error occurs.
> + * @see java.io.FilterInputStream#in
> + */
> + @Override
> + public void close() throws IOException {
> + closed = true;
> + super.close();
> + }
> +
> + /**
> + * Returns, whether this stream is already closed.
> + *
> + * @return True, if the stream is closed, otherwise false.
> + * @throws IOException An I/O error occurred.
> + */
> + @Override
> + public boolean isClosed() throws IOException {
> + return closed;
> + }
> +
> + /**
> + * Called to indicate, that the input streams limit has
> + * been exceeded.
> + *
> + * @param sizeMax The input streams limit, in bytes.
> + * @param count The actual number of bytes.
> + * @throws IOException The called method is expected
> + * to raise an IOException.
> + */
> + protected abstract void raiseError(long sizeMax, long count) throws IOException;
> +
> /**
> * Reads the next byte of data from this input stream. The value
> * byte is returned as an {@code int} in the range
> @@ -137,30 +162,4 @@ public abstract class LimitedInputStream extends FilterInputStream implements Cl
> return res;
> }
>
> - /**
> - * Returns, whether this stream is already closed.
> - *
> - * @return True, if the stream is closed, otherwise false.
> - * @throws IOException An I/O error occurred.
> - */
> - @Override
> - public boolean isClosed() throws IOException {
> - return closed;
> - }
> -
> - /**
> - * Closes this input stream and releases any system resources
> - * associated with the stream.
> - * This
> - * method simply performs {@code in.close()}.
> - *
> - * @throws IOException if an I/O error occurs.
> - * @see java.io.FilterInputStream#in
> - */
> - @Override
> - public void close() throws IOException {
> - closed = true;
> - super.close();
> - }
> -
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/Streams.java b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
> index 7289d06665..ecf82c25d0 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/Streams.java
> @@ -27,14 +27,6 @@ import org.apache.tomcat.util.http.fileupload.InvalidFileNameException;
> */
> public final class Streams {
>
> - /**
> - * Private constructor, to prevent instantiation.
> - * This class has only static methods.
> - */
> - private Streams() {
> - // Does nothing
> - }
> -
> /**
> * Default buffer size for use in
> * {@link #copy(InputStream, OutputStream, boolean)}.
> @@ -42,10 +34,41 @@ public final class Streams {
> public static final int DEFAULT_BUFFER_SIZE = 8192;
>
> /**
> + * Checks, whether the given file name is valid in the sense,
> + * that it doesn't contain any NUL characters. If the file name
> + * is valid, it will be returned without any modifications. Otherwise,
> + * an {@link InvalidFileNameException} is raised.
> + *
> + * @param fileName The file name to check
> + * @return Unmodified file name, if valid.
> + * @throws InvalidFileNameException The file name was found to be invalid.
> + */
> + public static String checkFileName(final String fileName) {
> + if (fileName != null && fileName.indexOf('\u0000') != -1) {
> + // fileName.replace("\u0000", "\\0")
> + final StringBuilder sb = new StringBuilder();
> + for (int i = 0; i < fileName.length(); i++) {
> + final char c = fileName.charAt(i);
> + switch (c) {
> + case 0:
> + sb.append("\\0");
> + break;
> + default:
> + sb.append(c);
> + break;
> + }
> + }
> + throw new InvalidFileNameException(fileName,
> + "Invalid file name: " + sb);
> + }
> + return fileName;
> + }
> +
> + /**
> * Copies the contents of the given {@link InputStream}
> * to the given {@link OutputStream}. Shortcut for
> * <pre>
> - * copy(pInputStream, pOutputStream, new byte[8192]);
> + * copy(pInputStream, outputStream, new byte[8192]);
> * </pre>
> *
> * @param inputStream The input stream, which is being read.
> @@ -116,35 +139,12 @@ public final class Streams {
> }
> }
>
> - /**
> - * Checks, whether the given file name is valid in the sense,
> - * that it doesn't contain any NUL characters. If the file name
> - * is valid, it will be returned without any modifications. Otherwise,
> - * an {@link InvalidFileNameException} is raised.
> - *
> - * @param fileName The file name to check
> - * @return Unmodified file name, if valid.
> - * @throws InvalidFileNameException The file name was found to be invalid.
> + /**
> + * Private constructor, to prevent instantiation.
> + * This class has only static methods.
> */
> - public static String checkFileName(final String fileName) {
> - if (fileName != null && fileName.indexOf('\u0000') != -1) {
> - // pFileName.replace("\u0000", "\\0")
> - final StringBuilder sb = new StringBuilder();
> - for (int i = 0; i < fileName.length(); i++) {
> - final char c = fileName.charAt(i);
> - switch (c) {
> - case 0:
> - sb.append("\\0");
> - break;
> - default:
> - sb.append(c);
> - break;
> - }
> - }
> - throw new InvalidFileNameException(fileName,
> - "Invalid file name: " + sb);
> - }
> - return fileName;
> + private Streams() {
> + // Does nothing
> }
>
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
> index 7debaf915e..461b00b354 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/MimeUtility.java
> @@ -75,13 +75,6 @@ public final class MimeUtility {
> MIME2JAVA.put("x-us-ascii", "ISO-8859-1");
> }
>
> - /**
> - * Hidden constructor, this class must not be instantiated.
> - */
> - private MimeUtility() {
> - // do nothing
> - }
> -
> /**
> * Decode a string of text obtained from a mail header into
> * its proper form. The text generally will consist of a
> @@ -207,7 +200,7 @@ public final class MimeUtility {
> }
>
> // pull out the character set information (this is the MIME name at this point).
> - final String charset = word.substring(2, charsetPos).toLowerCase(Locale.ENGLISH);
> + final String charset = word.substring(2, charsetPos).toLowerCase(Locale.ROOT);
>
> // now pull out the encoding token the same way.
> final int encodingPos = word.indexOf('?', charsetPos + 1);
> @@ -266,7 +259,7 @@ public final class MimeUtility {
> return null;
> }
>
> - final String mappedCharset = MIME2JAVA.get(charset.toLowerCase(Locale.ENGLISH));
> + final String mappedCharset = MIME2JAVA.get(charset.toLowerCase(Locale.ROOT));
> // if there is no mapping, then the original name is used. Many of the MIME character set
> // names map directly back into Java. The reverse isn't necessarily true.
> if (mappedCharset == null) {
> @@ -275,4 +268,11 @@ public final class MimeUtility {
> return mappedCharset;
> }
>
> + /**
> + * Hidden constructor, this class must not be instantiated.
> + */
> + private MimeUtility() {
> + // do nothing
> + }
> +
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
> index 72e9896fe6..a49fe432a7 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/ParseException.java
> @@ -17,6 +17,8 @@
> package org.apache.tomcat.util.http.fileupload.util.mime;
>
> /**
> + * Thrown for a parsing problem.
> + *
> * @since FileUpload 1.3
> */
> final class ParseException extends Exception {
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
> index 6a94f3c202..d1caac1a57 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/QuotedPrintableDecoder.java
> @@ -25,17 +25,35 @@ import java.io.OutputStream;
> final class QuotedPrintableDecoder {
>
> /**
> - * The shift value required to create the upper nibble
> - * from the first of 2 byte values converted from ascii hex.
> + * Carriage return character '{@value}'.
> */
> - private static final int UPPER_NIBBLE_SHIFT = Byte.SIZE / 2;
> + private static final char CR = '\r';
>
> /**
> - * Hidden constructor, this class must not be instantiated.
> + * Equal character '{@value}'.
> */
> - private QuotedPrintableDecoder() {
> - // do nothing
> - }
> + private static final char EQUAL = '=';
> +
> + /**
> + * Line feed character '{@value}'.
> + */
> + private static final char LF = '\n';
> +
> + /**
> + * Space character '{@value}'.
> + */
> + private static final char SP = ' ';
> +
> + /**
> + * Underscore character '{@value}'.
> + */
> + private static final char UNDERSCORE = '_';
> +
> + /**
> + * The shift value required to create the upper nibble
> + * from the first of 2 byte values converted from ASCII hex.
> + */
> + private static final int UPPER_NIBBLE_SHIFT = Byte.SIZE / 2;
>
> /**
> * Decode the encoded byte data writing it to the given output stream.
> @@ -56,9 +74,9 @@ final class QuotedPrintableDecoder {
> final byte ch = data[off++];
>
> // space characters were translated to '_' on encode, so we need to translate them back.
> - if (ch == '_') {
> - out.write(' ');
> - } else if (ch == '=') {
> + if (ch == UNDERSCORE) {
> + out.write(SP);
> + } else if (ch == EQUAL) {
> // we found an encoded character. Reduce the 3 char sequence to one.
> // but first, make sure we have two characters to work with.
> if (off + 1 >= endOffset) {
> @@ -69,8 +87,8 @@ final class QuotedPrintableDecoder {
> final byte b2 = data[off++];
>
> // we've found an encoded carriage return. The next char needs to be a newline
> - if (b1 == '\r') {
> - if (b2 != '\n') {
> + if (b1 == CR) {
> + if (b2 != LF) {
> throw new IOException("Invalid quoted printable encoding; CR must be followed by LF");
> }
> // this was a soft linebreak inserted by the encoding. We just toss this away
> @@ -79,7 +97,7 @@ final class QuotedPrintableDecoder {
> // this is a hex pair we need to convert back to a single byte.
> final int c1 = hexToBinary(b1);
> final int c2 = hexToBinary(b2);
> - out.write((c1 << UPPER_NIBBLE_SHIFT) | c2);
> + out.write(c1 << UPPER_NIBBLE_SHIFT | c2);
> // 3 bytes in, one byte out
> bytesWritten++;
> }
> @@ -96,7 +114,7 @@ final class QuotedPrintableDecoder {
> /**
> * Convert a hex digit to the binary value it represents.
> *
> - * @param b the ascii hex byte to convert (0-0, A-F, a-f)
> + * @param b the ASCII hex byte to convert (0-0, A-F, a-f)
> * @return the int value of the hex byte, 0-15
> * @throws IOException if the byte is not a valid hex digit.
> */
> @@ -109,4 +127,11 @@ final class QuotedPrintableDecoder {
> return i;
> }
>
> + /**
> + * Hidden constructor, this class must not be instantiated.
> + */
> + private QuotedPrintableDecoder() {
> + // do nothing
> + }
> +
> }
> diff --git a/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
> index 8a522a313f..c1a7ba0d0c 100644
> --- a/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
> +++ b/java/org/apache/tomcat/util/http/fileupload/util/mime/RFC2231Utility.java
> @@ -30,6 +30,12 @@ import java.io.UnsupportedEncodingException;
> * @see <a href="https://tools.ietf.org/html/rfc5987">RFC 5987</a>
> */
> public final class RFC2231Utility {
> +
> + /**
> + * Percent character '{@value}'.
> + */
> + private static final char PERCENT = '%';
> +
> /**
> * The Hexadecimal values char array.
> */
> @@ -55,54 +61,19 @@ public final class RFC2231Utility {
> }
> }
>
> - /**
> - * Private constructor so that no instances can be created. This class
> - * contains only static utility methods.
> - */
> - private RFC2231Utility() {
> - }
> -
> - /**
> - * Checks if Asterisk (*) at the end of parameter name to indicate,
> - * if it has charset and language information to decode the value.
> - * @param paramName The parameter, which is being checked.
> - * @return {@code true}, if encoded as per RFC 2231, {@code false} otherwise
> - */
> - public static boolean hasEncodedValue(final String paramName) {
> - if (paramName != null) {
> - return paramName.lastIndexOf('*') == (paramName.length() - 1);
> - }
> - return false;
> - }
> -
> - /**
> - * If {@code paramName} has Asterisk (*) at the end, it will be stripped off,
> - * else the passed value will be returned.
> - * @param paramName The parameter, which is being inspected.
> - * @return stripped {@code paramName} of Asterisk (*), if RFC2231 encoded
> - */
> - public static String stripDelimiter(final String paramName) {
> - if (hasEncodedValue(paramName)) {
> - final StringBuilder paramBuilder = new StringBuilder(paramName);
> - paramBuilder.deleteCharAt(paramName.lastIndexOf('*'));
> - return paramBuilder.toString();
> - }
> - return paramName;
> - }
> -
> /**
> * Decode a string of text obtained from a HTTP header as per RFC 2231
> * <p>
> - * <b>Eg 1.</b> {@code us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A}
> + * <strong>Eg 1.</strong> {@code us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A}
> * will be decoded to {@code This is ***fun***}
> * <p>
> - * <b>Eg 2.</b> {@code iso-8859-1'en'%A3%20rate}
> + * <strong>Eg 2.</strong> {@code iso-8859-1'en'%A3%20rate}
> * will be decoded to {@code £ rate}
> * <p>
> - * <b>Eg 3.</b> {@code UTF-8''%c2%a3%20and%20%e2%82%ac%20rates}
> + * <strong>Eg 3.</strong> {@code UTF-8''%c2%a3%20and%20%e2%82%ac%20rates}
> * will be decoded to {@code £ and € rates}
> *
> - * @param encodedText - Text to be decoded has a format of {@code <charset>'<language>'<encoded_value>}
> + * @param encodedText Text to be decoded has a format of {@code <charset>'<language>'<encoded_value>}
> * and ASCII only
> * @return Decoded text based on charset encoding
> * @throws UnsupportedEncodingException The requested character set wasn't found.
> @@ -124,8 +95,9 @@ public final class RFC2231Utility {
> }
>
> /**
> - * Convert {@code text} to their corresponding Hex value.
> - * @param text - ASCII text input
> + * Converts {@code text} to their corresponding Hex value.
> + *
> + * @param text ASCII text input
> * @return Byte array of characters decoded from ASCII table
> */
> private static byte[] fromHex(final String text) {
> @@ -133,13 +105,13 @@ public final class RFC2231Utility {
> final ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
> for (int i = 0; i < text.length();) {
> final char c = text.charAt(i++);
> - if (c == '%') {
> + if (c == PERCENT) {
> if (i > text.length() - 2) {
> break; // unterminated sequence
> }
> final byte b1 = HEX_DECODE[text.charAt(i++) & MASK];
> final byte b2 = HEX_DECODE[text.charAt(i++) & MASK];
> - out.write((b1 << shift) | b2);
> + out.write(b1 << shift | b2);
> } else {
> out.write((byte) c);
> }
> @@ -151,4 +123,39 @@ public final class RFC2231Utility {
> // good enough for standard values
> return mimeCharset;
> }
> +
> + /**
> + * Checks if Asterisk (*) at the end of parameter name to indicate,
> + * if it has charset and language information to decode the value.
> + * @param paramName The parameter, which is being checked.
> + * @return {@code true}, if encoded as per RFC 2231, {@code false} otherwise
> + */
> + public static boolean hasEncodedValue(final String paramName) {
> + if (paramName != null) {
> + return paramName.lastIndexOf('*') == paramName.length() - 1;
> + }
> + return false;
> + }
> +
> + /**
> + * If {@code paramName} has Asterisk (*) at the end, it will be stripped off,
> + * else the passed value will be returned.
> + * @param paramName The parameter, which is being inspected.
> + * @return stripped {@code paramName} of Asterisk (*), if RFC2231 encoded
> + */
> + public static String stripDelimiter(final String paramName) {
> + if (hasEncodedValue(paramName)) {
> + final StringBuilder paramBuilder = new StringBuilder(paramName);
> + paramBuilder.deleteCharAt(paramName.lastIndexOf('*'));
> + return paramBuilder.toString();
> + }
> + return paramName;
> + }
> +
> + /**
> + * Private constructor so that no instances can be created. This class
> + * contains only static utility methods.
> + */
> + private RFC2231Utility() {
> + }
> }
> diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
> index 9666aa852c..4aa28e1847 100644
> --- a/webapps/docs/changelog.xml
> +++ b/webapps/docs/changelog.xml
> @@ -277,6 +277,10 @@
> <update>
> Update Tomcat Native to 2.0.9. (markt)
> </update>
> + <update>
> + Update the internal fork of Apache Commons FileUpload to 7a8c324
> + (2023-06-03, 1.x-SNAPSHOT). (markt)
> + </update>
> </changelog>
> </subsection>
> </section>
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org