mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
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:
+2
-2
@@ -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();
|
||||
}
|
||||
|
||||
+454
-455
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user