diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java index 252cbf0d479..c0ac4d1542e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java @@ -355,8 +355,8 @@ public class MultipartHttpMessageConverter implements SmartHttpMessageConverter< PartGenerator partGenerator = new PartGenerator( this.maxInMemorySize, this.maxDiskUsagePerPart, this.maxParts, getTempDirectory()); - MultipartParser parser = new MultipartParser(this.maxHeadersSize, 2 * 1024); - parser.parse(message.getBody(), boundary, headersCharset, partGenerator); + MultipartParser.parse( + message.getBody(), boundary, headersCharset, this.maxHeadersSize, 2 * 1024, partGenerator); return partGenerator.getParts(); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartParser.java index 4d9b3deb45a..59839b1f5fb 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartParser.java @@ -42,492 +42,114 @@ import org.springframework.http.converter.HttpMessageConversionException; * and signals them to the {@link PartListener}. * * @author Brian Clozel + * @author Rossen Stoyanchev * @author Arjen Poutsma * @since 7.1 */ final class MultipartParser { + private static final byte[] CR_LF = {'\r', '\n'}; + + private static final byte HYPHEN = '-'; + + private static final byte[] TWO_HYPHENS = {HYPHEN, HYPHEN}; + + private static final String HEADER_ENTRY_SEPARATOR = "\\r\\n"; + private static final Log logger = LogFactory.getLog(MultipartParser.class); + private final byte[] boundary; + + private final Charset headersCharset; + private final int maxHeadersSize; - private final int bufferSize; + private final PartListener listener; - /** - * Create a new multipart parser instance. - * - * @param maxHeadersSize the maximum buffered header size - * @param bufferSize the size of the reading buffer - */ - MultipartParser(int maxHeadersSize, int bufferSize) { + private State state; + + + private MultipartParser( + byte[] boundary, Charset headersCharset, int maxHeadersSize, PartListener listener) { + + this.boundary = boundary; + this.headersCharset = headersCharset; this.maxHeadersSize = maxHeadersSize; - this.bufferSize = bufferSize; + this.listener = listener; + this.state = new PreambleState(); + } + + + void handleData(DataBuffer dataBuffer) { + this.state.data(dataBuffer); + } + + private void changeState(State newState, @Nullable DataBuffer remainder) { + if (logger.isTraceEnabled()) { + logger.trace("Changed state: " + this.state + " -> " + newState); + } + this.state.dispose(); + this.state = newState; + if (remainder != null) { + if (remainder.readableByteCount() > 0) { + newState.data(remainder); + } + else { + DataBufferUtils.release(remainder); + } + } } /** - * Parses the given stream of bytes into events published to the {@link PartListener}. - * @param input the input stream - * @param boundary the multipart boundary, as found in the {@code Content-Type} header - * @param headersCharset the charset to use for decoding headers - * @param listener a listener for parsed tokens + * Concatenates the given array of byte arrays. */ - public void parse(InputStream input, byte[] boundary, Charset headersCharset, PartListener listener) { + private static byte[] concat(byte[]... byteArrays) { + int len = 0; + for (byte[] byteArray : byteArrays) { + len += byteArray.length; + } + byte[] result = new byte[len]; + len = 0; + for (byte[] byteArray : byteArrays) { + System.arraycopy(byteArray, 0, result, len, byteArray.length); + len += byteArray.length; + } + return result; + } - InternalParser internalParser = new InternalParser(boundary, headersCharset, listener); + /** + * Parse the given stream of bytes into events published to the given {@link PartListener}. + * @param input the input stream + * @param boundary the multipart boundary, as found in the {@code Content-Type} header + * @param headersCharset the charset to use for decoding headers + * @param maxHeadersSize the maximum buffered header size + * @param bufferSize the size of the reading buffer + * @param listener a listener for parsed tokens + */ + public static void parse(InputStream input, byte[] boundary, Charset headersCharset, + int maxHeadersSize, int bufferSize, PartListener listener) { + + MultipartParser parser = new MultipartParser(boundary, headersCharset, maxHeadersSize, listener); try { while (true) { - byte[] read = input.readNBytes(this.bufferSize); + byte[] read = input.readNBytes(bufferSize); if (read.length == 0) { break; } - internalParser.state.data(DefaultDataBufferFactory.sharedInstance.wrap(read)); + parser.handleData(DefaultDataBufferFactory.sharedInstance.wrap(read)); } - internalParser.state.complete(); + parser.state.complete(); } catch (IOException ex) { - internalParser.state.dispose(); + parser.state.dispose(); listener.onError(new HttpMessageConversionException("Could not decode multipart message", ex)); } } - private final class InternalParser { - - private final byte[] boundary; - - private final Charset headersCharset; - - private final PartListener listener; - - private State state; - - InternalParser(byte[] boundary, Charset headersCharset, PartListener listener) { - this.boundary = boundary; - this.headersCharset = headersCharset; - this.listener = listener; - this.state = new PreambleState(); - } - - void changeState(State newState, @Nullable DataBuffer remainder) { - if (logger.isTraceEnabled()) { - logger.trace("Changed state: " + this.state + " -> " + newState); - } - this.state.dispose(); - this.state = newState; - if (remainder != null) { - if (remainder.readableByteCount() > 0) { - newState.data(remainder); - } - else { - DataBufferUtils.release(remainder); - } - } - } - - /** - * Concatenates the given array of byte arrays. - */ - private static byte[] concat(byte[]... byteArrays) { - int len = 0; - for (byte[] byteArray : byteArrays) { - len += byteArray.length; - } - byte[] result = new byte[len]; - len = 0; - for (byte[] byteArray : byteArrays) { - System.arraycopy(byteArray, 0, result, len, byteArray.length); - len += byteArray.length; - } - return result; - } - - - /** - * Represents the internal state of the {@link MultipartParser}. - * The flow for well-formed multipart messages is shown below: - *

-		 *     PREAMBLE
-		 *         |
-		 *         v
-		 *  +-->HEADERS--->DISPOSED
-		 *  |      |
-		 *  |      v
-		 *  +----BODY
-		 *  
- * For malformed messages the flow ends in DISPOSED. - */ - private interface State { - - byte[] CR_LF = {'\r', '\n'}; - - byte HYPHEN = '-'; - - byte[] TWO_HYPHENS = {HYPHEN, HYPHEN}; - - String HEADER_ENTRY_SEPARATOR = "\\r\\n"; - - void data(DataBuffer buf); - - void complete(); - - default void dispose() { - } - } - - /** - * The initial state of the parser. Looks for the first boundary of the - * multipart message. Note that the first boundary is not necessarily - * prefixed with {@code CR LF}; only the prefix {@code --} is required. - */ - private final class PreambleState implements State { - - private final DataBufferUtils.Matcher firstBoundary; - - - PreambleState() { - this.firstBoundary = DataBufferUtils.matcher(concat(TWO_HYPHENS, InternalParser.this.boundary)); - } - - /** - * Looks for the first boundary in the given buffer. If found, changes - * state to {@link HeadersState}, and passes on the remainder of the - * buffer. - */ - @Override - public void data(DataBuffer buf) { - int endIdx = this.firstBoundary.match(buf); - if (endIdx != -1) { - if (logger.isTraceEnabled()) { - logger.trace("First boundary found @" + endIdx + " in " + buf); - } - DataBuffer preambleBuffer = buf.split(endIdx + 1); - DataBufferUtils.release(preambleBuffer); - changeState(new HeadersState(), buf); - } - else { - DataBufferUtils.release(buf); - } - } - - @Override - public void complete() { - changeState(DisposedState.INSTANCE, null); - InternalParser.this.listener.onError(new HttpMessageConversionException("Could not find first boundary")); - } - - @Override - public String toString() { - return "PREAMBLE"; - } - - } - - /** - * The state of the parser dealing with part headers. Parses header - * buffers into a {@link HttpHeaders} instance, making sure that - * the amount does not exceed {@link #maxHeadersSize}. - */ - private final class HeadersState implements State { - - private final DataBufferUtils.Matcher endHeaders = DataBufferUtils.matcher(concat(CR_LF, CR_LF)); - - private final List buffers = new ArrayList<>(); - - private int byteCount; - - - /** - * First checks whether the multipart boundary leading to this state - * was the final boundary. Then looks for the header-body boundary - * ({@code CR LF CR LF}) in the given buffer. If found, checks whether - * the size of all header buffers does not exceed {@link #maxHeadersSize}, - * converts all buffers collected so far into a {@link HttpHeaders} object - * and changes to {@link BodyState}, passing the remainder of the - * buffer. If the boundary is not found, the buffer is collected if - * its size does not exceed {@link #maxHeadersSize}. - */ - @Override - public void data(DataBuffer buf) { - if (isLastBoundary(buf)) { - if (logger.isTraceEnabled()) { - logger.trace("Last boundary found in " + buf); - } - changeState(DisposedState.INSTANCE, buf); - InternalParser.this.listener.onComplete(); - return; - } - int endIdx = this.endHeaders.match(buf); - if (endIdx != -1) { - if (logger.isTraceEnabled()) { - logger.trace("End of headers found @" + endIdx + " in " + buf); - } - this.byteCount += endIdx; - if (belowMaxHeaderSize(this.byteCount)) { - DataBuffer headerBuf = buf.split(endIdx + 1); - this.buffers.add(headerBuf); - emitHeaders(); - changeState(new BodyState(), buf); - } - } - else { - this.byteCount += buf.readableByteCount(); - if (belowMaxHeaderSize(this.byteCount)) { - this.buffers.add(buf); - } - } - } - - private void emitHeaders() { - HttpHeaders headers = parseHeaders(); - if (logger.isTraceEnabled()) { - logger.trace("Emitting headers: " + headers); - } - InternalParser.this.listener.onHeaders(headers); - } - - /** - * If the given buffer is the first buffer, check whether it starts with {@code --}. - * If it is the second buffer, check whether it makes up {@code --} together with the first buffer. - */ - private boolean isLastBoundary(DataBuffer buf) { - return (this.buffers.isEmpty() && - buf.readableByteCount() >= 2 && - buf.getByte(0) == HYPHEN && buf.getByte(1) == HYPHEN) || - (this.buffers.size() == 1 && - this.buffers.get(0).readableByteCount() == 1 && - this.buffers.get(0).getByte(0) == HYPHEN && - buf.readableByteCount() >= 1 && - buf.getByte(0) == HYPHEN); - } - - /** - * Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize} - * and throws a {@link DataBufferLimitException} if not. - */ - private boolean belowMaxHeaderSize(long count) { - if (count <= MultipartParser.this.maxHeadersSize) { - return true; - } - else { - InternalParser.this.listener.onError( - new HttpMessageConversionException("Part headers exceeded the memory usage limit of " + - MultipartParser.this.maxHeadersSize + " bytes")); - return false; - } - } - - /** - * Parses the list of buffers into a {@link HttpHeaders} instance. - * Converts the joined buffers into a string using ISO=8859-1, and parses - * that string into key and values. - */ - private HttpHeaders parseHeaders() { - if (this.buffers.isEmpty()) { - return HttpHeaders.EMPTY; - } - DataBuffer joined = this.buffers.get(0).factory().join(this.buffers); - this.buffers.clear(); - String string = joined.toString(InternalParser.this.headersCharset); - DataBufferUtils.release(joined); - String[] lines = string.split(HEADER_ENTRY_SEPARATOR); - HttpHeaders result = new HttpHeaders(); - for (String line : lines) { - int idx = line.indexOf(':'); - if (idx != -1) { - String name = line.substring(0, idx); - String value = line.substring(idx + 1); - while (value.startsWith(" ")) { - value = value.substring(1); - } - result.add(name, value); - } - } - return result; - } - - @Override - public void complete() { - changeState(DisposedState.INSTANCE, null); - InternalParser.this.listener.onError(new HttpMessageConversionException("Could not find end of headers")); - } - - @Override - public void dispose() { - this.buffers.forEach(DataBufferUtils::release); - } - - @Override - public String toString() { - return "HEADERS"; - } - - } - - /** - * The state of the parser dealing with multipart bodies. Relays - * data buffers as {@link PartListener#onBody(DataBuffer, boolean)} - * until the boundary is found (or rather: {@code CR LF - - boundary}). - */ - private final class BodyState implements State { - - private final DataBufferUtils.Matcher boundaryMatcher; - - private final int boundaryLength; - - private final Deque queue = new ArrayDeque<>(); - - public BodyState() { - byte[] delimiter = concat(CR_LF, TWO_HYPHENS, InternalParser.this.boundary); - this.boundaryMatcher = DataBufferUtils.matcher(delimiter); - this.boundaryLength = delimiter.length; - } - - /** - * Checks whether the (end of the) needle {@code CR LF - - boundary} - * can be found in {@code buffer}. If found, the needle can overflow into the - * previous buffer, so we calculate the length and slice the current - * and previous buffers accordingly. We then change to {@link HeadersState} - * and pass on the remainder of {@code buffer}. If the needle is not found, we - * enqueue {@code buffer}. - */ - @Override - public void data(DataBuffer buffer) { - int endIdx = this.boundaryMatcher.match(buffer); - if (endIdx != -1) { - DataBuffer boundaryBuffer = buffer.split(endIdx + 1); - if (logger.isTraceEnabled()) { - logger.trace("Boundary found @" + endIdx + " in " + buffer); - } - int len = endIdx - this.boundaryLength + 1 - boundaryBuffer.readPosition(); - if (len > 0) { - // whole boundary in buffer. - // slice off the body part, and flush - DataBuffer body = boundaryBuffer.split(len); - DataBufferUtils.release(boundaryBuffer); - enqueue(body); - flush(); - } - else if (len < 0) { - // boundary spans multiple buffers, and we've just found the end - // iterate over buffers in reverse order - DataBufferUtils.release(boundaryBuffer); - DataBuffer prev; - while ((prev = this.queue.pollLast()) != null) { - int prevByteCount = prev.readableByteCount(); - int prevLen = prevByteCount + len; - if (prevLen >= 0) { - // slice body part of previous buffer, and flush it - DataBuffer body = prev.split(prevLen + prev.readPosition()); - DataBufferUtils.release(prev); - enqueue(body); - flush(); - break; - } - else { - // previous buffer only contains boundary bytes - DataBufferUtils.release(prev); - len += prevByteCount; - } - } - } - else /* if (len == 0) */ { - // buffer starts with complete delimiter, flush out the previous buffers - DataBufferUtils.release(boundaryBuffer); - flush(); - } - - changeState(new HeadersState(), buffer); - } - else { - enqueue(buffer); - } - } - - /** - * Store the given buffer. Emit buffers that cannot contain boundary bytes, - * by iterating over the queue in reverse order, and summing buffer sizes. - * The first buffer that passes the boundary length and subsequent buffers - * are emitted (in the correct, non-reverse order). - */ - private void enqueue(DataBuffer buf) { - this.queue.add(buf); - - int len = 0; - Deque emit = new ArrayDeque<>(); - for (Iterator iterator = this.queue.descendingIterator(); iterator.hasNext(); ) { - DataBuffer previous = iterator.next(); - if (len > this.boundaryLength) { - // addFirst to negate iterating in reverse order - emit.addFirst(previous); - iterator.remove(); - } - len += previous.readableByteCount(); - } - emit.forEach(buffer -> InternalParser.this.listener.onBody(buffer, false)); - } - - private void flush() { - for (Iterator iterator = this.queue.iterator(); iterator.hasNext(); ) { - DataBuffer buffer = iterator.next(); - boolean last = !iterator.hasNext(); - InternalParser.this.listener.onBody(buffer, last); - } - this.queue.clear(); - } - - @Override - public void complete() { - changeState(DisposedState.INSTANCE, null); - String msg = "Could not find end of body (␍␊--" + - new String(InternalParser.this.boundary, StandardCharsets.UTF_8) + - ")"; - InternalParser.this.listener.onError(new HttpMessageConversionException(msg)); - } - - @Override - public void dispose() { - this.queue.forEach(DataBufferUtils::release); - this.queue.clear(); - } - - @Override - public String toString() { - return "BODY"; - } - } - - /** - * The state of the parser when finished, either due to seeing the final - * boundary or to a malformed message. Releases all incoming buffers. - */ - private static final class DisposedState implements State { - - public static final DisposedState INSTANCE = new DisposedState(); - - private DisposedState() { - } - - @Override - public void data(DataBuffer buf) { - DataBufferUtils.release(buf); - } - - @Override - public void complete() { - } - - @Override - public String toString() { - return "DISPOSED"; - } - } - - } - - /** - * Listen for part events while parsing the inbound stream of data. + * Listener for part events from the parsing of multipart content. */ interface PartListener { @@ -552,6 +174,383 @@ final class MultipartParser { * Handle any error thrown during the parsing phase. */ void onError(Throwable error); + + } + + + /** + * Represents the internal state of the {@link MultipartParser}. + * The flow for well-formed multipart messages is shown below: + *

+	 *     PREAMBLE
+	 *         |
+	 *         v
+	 *  +-->HEADERS--->DISPOSED
+	 *  |      |
+	 *  |      v
+	 *  +----BODY
+	 *  
+ * For malformed messages the flow ends in DISPOSED. + */ + private interface State { + + void data(DataBuffer buf); + + void complete(); + + default void dispose() { + } + + } + + + /** + * The initial state of the parser. Looks for the first boundary of the + * multipart message. Note that the first boundary is not necessarily + * prefixed with {@code CR LF}; only the prefix {@code --} is required. + */ + private final class PreambleState implements State { + + private final DataBufferUtils.Matcher firstBoundary; + + + PreambleState() { + this.firstBoundary = DataBufferUtils.matcher(concat(TWO_HYPHENS, MultipartParser.this.boundary)); + } + + /** + * Looks for the first boundary in the given buffer. If found, changes + * state to {@link HeadersState}, and passes on the remainder of the + * buffer. + */ + @Override + public void data(DataBuffer buf) { + int endIdx = this.firstBoundary.match(buf); + if (endIdx != -1) { + if (logger.isTraceEnabled()) { + logger.trace("First boundary found @" + endIdx + " in " + buf); + } + DataBuffer preambleBuffer = buf.split(endIdx + 1); + DataBufferUtils.release(preambleBuffer); + changeState(new HeadersState(), buf); + } + else { + DataBufferUtils.release(buf); + } + } + + @Override + public void complete() { + changeState(DisposedState.INSTANCE, null); + MultipartParser.this.listener.onError(new HttpMessageConversionException("Could not find first boundary")); + } + + @Override + public String toString() { + return "PREAMBLE"; + } + + } + + /** + * The state of the parser dealing with part headers. Parses header + * buffers into a {@link HttpHeaders} instance, making sure that + * the amount does not exceed {@link #maxHeadersSize}. + */ + private final class HeadersState implements State { + + private final DataBufferUtils.Matcher endHeaders = DataBufferUtils.matcher(concat(CR_LF, CR_LF)); + + private final List buffers = new ArrayList<>(); + + private int byteCount; + + + /** + * First checks whether the multipart boundary leading to this state + * was the final boundary. Then looks for the header-body boundary + * ({@code CR LF CR LF}) in the given buffer. If found, checks whether + * the size of all header buffers does not exceed {@link #maxHeadersSize}, + * converts all buffers collected so far into a {@link HttpHeaders} object + * and changes to {@link BodyState}, passing the remainder of the + * buffer. If the boundary is not found, the buffer is collected if + * its size does not exceed {@link #maxHeadersSize}. + */ + @Override + public void data(DataBuffer buf) { + if (isLastBoundary(buf)) { + if (logger.isTraceEnabled()) { + logger.trace("Last boundary found in " + buf); + } + changeState(DisposedState.INSTANCE, buf); + MultipartParser.this.listener.onComplete(); + return; + } + int endIdx = this.endHeaders.match(buf); + if (endIdx != -1) { + if (logger.isTraceEnabled()) { + logger.trace("End of headers found @" + endIdx + " in " + buf); + } + this.byteCount += endIdx; + if (belowMaxHeaderSize(this.byteCount)) { + DataBuffer headerBuf = buf.split(endIdx + 1); + this.buffers.add(headerBuf); + emitHeaders(); + changeState(new BodyState(), buf); + } + } + else { + this.byteCount += buf.readableByteCount(); + if (belowMaxHeaderSize(this.byteCount)) { + this.buffers.add(buf); + } + } + } + + private void emitHeaders() { + HttpHeaders headers = parseHeaders(); + if (logger.isTraceEnabled()) { + logger.trace("Emitting headers: " + headers); + } + MultipartParser.this.listener.onHeaders(headers); + } + + /** + * If the given buffer is the first buffer, check whether it starts with {@code --}. + * If it is the second buffer, check whether it makes up {@code --} together with the first buffer. + */ + private boolean isLastBoundary(DataBuffer buf) { + return (this.buffers.isEmpty() && + buf.readableByteCount() >= 2 && + buf.getByte(0) == HYPHEN && buf.getByte(1) == HYPHEN) || + (this.buffers.size() == 1 && + this.buffers.get(0).readableByteCount() == 1 && + this.buffers.get(0).getByte(0) == HYPHEN && + buf.readableByteCount() >= 1 && + buf.getByte(0) == HYPHEN); + } + + /** + * Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize} + * and throws a {@link DataBufferLimitException} if not. + */ + private boolean belowMaxHeaderSize(long count) { + if (count <= MultipartParser.this.maxHeadersSize) { + return true; + } + else { + MultipartParser.this.listener.onError( + new HttpMessageConversionException("Part headers exceeded the memory usage limit of " + + MultipartParser.this.maxHeadersSize + " bytes")); + return false; + } + } + + /** + * Parses the list of buffers into a {@link HttpHeaders} instance. + * Converts the joined buffers into a string using ISO=8859-1, and parses + * that string into key and values. + */ + private HttpHeaders parseHeaders() { + if (this.buffers.isEmpty()) { + return HttpHeaders.EMPTY; + } + DataBuffer joined = this.buffers.get(0).factory().join(this.buffers); + this.buffers.clear(); + String string = joined.toString(MultipartParser.this.headersCharset); + DataBufferUtils.release(joined); + String[] lines = string.split(HEADER_ENTRY_SEPARATOR); + HttpHeaders result = new HttpHeaders(); + for (String line : lines) { + int idx = line.indexOf(':'); + if (idx != -1) { + String name = line.substring(0, idx); + String value = line.substring(idx + 1); + while (value.startsWith(" ")) { + value = value.substring(1); + } + result.add(name, value); + } + } + return result; + } + + @Override + public void complete() { + changeState(DisposedState.INSTANCE, null); + MultipartParser.this.listener.onError(new HttpMessageConversionException("Could not find end of headers")); + } + + @Override + public void dispose() { + this.buffers.forEach(DataBufferUtils::release); + } + + @Override + public String toString() { + return "HEADERS"; + } + + } + + /** + * The state of the parser dealing with multipart bodies. Relays + * data buffers as {@link PartListener#onBody(DataBuffer, boolean)} + * until the boundary is found (or rather: {@code CR LF - - boundary}). + */ + private final class BodyState implements State { + + private final DataBufferUtils.Matcher boundaryMatcher; + + private final int boundaryLength; + + private final Deque queue = new ArrayDeque<>(); + + public BodyState() { + byte[] delimiter = concat(CR_LF, TWO_HYPHENS, MultipartParser.this.boundary); + this.boundaryMatcher = DataBufferUtils.matcher(delimiter); + this.boundaryLength = delimiter.length; + } + + /** + * Checks whether the (end of the) needle {@code CR LF - - boundary} + * can be found in {@code buffer}. If found, the needle can overflow into the + * previous buffer, so we calculate the length and slice the current + * and previous buffers accordingly. We then change to {@link HeadersState} + * and pass on the remainder of {@code buffer}. If the needle is not found, we + * enqueue {@code buffer}. + */ + @Override + public void data(DataBuffer buffer) { + int endIdx = this.boundaryMatcher.match(buffer); + if (endIdx != -1) { + DataBuffer boundaryBuffer = buffer.split(endIdx + 1); + if (logger.isTraceEnabled()) { + logger.trace("Boundary found @" + endIdx + " in " + buffer); + } + int len = endIdx - this.boundaryLength + 1 - boundaryBuffer.readPosition(); + if (len > 0) { + // whole boundary in buffer. + // slice off the body part, and flush + DataBuffer body = boundaryBuffer.split(len); + DataBufferUtils.release(boundaryBuffer); + enqueue(body); + flush(); + } + else if (len < 0) { + // boundary spans multiple buffers, and we've just found the end + // iterate over buffers in reverse order + DataBufferUtils.release(boundaryBuffer); + DataBuffer prev; + while ((prev = this.queue.pollLast()) != null) { + int prevByteCount = prev.readableByteCount(); + int prevLen = prevByteCount + len; + if (prevLen >= 0) { + // slice body part of previous buffer, and flush it + DataBuffer body = prev.split(prevLen + prev.readPosition()); + DataBufferUtils.release(prev); + enqueue(body); + flush(); + break; + } + else { + // previous buffer only contains boundary bytes + DataBufferUtils.release(prev); + len += prevByteCount; + } + } + } + else /* if (len == 0) */ { + // buffer starts with complete delimiter, flush out the previous buffers + DataBufferUtils.release(boundaryBuffer); + flush(); + } + + changeState(new HeadersState(), buffer); + } + else { + enqueue(buffer); + } + } + + /** + * Store the given buffer. Emit buffers that cannot contain boundary bytes, + * by iterating over the queue in reverse order, and summing buffer sizes. + * The first buffer that passes the boundary length and subsequent buffers + * are emitted (in the correct, non-reverse order). + */ + private void enqueue(DataBuffer buf) { + this.queue.add(buf); + + int len = 0; + Deque emit = new ArrayDeque<>(); + for (Iterator iterator = this.queue.descendingIterator(); iterator.hasNext(); ) { + DataBuffer previous = iterator.next(); + if (len > this.boundaryLength) { + // addFirst to negate iterating in reverse order + emit.addFirst(previous); + iterator.remove(); + } + len += previous.readableByteCount(); + } + emit.forEach(buffer -> MultipartParser.this.listener.onBody(buffer, false)); + } + + private void flush() { + for (Iterator iterator = this.queue.iterator(); iterator.hasNext(); ) { + DataBuffer buffer = iterator.next(); + boolean last = !iterator.hasNext(); + MultipartParser.this.listener.onBody(buffer, last); + } + this.queue.clear(); + } + + @Override + public void complete() { + changeState(DisposedState.INSTANCE, null); + String msg = "Could not find end of body (␍␊--" + + new String(MultipartParser.this.boundary, StandardCharsets.UTF_8) + + ")"; + MultipartParser.this.listener.onError(new HttpMessageConversionException(msg)); + } + + @Override + public void dispose() { + this.queue.forEach(DataBufferUtils::release); + this.queue.clear(); + } + + @Override + public String toString() { + return "BODY"; + } + } + + + /** + * The state of the parser when finished, either due to seeing the final + * boundary or to a malformed message. Releases all incoming buffers. + */ + private static final class DisposedState implements State { + + public static final DisposedState INSTANCE = new DisposedState(); + + private DisposedState() { + } + + @Override + public void data(DataBuffer buf) { + DataBufferUtils.release(buf); + } + + @Override + public void complete() { + } + + @Override + public String toString() { + return "DISPOSED"; + } } } diff --git a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartParserTests.java b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartParserTests.java index 2110e9ba39b..067ee7cd771 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartParserTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/multipart/MultipartParserTests.java @@ -189,8 +189,7 @@ class MultipartParserTests { private void parse(String fileName, String boundary, MultipartParser.PartListener listener) throws Exception { try (InputStream input = createStream(fileName)) { - MultipartParser multipartParser = new MultipartParser(10 * 1024, 4 * 1024); - multipartParser.parse(input, boundary.getBytes(UTF_8), StandardCharsets.UTF_8, listener); + MultipartParser.parse(input, boundary.getBytes(UTF_8), UTF_8, 10 * 1024, 4 * 1024, listener); } }