Minor refactoring in MultipartParser

Move the nested InternalParser class up, merging it with the top-level
MultipartParser, make the constructor private, and expose a static
parse method.
This commit is contained in:
rstoyanchev
2026-06-17 13:05:10 +01:00
parent 7565c51dc5
commit 97db213d41
3 changed files with 457 additions and 459 deletions
@@ -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();
}
@@ -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:
* <p><pre>
* PREAMBLE
* |
* v
* +-->HEADERS--->DISPOSED
* | |
* | v
* +----BODY
* </pre>
* 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<DataBuffer> 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<DataBuffer> 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<DataBuffer> emit = new ArrayDeque<>();
for (Iterator<DataBuffer> 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<DataBuffer> 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:
* <p><pre>
* PREAMBLE
* |
* v
* +-->HEADERS--->DISPOSED
* | |
* | v
* +----BODY
* </pre>
* 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<DataBuffer> 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<DataBuffer> 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<DataBuffer> emit = new ArrayDeque<>();
for (Iterator<DataBuffer> 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<DataBuffer> 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";
}
}
}
@@ -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);
}
}