/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http.multipart;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.InternalThreadLocalMap;
import io.netty.util.internal.StringUtil;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static io.netty.buffer.Unpooled.*;
import static io.netty.util.internal.ObjectUtil.*;
This decoder will decode Body and can handle POST BODY.
You MUST call destroy()
after completion to release all resources. /**
* This decoder will decode Body and can handle POST BODY.
*
* You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
*
*/
public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequestDecoder {
Factory used to create InterfaceHttpData
/**
* Factory used to create InterfaceHttpData
*/
private final HttpDataFactory factory;
Request to decode
/**
* Request to decode
*/
private final HttpRequest request;
Default charset to use
/**
* Default charset to use
*/
private Charset charset;
Does the last chunk already received
/**
* Does the last chunk already received
*/
private boolean isLastChunk;
HttpDatas from Body
/**
* HttpDatas from Body
*/
private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
HttpDatas as Map from Body
/**
* HttpDatas as Map from Body
*/
private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
CaseIgnoringComparator.INSTANCE);
The current channelBuffer
/**
* The current channelBuffer
*/
private ByteBuf undecodedChunk;
Body HttpDatas current position
/**
* Body HttpDatas current position
*/
private int bodyListHttpDataRank;
If multipart, this is the boundary for the global multipart
/**
* If multipart, this is the boundary for the global multipart
*/
private String multipartDataBoundary;
If multipart, there could be internal multiparts (mixed) to the global
multipart. Only one level is allowed.
/**
* If multipart, there could be internal multiparts (mixed) to the global
* multipart. Only one level is allowed.
*/
private String multipartMixedBoundary;
Current getStatus
/**
* Current getStatus
*/
private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
Used in Multipart
/**
* Used in Multipart
*/
private Map<CharSequence, Attribute> currentFieldAttributes;
The current FileUpload that is currently in decode process
/**
* The current FileUpload that is currently in decode process
*/
private FileUpload currentFileUpload;
The current Attribute that is currently in decode process
/**
* The current Attribute that is currently in decode process
*/
private Attribute currentAttribute;
private boolean destroyed;
private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
Params: - request –
the request to decode
Throws: - NullPointerException –
for request
- ErrorDataDecoderException –
if the default charset was wrong when decoding or other
errors
/**
*
* @param request
* the request to decode
* @throws NullPointerException
* for request
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostMultipartRequestDecoder(HttpRequest request) {
this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
}
Params: - factory –
the factory used to create InterfaceHttpData
- request –
the request to decode
Throws: - NullPointerException –
for request or factory
- ErrorDataDecoderException –
if the default charset was wrong when decoding or other
errors
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to decode
* @throws NullPointerException
* for request or factory
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) {
this(factory, request, HttpConstants.DEFAULT_CHARSET);
}
Params: - factory –
the factory used to create InterfaceHttpData
- request –
the request to decode
- charset –
the charset to use as default
Throws: - NullPointerException –
for request or charset or factory
- ErrorDataDecoderException –
if the default charset was wrong when decoding or other
errors
/**
*
* @param factory
* the factory used to create InterfaceHttpData
* @param request
* the request to decode
* @param charset
* the charset to use as default
* @throws NullPointerException
* for request or charset or factory
* @throws ErrorDataDecoderException
* if the default charset was wrong when decoding or other
* errors
*/
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
this.request = checkNotNull(request, "request");
this.charset = checkNotNull(charset, "charset");
this.factory = checkNotNull(factory, "factory");
// Fill default values
setMultipart(this.request.headers().get(HttpHeaderNames.CONTENT_TYPE));
if (request instanceof HttpContent) {
// Offer automatically if the given request is als type of HttpContent
// See #1089
offer((HttpContent) request);
} else {
undecodedChunk = buffer();
parseBody();
}
}
Set from the request ContentType the multipartDataBoundary and the possible charset.
/**
* Set from the request ContentType the multipartDataBoundary and the possible charset.
*/
private void setMultipart(String contentType) {
String[] dataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(contentType);
if (dataBoundary != null) {
multipartDataBoundary = dataBoundary[0];
if (dataBoundary.length > 1 && dataBoundary[1] != null) {
charset = Charset.forName(dataBoundary[1]);
}
} else {
multipartDataBoundary = null;
}
currentStatus = MultiPartStatus.HEADERDELIMITER;
}
private void checkDestroyed() {
if (destroyed) {
throw new IllegalStateException(HttpPostMultipartRequestDecoder.class.getSimpleName()
+ " was destroyed already");
}
}
True if this request is a Multipart request
Returns: True if this request is a Multipart request
/**
* True if this request is a Multipart request
*
* @return True if this request is a Multipart request
*/
@Override
public boolean isMultipart() {
checkDestroyed();
return true;
}
Set the amount of bytes after which read bytes in the buffer should be discarded. Setting this lower gives lower memory usage but with the overhead of more memory copies. Use 0
to disable it. /**
* Set the amount of bytes after which read bytes in the buffer should be discarded.
* Setting this lower gives lower memory usage but with the overhead of more memory copies.
* Use {@code 0} to disable it.
*/
@Override
public void setDiscardThreshold(int discardThreshold) {
this.discardThreshold = checkPositiveOrZero(discardThreshold, "discardThreshold");
}
Return the threshold in bytes after which read data in the buffer should be discarded.
/**
* Return the threshold in bytes after which read data in the buffer should be discarded.
*/
@Override
public int getDiscardThreshold() {
return discardThreshold;
}
This getMethod returns a List of all HttpDatas from body.
If chunked, all chunks must have been offered using offer() getMethod. If
not, NotEnoughDataDecoderException will be raised.
Throws: - NotEnoughDataDecoderException –
Need more chunks
Returns: the list of HttpDatas from Body part for POST getMethod
/**
* This getMethod returns a List of all HttpDatas from body.<br>
*
* If chunked, all chunks must have been offered using offer() getMethod. If
* not, NotEnoughDataDecoderException will be raised.
*
* @return the list of HttpDatas from Body part for POST getMethod
* @throws NotEnoughDataDecoderException
* Need more chunks
*/
@Override
public List<InterfaceHttpData> getBodyHttpDatas() {
checkDestroyed();
if (!isLastChunk) {
throw new NotEnoughDataDecoderException();
}
return bodyListHttpData;
}
This getMethod returns a List of all HttpDatas with the given name from
body.
If chunked, all chunks must have been offered using offer() getMethod. If
not, NotEnoughDataDecoderException will be raised.
Throws: - NotEnoughDataDecoderException –
need more chunks
Returns: All Body HttpDatas with the given name (ignore case)
/**
* This getMethod returns a List of all HttpDatas with the given name from
* body.<br>
*
* If chunked, all chunks must have been offered using offer() getMethod. If
* not, NotEnoughDataDecoderException will be raised.
*
* @return All Body HttpDatas with the given name (ignore case)
* @throws NotEnoughDataDecoderException
* need more chunks
*/
@Override
public List<InterfaceHttpData> getBodyHttpDatas(String name) {
checkDestroyed();
if (!isLastChunk) {
throw new NotEnoughDataDecoderException();
}
return bodyMapHttpData.get(name);
}
This getMethod returns the first InterfaceHttpData with the given name from
body.
If chunked, all chunks must have been offered using offer() getMethod. If
not, NotEnoughDataDecoderException will be raised.
Throws: - NotEnoughDataDecoderException –
need more chunks
Returns: The first Body InterfaceHttpData with the given name (ignore
case)
/**
* This getMethod returns the first InterfaceHttpData with the given name from
* body.<br>
*
* If chunked, all chunks must have been offered using offer() getMethod. If
* not, NotEnoughDataDecoderException will be raised.
*
* @return The first Body InterfaceHttpData with the given name (ignore
* case)
* @throws NotEnoughDataDecoderException
* need more chunks
*/
@Override
public InterfaceHttpData getBodyHttpData(String name) {
checkDestroyed();
if (!isLastChunk) {
throw new NotEnoughDataDecoderException();
}
List<InterfaceHttpData> list = bodyMapHttpData.get(name);
if (list != null) {
return list.get(0);
}
return null;
}
Initialized the internals from a new chunk
Params: - content –
the new received chunk
Throws: - ErrorDataDecoderException –
if there is a problem with the charset decoding or other
errors
/**
* Initialized the internals from a new chunk
*
* @param content
* the new received chunk
* @throws ErrorDataDecoderException
* if there is a problem with the charset decoding or other
* errors
*/
@Override
public HttpPostMultipartRequestDecoder offer(HttpContent content) {
checkDestroyed();
// Maybe we should better not copy here for performance reasons but this will need
// more care by the caller to release the content in a correct manner later
// So maybe something to optimize on a later stage
ByteBuf buf = content.content();
if (undecodedChunk == null) {
undecodedChunk = buf.copy();
} else {
undecodedChunk.writeBytes(buf);
}
if (content instanceof LastHttpContent) {
isLastChunk = true;
}
parseBody();
if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
undecodedChunk.discardReadBytes();
}
return this;
}
True if at current getStatus, there is an available decoded
InterfaceHttpData from the Body.
This getMethod works for chunked and not chunked request.
Throws: - EndOfDataDecoderException –
No more data will be available
Returns: True if at current getStatus, there is a decoded InterfaceHttpData
/**
* True if at current getStatus, there is an available decoded
* InterfaceHttpData from the Body.
*
* This getMethod works for chunked and not chunked request.
*
* @return True if at current getStatus, there is a decoded InterfaceHttpData
* @throws EndOfDataDecoderException
* No more data will be available
*/
@Override
public boolean hasNext() {
checkDestroyed();
if (currentStatus == MultiPartStatus.EPILOGUE) {
// OK except if end of list
if (bodyListHttpDataRank >= bodyListHttpData.size()) {
throw new EndOfDataDecoderException();
}
}
return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
}
Returns the next available InterfaceHttpData or null if, at the time it is called, there is no more available InterfaceHttpData. A subsequent call to offer(httpChunk) could enable more data. Be sure to call ReferenceCounted.release()
after you are done with processing to make sure to not leak any resources Throws: - EndOfDataDecoderException –
No more data will be available
Returns: the next available InterfaceHttpData or null if none
/**
* Returns the next available InterfaceHttpData or null if, at the time it
* is called, there is no more available InterfaceHttpData. A subsequent
* call to offer(httpChunk) could enable more data.
*
* Be sure to call {@link InterfaceHttpData#release()} after you are done
* with processing to make sure to not leak any resources
*
* @return the next available InterfaceHttpData or null if none
* @throws EndOfDataDecoderException
* No more data will be available
*/
@Override
public InterfaceHttpData next() {
checkDestroyed();
if (hasNext()) {
return bodyListHttpData.get(bodyListHttpDataRank++);
}
return null;
}
@Override
public InterfaceHttpData currentPartialHttpData() {
if (currentFileUpload != null) {
return currentFileUpload;
} else {
return currentAttribute;
}
}
This getMethod will parse as much as possible data and fill the list and map
Throws: - ErrorDataDecoderException –
if there is a problem with the charset decoding or other
errors
/**
* This getMethod will parse as much as possible data and fill the list and map
*
* @throws ErrorDataDecoderException
* if there is a problem with the charset decoding or other
* errors
*/
private void parseBody() {
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
if (isLastChunk) {
currentStatus = MultiPartStatus.EPILOGUE;
}
return;
}
parseBodyMultipart();
}
Utility function to add a new decoded data
/**
* Utility function to add a new decoded data
*/
protected void addHttpData(InterfaceHttpData data) {
if (data == null) {
return;
}
List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
if (datas == null) {
datas = new ArrayList<InterfaceHttpData>(1);
bodyMapHttpData.put(data.getName(), datas);
}
datas.add(data);
bodyListHttpData.add(data);
}
Parse the Body for multipart
Throws: - ErrorDataDecoderException –
if there is a problem with the charset decoding or other
errors
/**
* Parse the Body for multipart
*
* @throws ErrorDataDecoderException
* if there is a problem with the charset decoding or other
* errors
*/
private void parseBodyMultipart() {
if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
// nothing to decode
return;
}
InterfaceHttpData data = decodeMultipart(currentStatus);
while (data != null) {
addHttpData(data);
if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
break;
}
data = decodeMultipart(currentStatus);
}
}
Decode a multipart request by pieces
NOTSTARTED PREAMBLE (
(HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
(HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
(MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
MIXEDCLOSEDELIMITER)*
CLOSEDELIMITER)+ EPILOGUE
Inspired from HttpMessageDecoder
Throws: - ErrorDataDecoderException –
if an error occurs
Returns: the next decoded InterfaceHttpData or null if none until now.
/**
* Decode a multipart request by pieces<br>
* <br>
* NOTSTARTED PREAMBLE (<br>
* (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*<br>
* (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE<br>
* (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+<br>
* MIXEDCLOSEDELIMITER)*<br>
* CLOSEDELIMITER)+ EPILOGUE<br>
*
* Inspired from HttpMessageDecoder
*
* @return the next decoded InterfaceHttpData or null if none until now.
* @throws ErrorDataDecoderException
* if an error occurs
*/
private InterfaceHttpData decodeMultipart(MultiPartStatus state) {
switch (state) {
case NOTSTARTED:
throw new ErrorDataDecoderException("Should not be called with the current getStatus");
case PREAMBLE:
// Content-type: multipart/form-data, boundary=AaB03x
throw new ErrorDataDecoderException("Should not be called with the current getStatus");
case HEADERDELIMITER: {
// --AaB03x or --AaB03x--
return findMultipartDelimiter(multipartDataBoundary, MultiPartStatus.DISPOSITION,
MultiPartStatus.PREEPILOGUE);
}
case DISPOSITION: {
// content-disposition: form-data; name="field1"
// content-disposition: form-data; name="pics"; filename="file1.txt"
// and other immediate values like
// Content-type: image/gif
// Content-Type: text/plain
// Content-Type: text/plain; charset=ISO-8859-1
// Content-Transfer-Encoding: binary
// The following line implies a change of mode (mixed mode)
// Content-type: multipart/mixed, boundary=BbC04y
return findMultipartDisposition();
}
case FIELD: {
// Now get value according to Content-Type and Charset
Charset localCharset = null;
Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
if (charsetAttribute != null) {
try {
localCharset = Charset.forName(charsetAttribute.getValue());
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
} catch (UnsupportedCharsetException e) {
throw new ErrorDataDecoderException(e);
}
}
Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
if (currentAttribute == null) {
Attribute lengthAttribute = currentFieldAttributes
.get(HttpHeaderNames.CONTENT_LENGTH);
long size;
try {
size = lengthAttribute != null? Long.parseLong(lengthAttribute
.getValue()) : 0L;
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
} catch (NumberFormatException ignored) {
size = 0;
}
try {
if (size > 0) {
currentAttribute = factory.createAttribute(request,
cleanString(nameAttribute.getValue()), size);
} else {
currentAttribute = factory.createAttribute(request,
cleanString(nameAttribute.getValue()));
}
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
}
if (localCharset != null) {
currentAttribute.setCharset(localCharset);
}
}
// load data
if (!loadDataMultipart(undecodedChunk, multipartDataBoundary, currentAttribute)) {
// Delimiter is not found. Need more chunks.
return null;
}
Attribute finalAttribute = currentAttribute;
currentAttribute = null;
currentFieldAttributes = null;
// ready to load the next one
currentStatus = MultiPartStatus.HEADERDELIMITER;
return finalAttribute;
}
case FILEUPLOAD: {
// eventually restart from existing FileUpload
return getFileUpload(multipartDataBoundary);
}
case MIXEDDELIMITER: {
// --AaB03x or --AaB03x--
// Note that currentFieldAttributes exists
return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION,
MultiPartStatus.HEADERDELIMITER);
}
case MIXEDDISPOSITION: {
return findMultipartDisposition();
}
case MIXEDFILEUPLOAD: {
// eventually restart from existing FileUpload
return getFileUpload(multipartMixedBoundary);
}
case PREEPILOGUE:
return null;
case EPILOGUE:
return null;
default:
throw new ErrorDataDecoderException("Shouldn't reach here.");
}
}
Skip control Characters
Throws: - NotEnoughDataDecoderException –
/**
* Skip control Characters
*
* @throws NotEnoughDataDecoderException
*/
private static void skipControlCharacters(ByteBuf undecodedChunk) {
if (!undecodedChunk.hasArray()) {
try {
skipControlCharactersStandard(undecodedChunk);
} catch (IndexOutOfBoundsException e1) {
throw new NotEnoughDataDecoderException(e1);
}
return;
}
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
while (sao.pos < sao.limit) {
char c = (char) (sao.bytes[sao.pos++] & 0xFF);
if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
sao.setReadPosition(1);
return;
}
}
throw new NotEnoughDataDecoderException("Access out of bounds");
}
private static void skipControlCharactersStandard(ByteBuf undecodedChunk) {
for (;;) {
char c = (char) undecodedChunk.readUnsignedByte();
if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
break;
}
}
}
Find the next Multipart Delimiter
Params: - delimiter –
delimiter to find
- dispositionStatus –
the next getStatus if the delimiter is a start
- closeDelimiterStatus –
the next getStatus if the delimiter is a close delimiter
Throws: Returns: the next InterfaceHttpData if any
/**
* Find the next Multipart Delimiter
*
* @param delimiter
* delimiter to find
* @param dispositionStatus
* the next getStatus if the delimiter is a start
* @param closeDelimiterStatus
* the next getStatus if the delimiter is a close delimiter
* @return the next InterfaceHttpData if any
* @throws ErrorDataDecoderException
*/
private InterfaceHttpData findMultipartDelimiter(String delimiter, MultiPartStatus dispositionStatus,
MultiPartStatus closeDelimiterStatus) {
// --AaB03x or --AaB03x--
int readerIndex = undecodedChunk.readerIndex();
try {
skipControlCharacters(undecodedChunk);
} catch (NotEnoughDataDecoderException ignored) {
undecodedChunk.readerIndex(readerIndex);
return null;
}
skipOneLine();
String newline;
try {
newline = readDelimiter(undecodedChunk, delimiter);
} catch (NotEnoughDataDecoderException ignored) {
undecodedChunk.readerIndex(readerIndex);
return null;
}
if (newline.equals(delimiter)) {
currentStatus = dispositionStatus;
return decodeMultipart(dispositionStatus);
}
if (newline.equals(delimiter + "--")) {
// CLOSEDELIMITER or MIXED CLOSEDELIMITER found
currentStatus = closeDelimiterStatus;
if (currentStatus == MultiPartStatus.HEADERDELIMITER) {
// MIXEDCLOSEDELIMITER
// end of the Mixed part
currentFieldAttributes = null;
return decodeMultipart(MultiPartStatus.HEADERDELIMITER);
}
return null;
}
undecodedChunk.readerIndex(readerIndex);
throw new ErrorDataDecoderException("No Multipart delimiter found");
}
Find the next Disposition
Throws: Returns: the next InterfaceHttpData if any
/**
* Find the next Disposition
*
* @return the next InterfaceHttpData if any
* @throws ErrorDataDecoderException
*/
private InterfaceHttpData findMultipartDisposition() {
int readerIndex = undecodedChunk.readerIndex();
if (currentStatus == MultiPartStatus.DISPOSITION) {
currentFieldAttributes = new TreeMap<CharSequence, Attribute>(CaseIgnoringComparator.INSTANCE);
}
// read many lines until empty line with newline found! Store all data
while (!skipOneLine()) {
String newline;
try {
skipControlCharacters(undecodedChunk);
newline = readLine(undecodedChunk, charset);
} catch (NotEnoughDataDecoderException ignored) {
undecodedChunk.readerIndex(readerIndex);
return null;
}
String[] contents = splitMultipartHeader(newline);
if (HttpHeaderNames.CONTENT_DISPOSITION.contentEqualsIgnoreCase(contents[0])) {
boolean checkSecondArg;
if (currentStatus == MultiPartStatus.DISPOSITION) {
checkSecondArg = HttpHeaderValues.FORM_DATA.contentEqualsIgnoreCase(contents[1]);
} else {
checkSecondArg = HttpHeaderValues.ATTACHMENT.contentEqualsIgnoreCase(contents[1])
|| HttpHeaderValues.FILE.contentEqualsIgnoreCase(contents[1]);
}
if (checkSecondArg) {
// read next values and store them in the map as Attribute
for (int i = 2; i < contents.length; i++) {
String[] values = contents[i].split("=", 2);
Attribute attribute;
try {
attribute = getContentDispositionAttribute(values);
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
}
currentFieldAttributes.put(attribute.getName(), attribute);
}
}
} else if (HttpHeaderNames.CONTENT_TRANSFER_ENCODING.contentEqualsIgnoreCase(contents[0])) {
Attribute attribute;
try {
attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_TRANSFER_ENCODING.toString(),
cleanString(contents[1]));
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
}
currentFieldAttributes.put(HttpHeaderNames.CONTENT_TRANSFER_ENCODING, attribute);
} else if (HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(contents[0])) {
Attribute attribute;
try {
attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_LENGTH.toString(),
cleanString(contents[1]));
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
}
currentFieldAttributes.put(HttpHeaderNames.CONTENT_LENGTH, attribute);
} else if (HttpHeaderNames.CONTENT_TYPE.contentEqualsIgnoreCase(contents[0])) {
// Take care of possible "multipart/mixed"
if (HttpHeaderValues.MULTIPART_MIXED.contentEqualsIgnoreCase(contents[1])) {
if (currentStatus == MultiPartStatus.DISPOSITION) {
String values = StringUtil.substringAfter(contents[2], '=');
multipartMixedBoundary = "--" + values;
currentStatus = MultiPartStatus.MIXEDDELIMITER;
return decodeMultipart(MultiPartStatus.MIXEDDELIMITER);
} else {
throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart");
}
} else {
for (int i = 1; i < contents.length; i++) {
final String charsetHeader = HttpHeaderValues.CHARSET.toString();
if (contents[i].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
String values = StringUtil.substringAfter(contents[i], '=');
Attribute attribute;
try {
attribute = factory.createAttribute(request, charsetHeader, cleanString(values));
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
}
currentFieldAttributes.put(HttpHeaderValues.CHARSET, attribute);
} else {
Attribute attribute;
try {
attribute = factory.createAttribute(request,
cleanString(contents[0]), contents[i]);
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
}
currentFieldAttributes.put(attribute.getName(), attribute);
}
}
}
} else {
throw new ErrorDataDecoderException("Unknown Params: " + newline);
}
}
// Is it a FileUpload
Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
if (currentStatus == MultiPartStatus.DISPOSITION) {
if (filenameAttribute != null) {
// FileUpload
currentStatus = MultiPartStatus.FILEUPLOAD;
// do not change the buffer position
return decodeMultipart(MultiPartStatus.FILEUPLOAD);
} else {
// Field
currentStatus = MultiPartStatus.FIELD;
// do not change the buffer position
return decodeMultipart(MultiPartStatus.FIELD);
}
} else {
if (filenameAttribute != null) {
// FileUpload
currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
// do not change the buffer position
return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
} else {
// Field is not supported in MIXED mode
throw new ErrorDataDecoderException("Filename not found");
}
}
}
private static final String FILENAME_ENCODED = HttpHeaderValues.FILENAME.toString() + '*';
private Attribute getContentDispositionAttribute(String... values) {
String name = cleanString(values[0]);
String value = values[1];
// Filename can be token, quoted or encoded. See https://tools.ietf.org/html/rfc5987
if (HttpHeaderValues.FILENAME.contentEquals(name)) {
// Value is quoted or token. Strip if quoted:
int last = value.length() - 1;
if (last > 0 &&
value.charAt(0) == HttpConstants.DOUBLE_QUOTE &&
value.charAt(last) == HttpConstants.DOUBLE_QUOTE) {
value = value.substring(1, last);
}
} else if (FILENAME_ENCODED.equals(name)) {
try {
name = HttpHeaderValues.FILENAME.toString();
String[] split = value.split("'", 3);
value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0]));
} catch (ArrayIndexOutOfBoundsException e) {
throw new ErrorDataDecoderException(e);
} catch (UnsupportedCharsetException e) {
throw new ErrorDataDecoderException(e);
}
} else {
// otherwise we need to clean the value
value = cleanString(value);
}
return factory.createAttribute(request, name, value);
}
Get the FileUpload (new one or current one)
Params: - delimiter –
the delimiter to use
Throws: Returns: the InterfaceHttpData if any
/**
* Get the FileUpload (new one or current one)
*
* @param delimiter
* the delimiter to use
* @return the InterfaceHttpData if any
* @throws ErrorDataDecoderException
*/
protected InterfaceHttpData getFileUpload(String delimiter) {
// eventually restart from existing FileUpload
// Now get value according to Content-Type and Charset
Attribute encoding = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
Charset localCharset = charset;
// Default
TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7;
if (encoding != null) {
String code;
try {
code = encoding.getValue().toLowerCase();
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
}
if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value())) {
localCharset = CharsetUtil.US_ASCII;
} else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) {
localCharset = CharsetUtil.ISO_8859_1;
mechanism = TransferEncodingMechanism.BIT8;
} else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
// no real charset, so let the default
mechanism = TransferEncodingMechanism.BINARY;
} else {
throw new ErrorDataDecoderException("TransferEncoding Unknown: " + code);
}
}
Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
if (charsetAttribute != null) {
try {
localCharset = Charset.forName(charsetAttribute.getValue());
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
} catch (UnsupportedCharsetException e) {
throw new ErrorDataDecoderException(e);
}
}
if (currentFileUpload == null) {
Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TYPE);
Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_LENGTH);
long size;
try {
size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L;
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
} catch (NumberFormatException ignored) {
size = 0;
}
try {
String contentType;
if (contentTypeAttribute != null) {
contentType = contentTypeAttribute.getValue();
} else {
contentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
}
currentFileUpload = factory.createFileUpload(request,
cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()),
contentType, mechanism.value(), localCharset,
size);
} catch (NullPointerException e) {
throw new ErrorDataDecoderException(e);
} catch (IllegalArgumentException e) {
throw new ErrorDataDecoderException(e);
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
}
}
// load data as much as possible
if (!loadDataMultipart(undecodedChunk, delimiter, currentFileUpload)) {
// Delimiter is not found. Need more chunks.
return null;
}
if (currentFileUpload.isCompleted()) {
// ready to load the next one
if (currentStatus == MultiPartStatus.FILEUPLOAD) {
currentStatus = MultiPartStatus.HEADERDELIMITER;
currentFieldAttributes = null;
} else {
currentStatus = MultiPartStatus.MIXEDDELIMITER;
cleanMixedAttributes();
}
FileUpload fileUpload = currentFileUpload;
currentFileUpload = null;
return fileUpload;
}
// do not change the buffer position
// since some can be already saved into FileUpload
// So do not change the currentStatus
return null;
}
Destroy the HttpPostMultipartRequestDecoder
and release all it resources. After this method was called it is not possible to operate on it anymore. /**
* Destroy the {@link HttpPostMultipartRequestDecoder} and release all it resources. After this method
* was called it is not possible to operate on it anymore.
*/
@Override
public void destroy() {
checkDestroyed();
cleanFiles();
destroyed = true;
if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
undecodedChunk.release();
undecodedChunk = null;
}
// release all data which was not yet pulled
for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
bodyListHttpData.get(i).release();
}
}
Clean all HttpDatas (on Disk) for the current request.
/**
* Clean all HttpDatas (on Disk) for the current request.
*/
@Override
public void cleanFiles() {
checkDestroyed();
factory.cleanRequestHttpData(request);
}
Remove the given FileUpload from the list of FileUploads to clean
/**
* Remove the given FileUpload from the list of FileUploads to clean
*/
@Override
public void removeHttpDataFromClean(InterfaceHttpData data) {
checkDestroyed();
factory.removeHttpDataFromClean(request, data);
}
Remove all Attributes that should be cleaned between two FileUpload in
Mixed mode
/**
* Remove all Attributes that should be cleaned between two FileUpload in
* Mixed mode
*/
private void cleanMixedAttributes() {
currentFieldAttributes.remove(HttpHeaderValues.CHARSET);
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_LENGTH);
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TYPE);
currentFieldAttributes.remove(HttpHeaderValues.FILENAME);
}
Read one line up to the CRLF or LF
Throws: - NotEnoughDataDecoderException – Need more chunks and reset the
readerIndex
to the previous value
Returns: the String from one line
/**
* Read one line up to the CRLF or LF
*
* @return the String from one line
* @throws NotEnoughDataDecoderException
* Need more chunks and reset the {@code readerIndex} to the previous
* value
*/
private static String readLineStandard(ByteBuf undecodedChunk, Charset charset) {
int readerIndex = undecodedChunk.readerIndex();
try {
ByteBuf line = buffer(64);
while (undecodedChunk.isReadable()) {
byte nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.CR) {
// check but do not changed readerIndex
nextByte = undecodedChunk.getByte(undecodedChunk.readerIndex());
if (nextByte == HttpConstants.LF) {
// force read
undecodedChunk.readByte();
return line.toString(charset);
} else {
// Write CR (not followed by LF)
line.writeByte(HttpConstants.CR);
}
} else if (nextByte == HttpConstants.LF) {
return line.toString(charset);
} else {
line.writeByte(nextByte);
}
}
} catch (IndexOutOfBoundsException e) {
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException(e);
}
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
Read one line up to the CRLF or LF
Throws: - NotEnoughDataDecoderException – Need more chunks and reset the
readerIndex
to the previous value
Returns: the String from one line
/**
* Read one line up to the CRLF or LF
*
* @return the String from one line
* @throws NotEnoughDataDecoderException
* Need more chunks and reset the {@code readerIndex} to the previous
* value
*/
private static String readLine(ByteBuf undecodedChunk, Charset charset) {
if (!undecodedChunk.hasArray()) {
return readLineStandard(undecodedChunk, charset);
}
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
int readerIndex = undecodedChunk.readerIndex();
try {
ByteBuf line = buffer(64);
while (sao.pos < sao.limit) {
byte nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.CR) {
if (sao.pos < sao.limit) {
nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.LF) {
sao.setReadPosition(0);
return line.toString(charset);
} else {
// Write CR (not followed by LF)
sao.pos--;
line.writeByte(HttpConstants.CR);
}
} else {
line.writeByte(nextByte);
}
} else if (nextByte == HttpConstants.LF) {
sao.setReadPosition(0);
return line.toString(charset);
} else {
line.writeByte(nextByte);
}
}
} catch (IndexOutOfBoundsException e) {
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException(e);
}
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
Read one line up to --delimiter or --delimiter-- and if existing the CRLF
or LF Read one line up to --delimiter or --delimiter-- and if existing
the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter
(--delimiter) but not for closing delimiter (--delimiter--) since some
clients does not include CRLF in this case.
Params: - delimiter –
of the form --string, such that '--' is already included
Throws: - NotEnoughDataDecoderException – Need more chunks and reset the
readerIndex
to the previous value
Returns: the String from one line as the delimiter searched (opening or
closing)
/**
* Read one line up to --delimiter or --delimiter-- and if existing the CRLF
* or LF Read one line up to --delimiter or --delimiter-- and if existing
* the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter
* (--delimiter) but not for closing delimiter (--delimiter--) since some
* clients does not include CRLF in this case.
*
* @param delimiter
* of the form --string, such that '--' is already included
* @return the String from one line as the delimiter searched (opening or
* closing)
* @throws NotEnoughDataDecoderException
* Need more chunks and reset the {@code readerIndex} to the previous
* value
*/
private static String readDelimiterStandard(ByteBuf undecodedChunk, String delimiter) {
int readerIndex = undecodedChunk.readerIndex();
try {
StringBuilder sb = new StringBuilder(64);
int delimiterPos = 0;
int len = delimiter.length();
while (undecodedChunk.isReadable() && delimiterPos < len) {
byte nextByte = undecodedChunk.readByte();
if (nextByte == delimiter.charAt(delimiterPos)) {
delimiterPos++;
sb.append((char) nextByte);
} else {
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
}
// Now check if either opening delimiter or closing delimiter
if (undecodedChunk.isReadable()) {
byte nextByte = undecodedChunk.readByte();
// first check for opening delimiter
if (nextByte == HttpConstants.CR) {
nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.LF) {
return sb.toString();
} else {
// error since CR must be followed by LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else if (nextByte == HttpConstants.LF) {
return sb.toString();
} else if (nextByte == '-') {
sb.append('-');
// second check for closing delimiter
nextByte = undecodedChunk.readByte();
if (nextByte == '-') {
sb.append('-');
// now try to find if CRLF or LF there
if (undecodedChunk.isReadable()) {
nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.CR) {
nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.LF) {
return sb.toString();
} else {
// error CR without LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else if (nextByte == HttpConstants.LF) {
return sb.toString();
} else {
// No CRLF but ok however (Adobe Flash uploader)
// minus 1 since we read one char ahead but
// should not
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
return sb.toString();
}
}
// FIXME what do we do here?
// either considering it is fine, either waiting for
// more data to come?
// lets try considering it is fine...
return sb.toString();
}
// only one '-' => not enough
// whatever now => error since incomplete
}
}
} catch (IndexOutOfBoundsException e) {
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException(e);
}
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
Read one line up to --delimiter or --delimiter-- and if existing the CRLF
or LF. Note that CRLF or LF are mandatory for opening delimiter
(--delimiter) but not for closing delimiter (--delimiter--) since some
clients does not include CRLF in this case.
Params: - delimiter –
of the form --string, such that '--' is already included
Throws: - NotEnoughDataDecoderException –
Need more chunks and reset the readerInder to the previous
value
Returns: the String from one line as the delimiter searched (opening or
closing)
/**
* Read one line up to --delimiter or --delimiter-- and if existing the CRLF
* or LF. Note that CRLF or LF are mandatory for opening delimiter
* (--delimiter) but not for closing delimiter (--delimiter--) since some
* clients does not include CRLF in this case.
*
* @param delimiter
* of the form --string, such that '--' is already included
* @return the String from one line as the delimiter searched (opening or
* closing)
* @throws NotEnoughDataDecoderException
* Need more chunks and reset the readerInder to the previous
* value
*/
private static String readDelimiter(ByteBuf undecodedChunk, String delimiter) {
if (!undecodedChunk.hasArray()) {
return readDelimiterStandard(undecodedChunk, delimiter);
}
SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
int readerIndex = undecodedChunk.readerIndex();
int delimiterPos = 0;
int len = delimiter.length();
try {
StringBuilder sb = new StringBuilder(64);
// check conformity with delimiter
while (sao.pos < sao.limit && delimiterPos < len) {
byte nextByte = sao.bytes[sao.pos++];
if (nextByte == delimiter.charAt(delimiterPos)) {
delimiterPos++;
sb.append((char) nextByte);
} else {
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
}
// Now check if either opening delimiter or closing delimiter
if (sao.pos < sao.limit) {
byte nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.CR) {
// first check for opening delimiter
if (sao.pos < sao.limit) {
nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.LF) {
sao.setReadPosition(0);
return sb.toString();
} else {
// error CR without LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else {
// error since CR must be followed by LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else if (nextByte == HttpConstants.LF) {
// same first check for opening delimiter where LF used with
// no CR
sao.setReadPosition(0);
return sb.toString();
} else if (nextByte == '-') {
sb.append('-');
// second check for closing delimiter
if (sao.pos < sao.limit) {
nextByte = sao.bytes[sao.pos++];
if (nextByte == '-') {
sb.append('-');
// now try to find if CRLF or LF there
if (sao.pos < sao.limit) {
nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.CR) {
if (sao.pos < sao.limit) {
nextByte = sao.bytes[sao.pos++];
if (nextByte == HttpConstants.LF) {
sao.setReadPosition(0);
return sb.toString();
} else {
// error CR without LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else {
// error CR without LF
// delimiter not found so break here !
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
} else if (nextByte == HttpConstants.LF) {
sao.setReadPosition(0);
return sb.toString();
} else {
// No CRLF but ok however (Adobe Flash
// uploader)
// minus 1 since we read one char ahead but
// should not
sao.setReadPosition(1);
return sb.toString();
}
}
// FIXME what do we do here?
// either considering it is fine, either waiting for
// more data to come?
// lets try considering it is fine...
sao.setReadPosition(0);
return sb.toString();
}
// whatever now => error since incomplete
// only one '-' => not enough or whatever not enough
// element
}
}
}
} catch (IndexOutOfBoundsException e) {
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException(e);
}
undecodedChunk.readerIndex(readerIndex);
throw new NotEnoughDataDecoderException();
}
Load the field value or file data from a Multipart request
Throws: Returns: true
if the last chunk is loaded (boundary delimiter found), false
if need more chunks
/**
* Load the field value or file data from a Multipart request
*
* @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
* @throws ErrorDataDecoderException
*/
private static boolean loadDataMultipartStandard(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
final int startReaderIndex = undecodedChunk.readerIndex();
final int delimeterLength = delimiter.length();
int index = 0;
int lastPosition = startReaderIndex;
byte prevByte = HttpConstants.LF;
boolean delimiterFound = false;
while (undecodedChunk.isReadable()) {
final byte nextByte = undecodedChunk.readByte();
// Check the delimiter
if (prevByte == HttpConstants.LF && nextByte == delimiter.codePointAt(index)) {
index++;
if (delimeterLength == index) {
delimiterFound = true;
break;
}
continue;
}
lastPosition = undecodedChunk.readerIndex();
if (nextByte == HttpConstants.LF) {
index = 0;
lastPosition -= (prevByte == HttpConstants.CR)? 2 : 1;
}
prevByte = nextByte;
}
if (prevByte == HttpConstants.CR) {
lastPosition--;
}
ByteBuf content = undecodedChunk.copy(startReaderIndex, lastPosition - startReaderIndex);
try {
httpData.addContent(content, delimiterFound);
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
}
undecodedChunk.readerIndex(lastPosition);
return delimiterFound;
}
Load the field value from a Multipart request
Throws: Returns: true
if the last chunk is loaded (boundary delimiter found), false
if need more chunks
/**
* Load the field value from a Multipart request
*
* @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
* @throws ErrorDataDecoderException
*/
private static boolean loadDataMultipart(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
if (!undecodedChunk.hasArray()) {
return loadDataMultipartStandard(undecodedChunk, delimiter, httpData);
}
final SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
final int startReaderIndex = undecodedChunk.readerIndex();
final int delimeterLength = delimiter.length();
int index = 0;
int lastRealPos = sao.pos;
byte prevByte = HttpConstants.LF;
boolean delimiterFound = false;
while (sao.pos < sao.limit) {
final byte nextByte = sao.bytes[sao.pos++];
// Check the delimiter
if (prevByte == HttpConstants.LF && nextByte == delimiter.codePointAt(index)) {
index++;
if (delimeterLength == index) {
delimiterFound = true;
break;
}
continue;
}
lastRealPos = sao.pos;
if (nextByte == HttpConstants.LF) {
index = 0;
lastRealPos -= (prevByte == HttpConstants.CR)? 2 : 1;
}
prevByte = nextByte;
}
if (prevByte == HttpConstants.CR) {
lastRealPos--;
}
final int lastPosition = sao.getReadPosition(lastRealPos);
final ByteBuf content = undecodedChunk.copy(startReaderIndex, lastPosition - startReaderIndex);
try {
httpData.addContent(content, delimiterFound);
} catch (IOException e) {
throw new ErrorDataDecoderException(e);
}
undecodedChunk.readerIndex(lastPosition);
return delimiterFound;
}
Clean the String from any unallowed character
Returns: the cleaned String
/**
* Clean the String from any unallowed character
*
* @return the cleaned String
*/
private static String cleanString(String field) {
int size = field.length();
StringBuilder sb = new StringBuilder(size);
for (int i = 0; i < size; i++) {
char nextChar = field.charAt(i);
switch (nextChar) {
case HttpConstants.COLON:
case HttpConstants.COMMA:
case HttpConstants.EQUALS:
case HttpConstants.SEMICOLON:
case HttpConstants.HT:
sb.append(HttpConstants.SP_CHAR);
break;
case HttpConstants.DOUBLE_QUOTE:
// nothing added, just removes it
break;
default:
sb.append(nextChar);
break;
}
}
return sb.toString().trim();
}
Skip one empty line
Returns: True if one empty line was skipped
/**
* Skip one empty line
*
* @return True if one empty line was skipped
*/
private boolean skipOneLine() {
if (!undecodedChunk.isReadable()) {
return false;
}
byte nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.CR) {
if (!undecodedChunk.isReadable()) {
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
return false;
}
nextByte = undecodedChunk.readByte();
if (nextByte == HttpConstants.LF) {
return true;
}
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2);
return false;
}
if (nextByte == HttpConstants.LF) {
return true;
}
undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
return false;
}
Split one header in Multipart
Returns: an array of String where rank 0 is the name of the header,
follows by several values that were separated by ';' or ','
/**
* Split one header in Multipart
*
* @return an array of String where rank 0 is the name of the header,
* follows by several values that were separated by ';' or ','
*/
private static String[] splitMultipartHeader(String sb) {
ArrayList<String> headers = new ArrayList<String>(1);
int nameStart;
int nameEnd;
int colonEnd;
int valueStart;
int valueEnd;
nameStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
for (nameEnd = nameStart; nameEnd < sb.length(); nameEnd++) {
char ch = sb.charAt(nameEnd);
if (ch == ':' || Character.isWhitespace(ch)) {
break;
}
}
for (colonEnd = nameEnd; colonEnd < sb.length(); colonEnd++) {
if (sb.charAt(colonEnd) == ':') {
colonEnd++;
break;
}
}
valueStart = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd);
valueEnd = HttpPostBodyUtil.findEndOfString(sb);
headers.add(sb.substring(nameStart, nameEnd));
String svalue = (valueStart >= valueEnd) ? StringUtil.EMPTY_STRING : sb.substring(valueStart, valueEnd);
String[] values;
if (svalue.indexOf(';') >= 0) {
values = splitMultipartHeaderValues(svalue);
} else {
values = svalue.split(",");
}
for (String value : values) {
headers.add(value.trim());
}
String[] array = new String[headers.size()];
for (int i = 0; i < headers.size(); i++) {
array[i] = headers.get(i);
}
return array;
}
Split one header value in Multipart
Returns: an array of String where values that were separated by ';' or ','
/**
* Split one header value in Multipart
* @return an array of String where values that were separated by ';' or ','
*/
private static String[] splitMultipartHeaderValues(String svalue) {
List<String> values = InternalThreadLocalMap.get().arrayList(1);
boolean inQuote = false;
boolean escapeNext = false;
int start = 0;
for (int i = 0; i < svalue.length(); i++) {
char c = svalue.charAt(i);
if (inQuote) {
if (escapeNext) {
escapeNext = false;
} else {
if (c == '\\') {
escapeNext = true;
} else if (c == '"') {
inQuote = false;
}
}
} else {
if (c == '"') {
inQuote = true;
} else if (c == ';') {
values.add(svalue.substring(start, i));
start = i + 1;
}
}
}
values.add(svalue.substring(start));
return values.toArray(new String[values.size()]);
}
}