View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.multipart;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.http.HttpConstants;
20  import io.netty.handler.codec.http.HttpContent;
21  import io.netty.handler.codec.http.HttpHeaderNames;
22  import io.netty.handler.codec.http.HttpHeaderValues;
23  import io.netty.handler.codec.http.HttpRequest;
24  import io.netty.handler.codec.http.LastHttpContent;
25  import io.netty.handler.codec.http.QueryStringDecoder;
26  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
27  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism;
28  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
29  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
30  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
31  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
32  import io.netty.util.CharsetUtil;
33  import io.netty.util.internal.EmptyArrays;
34  import io.netty.util.internal.InternalThreadLocalMap;
35  import io.netty.util.internal.PlatformDependent;
36  import io.netty.util.internal.StringUtil;
37  
38  import java.io.IOException;
39  import java.nio.charset.Charset;
40  import java.nio.charset.IllegalCharsetNameException;
41  import java.nio.charset.UnsupportedCharsetException;
42  import java.util.ArrayList;
43  import java.util.List;
44  import java.util.Locale;
45  import java.util.Map;
46  import java.util.TreeMap;
47  
48  import static io.netty.util.internal.ObjectUtil.*;
49  
50  /**
51   * This decoder will decode Body and can handle POST BODY.
52   *
53   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
54   *
55   */
56  public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequestDecoder {
57  
58      /**
59       * Factory used to create InterfaceHttpData
60       */
61      private final HttpDataFactory factory;
62  
63      /**
64       * Request to decode
65       */
66      private final HttpRequest request;
67  
68      /**
69       * The maximum number of fields allows by the form
70       */
71      private final int maxFields;
72  
73      /**
74       * The maximum number of accumulated bytes when decoding a field
75       */
76      private final int maxBufferedBytes;
77  
78      /**
79       * Default charset to use
80       */
81      private Charset charset;
82  
83      /**
84       * Does the last chunk already received
85       */
86      private boolean isLastChunk;
87  
88      /**
89       * HttpDatas from Body
90       */
91      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
92  
93      /**
94       * HttpDatas as Map from Body
95       */
96      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
97              CaseIgnoringComparator.INSTANCE);
98  
99      /**
100      * The current channelBuffer
101      */
102     private ByteBuf undecodedChunk;
103 
104     /**
105      * Body HttpDatas current position
106      */
107     private int bodyListHttpDataRank;
108 
109     /**
110      * If multipart, this is the boundary for the global multipart
111      */
112     private final String multipartDataBoundary;
113 
114     /**
115      * If multipart, there could be internal multiparts (mixed) to the global
116      * multipart. Only one level is allowed.
117      */
118     private String multipartMixedBoundary;
119 
120     /**
121      * Current getStatus
122      */
123     private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
124 
125     /**
126      * Used in Multipart
127      */
128     private Map<CharSequence, Attribute> currentFieldAttributes;
129 
130     /**
131      * The current FileUpload that is currently in decode process
132      */
133     private FileUpload currentFileUpload;
134 
135     /**
136      * The current Attribute that is currently in decode process
137      */
138     private Attribute currentAttribute;
139 
140     private boolean destroyed;
141 
142     private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
143 
144     /**
145      *
146      * @param request
147      *            the request to decode
148      * @throws NullPointerException
149      *             for request
150      * @throws ErrorDataDecoderException
151      *             if the default charset was wrong when decoding or other
152      *             errors
153      */
154     public HttpPostMultipartRequestDecoder(HttpRequest request) {
155         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
156     }
157 
158     /**
159      *
160      * @param factory
161      *            the factory used to create InterfaceHttpData
162      * @param request
163      *            the request to decode
164      * @throws NullPointerException
165      *             for request or factory
166      * @throws ErrorDataDecoderException
167      *             if the default charset was wrong when decoding or other
168      *             errors
169      */
170     public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) {
171         this(factory, request, HttpConstants.DEFAULT_CHARSET);
172     }
173 
174     /**
175      *
176      * @param factory
177      *            the factory used to create InterfaceHttpData
178      * @param request
179      *            the request to decode
180      * @param charset
181      *            the charset to use as default
182      * @throws NullPointerException
183      *             for request or charset or factory
184      * @throws ErrorDataDecoderException
185      *             if the default charset was wrong when decoding or other
186      *             errors
187      */
188     public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
189         this(factory, request, charset, HttpPostRequestDecoder.DEFAULT_MAX_FIELDS,
190                 HttpPostRequestDecoder.DEFAULT_MAX_BUFFERED_BYTES);
191     }
192 
193     /**
194      *
195      * @param factory
196      *            the factory used to create InterfaceHttpData
197      * @param request
198      *            the request to decode
199      * @param charset
200      *            the charset to use as default
201      * @param maxFields
202      *            the maximum number of fields the form can have, {@code -1} to disable
203      * @param maxBufferedBytes
204      *            the maximum number of bytes the decoder can buffer when decoding a field, {@code -1} to disable
205      * @throws NullPointerException
206      *             for request or charset or factory
207      * @throws ErrorDataDecoderException
208      *             if the default charset was wrong when decoding or other
209      *             errors
210      */
211     public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset,
212                                            int maxFields, int maxBufferedBytes) {
213         this.request = checkNotNull(request, "request");
214         this.charset = checkNotNull(charset, "charset");
215         this.factory = checkNotNull(factory, "factory");
216         this.maxFields = maxFields;
217         this.maxBufferedBytes = maxBufferedBytes;
218         // Fill default values
219 
220         String contentTypeValue = this.request.headers().get(HttpHeaderNames.CONTENT_TYPE);
221         if (contentTypeValue == null) {
222             throw new ErrorDataDecoderException("No '" + HttpHeaderNames.CONTENT_TYPE + "' header present.");
223         }
224 
225         String[] dataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(contentTypeValue);
226         if (dataBoundary != null) {
227             multipartDataBoundary = dataBoundary[0];
228             if (dataBoundary.length > 1 && dataBoundary[1] != null) {
229                 try {
230                     this.charset = Charset.forName(dataBoundary[1]);
231                 } catch (IllegalCharsetNameException e) {
232                     throw new ErrorDataDecoderException(e);
233                 }
234             }
235         } else {
236             multipartDataBoundary = null;
237         }
238         currentStatus = MultiPartStatus.HEADERDELIMITER;
239 
240         try {
241             if (request instanceof HttpContent) {
242                 // Offer automatically if the given request is als type of HttpContent
243                 // See #1089
244                 offer((HttpContent) request);
245             } else {
246                 parseBody();
247             }
248         } catch (Throwable e) {
249             destroy();
250             PlatformDependent.throwException(e);
251         }
252     }
253 
254     private void checkDestroyed() {
255         if (destroyed) {
256             throw new IllegalStateException(HttpPostMultipartRequestDecoder.class.getSimpleName()
257                     + " was destroyed already");
258         }
259     }
260 
261     /**
262      * True if this request is a Multipart request
263      *
264      * @return True if this request is a Multipart request
265      */
266     @Override
267     public boolean isMultipart() {
268         checkDestroyed();
269         return true;
270     }
271 
272     /**
273      * Set the amount of bytes after which read bytes in the buffer should be discarded.
274      * Setting this lower gives lower memory usage but with the overhead of more memory copies.
275      * Use {@code 0} to disable it.
276      */
277     @Override
278     public void setDiscardThreshold(int discardThreshold) {
279         this.discardThreshold = checkPositiveOrZero(discardThreshold, "discardThreshold");
280     }
281 
282     /**
283      * Return the threshold in bytes after which read data in the buffer should be discarded.
284      */
285     @Override
286     public int getDiscardThreshold() {
287         return discardThreshold;
288     }
289 
290     /**
291      * This getMethod returns a List of all HttpDatas from body.<br>
292      *
293      * If chunked, all chunks must have been offered using offer() getMethod. If
294      * not, NotEnoughDataDecoderException will be raised.
295      *
296      * @return the list of HttpDatas from Body part for POST getMethod
297      * @throws NotEnoughDataDecoderException
298      *             Need more chunks
299      */
300     @Override
301     public List<InterfaceHttpData> getBodyHttpDatas() {
302         checkDestroyed();
303 
304         if (!isLastChunk) {
305             throw new NotEnoughDataDecoderException();
306         }
307         return bodyListHttpData;
308     }
309 
310     /**
311      * This getMethod returns a List of all HttpDatas with the given name from
312      * body.<br>
313      *
314      * If chunked, all chunks must have been offered using offer() getMethod. If
315      * not, NotEnoughDataDecoderException will be raised.
316      *
317      * @return All Body HttpDatas with the given name (ignore case)
318      * @throws NotEnoughDataDecoderException
319      *             need more chunks
320      */
321     @Override
322     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
323         checkDestroyed();
324 
325         if (!isLastChunk) {
326             throw new NotEnoughDataDecoderException();
327         }
328         return bodyMapHttpData.get(name);
329     }
330 
331     /**
332      * This getMethod returns the first InterfaceHttpData with the given name from
333      * body.<br>
334      *
335      * If chunked, all chunks must have been offered using offer() getMethod. If
336      * not, NotEnoughDataDecoderException will be raised.
337      *
338      * @return The first Body InterfaceHttpData with the given name (ignore
339      *         case)
340      * @throws NotEnoughDataDecoderException
341      *             need more chunks
342      */
343     @Override
344     public InterfaceHttpData getBodyHttpData(String name) {
345         checkDestroyed();
346 
347         if (!isLastChunk) {
348             throw new NotEnoughDataDecoderException();
349         }
350         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
351         if (list != null) {
352             return list.get(0);
353         }
354         return null;
355     }
356 
357     /**
358      * Initialized the internals from a new chunk
359      *
360      * @param content
361      *            the new received chunk
362      * @throws ErrorDataDecoderException
363      *             if there is a problem with the charset decoding or other
364      *             errors
365      */
366     @Override
367     public HttpPostMultipartRequestDecoder offer(HttpContent content) {
368         checkDestroyed();
369 
370         if (content instanceof LastHttpContent) {
371             isLastChunk = true;
372         }
373 
374         ByteBuf buf = content.content();
375         if (undecodedChunk == null) {
376             undecodedChunk =
377                     // Since the Handler will release the incoming later on, we need to copy it
378                     //
379                     // We are explicit allocate a buffer and NOT calling copy() as otherwise it may set a maxCapacity
380                     // which is not really usable for us as we may exceed it once we add more bytes.
381                     buf.alloc().buffer(buf.readableBytes()).writeBytes(buf);
382         } else {
383             undecodedChunk.writeBytes(buf);
384         }
385         parseBody();
386         if (maxBufferedBytes > 0 && undecodedChunk != null && undecodedChunk.readableBytes() > maxBufferedBytes) {
387             throw new HttpPostRequestDecoder.TooLongFormFieldException();
388         }
389         if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
390             if (undecodedChunk.refCnt() == 1) {
391                 // It's safe to call discardBytes() as we are the only owner of the buffer.
392                 undecodedChunk.discardReadBytes();
393             } else {
394                 // There seems to be multiple references of the buffer. Let's copy the data and release the buffer to
395                 // ensure we can give back memory to the system.
396                 ByteBuf buffer = undecodedChunk.alloc().buffer(undecodedChunk.readableBytes());
397                 buffer.writeBytes(undecodedChunk);
398                 undecodedChunk.release();
399                 undecodedChunk = buffer;
400             }
401         }
402         return this;
403     }
404 
405     /**
406      * True if at current getStatus, there is an available decoded
407      * InterfaceHttpData from the Body.
408      *
409      * This getMethod works for chunked and not chunked request.
410      *
411      * @return True if at current getStatus, there is a decoded InterfaceHttpData
412      * @throws EndOfDataDecoderException
413      *             No more data will be available
414      */
415     @Override
416     public boolean hasNext() {
417         checkDestroyed();
418 
419         if (currentStatus == MultiPartStatus.EPILOGUE) {
420             // OK except if end of list
421             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
422                 throw new EndOfDataDecoderException();
423             }
424         }
425         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
426     }
427 
428     /**
429      * Returns the next available InterfaceHttpData or null if, at the time it
430      * is called, there is no more available InterfaceHttpData. A subsequent
431      * call to offer(httpChunk) could enable more data.
432      *
433      * Be sure to call {@link InterfaceHttpData#release()} after you are done
434      * with processing to make sure to not leak any resources
435      *
436      * @return the next available InterfaceHttpData or null if none
437      * @throws EndOfDataDecoderException
438      *             No more data will be available
439      */
440     @Override
441     public InterfaceHttpData next() {
442         checkDestroyed();
443 
444         if (hasNext()) {
445             return bodyListHttpData.get(bodyListHttpDataRank++);
446         }
447         return null;
448     }
449 
450     @Override
451     public InterfaceHttpData currentPartialHttpData() {
452         if (currentFileUpload != null) {
453             return currentFileUpload;
454         } else {
455             return currentAttribute;
456         }
457     }
458 
459     /**
460      * This getMethod will parse as much as possible data and fill the list and map
461      *
462      * @throws ErrorDataDecoderException
463      *             if there is a problem with the charset decoding or other
464      *             errors
465      */
466     private void parseBody() {
467         if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
468             if (isLastChunk) {
469                 currentStatus = MultiPartStatus.EPILOGUE;
470             }
471             return;
472         }
473         parseBodyMultipart();
474     }
475 
476     /**
477      * Utility function to add a new decoded data
478      */
479     protected void addHttpData(InterfaceHttpData data) {
480         if (data == null) {
481             return;
482         }
483         if (maxFields > 0 && bodyListHttpData.size() >= maxFields) {
484             throw new HttpPostRequestDecoder.TooManyFormFieldsException();
485         }
486         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
487         if (datas == null) {
488             datas = new ArrayList<InterfaceHttpData>(1);
489             bodyMapHttpData.put(data.getName(), datas);
490         }
491         datas.add(data);
492         bodyListHttpData.add(data);
493     }
494 
495     /**
496      * Parse the Body for multipart
497      *
498      * @throws ErrorDataDecoderException
499      *             if there is a problem with the charset decoding or other
500      *             errors
501      */
502     private void parseBodyMultipart() {
503         if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
504             // nothing to decode
505             return;
506         }
507         InterfaceHttpData data = decodeMultipart(currentStatus);
508         while (data != null) {
509             addHttpData(data);
510             if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
511                 break;
512             }
513             data = decodeMultipart(currentStatus);
514         }
515     }
516 
517     /**
518      * Decode a multipart request by pieces<br>
519      * <br>
520      * NOTSTARTED PREAMBLE (<br>
521      * (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*<br>
522      * (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE<br>
523      * (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+<br>
524      * MIXEDCLOSEDELIMITER)*<br>
525      * CLOSEDELIMITER)+ EPILOGUE<br>
526      *
527      * Inspired from HttpMessageDecoder
528      *
529      * @return the next decoded InterfaceHttpData or null if none until now.
530      * @throws ErrorDataDecoderException
531      *             if an error occurs
532      */
533     private InterfaceHttpData decodeMultipart(MultiPartStatus state) {
534         switch (state) {
535         case NOTSTARTED:
536             throw new ErrorDataDecoderException("Should not be called with the current getStatus");
537         case PREAMBLE:
538             // Content-type: multipart/form-data, boundary=AaB03x
539             throw new ErrorDataDecoderException("Should not be called with the current getStatus");
540         case HEADERDELIMITER: {
541             // --AaB03x or --AaB03x--
542             return findMultipartDelimiter(multipartDataBoundary, MultiPartStatus.DISPOSITION,
543                     MultiPartStatus.PREEPILOGUE);
544         }
545         case DISPOSITION: {
546             // content-disposition: form-data; name="field1"
547             // content-disposition: form-data; name="pics"; filename="file1.txt"
548             // and other immediate values like
549             // Content-type: image/gif
550             // Content-Type: text/plain
551             // Content-Type: text/plain; charset=ISO-8859-1
552             // Content-Transfer-Encoding: binary
553             // The following line implies a change of mode (mixed mode)
554             // Content-type: multipart/mixed, boundary=BbC04y
555             return findMultipartDisposition();
556         }
557         case FIELD: {
558             // Now get value according to Content-Type and Charset
559             Charset localCharset = null;
560             Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
561             if (charsetAttribute != null) {
562                 try {
563                     localCharset = Charset.forName(charsetAttribute.getValue());
564                 } catch (IOException e) {
565                     throw new ErrorDataDecoderException(e);
566                 } catch (UnsupportedCharsetException e) {
567                     throw new ErrorDataDecoderException(e);
568                 }
569             }
570             Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
571             if (currentAttribute == null) {
572                 Attribute lengthAttribute = currentFieldAttributes
573                         .get(HttpHeaderNames.CONTENT_LENGTH);
574                 long size;
575                 try {
576                     size = lengthAttribute != null? Long.parseLong(lengthAttribute
577                             .getValue()) : 0L;
578                 } catch (IOException e) {
579                     throw new ErrorDataDecoderException(e);
580                 } catch (NumberFormatException ignored) {
581                     size = 0;
582                 }
583                 try {
584                     if (size > 0) {
585                         currentAttribute = factory.createAttribute(request,
586                                 cleanString(nameAttribute.getValue()), size);
587                     } else {
588                         currentAttribute = factory.createAttribute(request,
589                                 cleanString(nameAttribute.getValue()));
590                     }
591                 } catch (NullPointerException e) {
592                     throw new ErrorDataDecoderException(e);
593                 } catch (IllegalArgumentException e) {
594                     throw new ErrorDataDecoderException(e);
595                 } catch (IOException e) {
596                     throw new ErrorDataDecoderException(e);
597                 }
598                 if (localCharset != null) {
599                     currentAttribute.setCharset(localCharset);
600                 }
601             }
602             // load data
603             if (!loadDataMultipartOptimized(undecodedChunk, multipartDataBoundary, currentAttribute)) {
604                 // Delimiter is not found. Need more chunks.
605                 return null;
606             }
607             Attribute finalAttribute = currentAttribute;
608             currentAttribute = null;
609             currentFieldAttributes = null;
610             // ready to load the next one
611             currentStatus = MultiPartStatus.HEADERDELIMITER;
612             return finalAttribute;
613         }
614         case FILEUPLOAD: {
615             // eventually restart from existing FileUpload
616             return getFileUpload(multipartDataBoundary);
617         }
618         case MIXEDDELIMITER: {
619             // --AaB03x or --AaB03x--
620             // Note that currentFieldAttributes exists
621             return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION,
622                     MultiPartStatus.HEADERDELIMITER);
623         }
624         case MIXEDDISPOSITION: {
625             return findMultipartDisposition();
626         }
627         case MIXEDFILEUPLOAD: {
628             // eventually restart from existing FileUpload
629             return getFileUpload(multipartMixedBoundary);
630         }
631         case PREEPILOGUE:
632             return null;
633         case EPILOGUE:
634             return null;
635         default:
636             throw new ErrorDataDecoderException("Shouldn't reach here.");
637         }
638     }
639 
640     /**
641      * Skip control Characters
642      *
643      * @throws NotEnoughDataDecoderException
644      */
645     private static void skipControlCharacters(ByteBuf undecodedChunk) {
646         if (!undecodedChunk.hasArray()) {
647             try {
648                 skipControlCharactersStandard(undecodedChunk);
649             } catch (IndexOutOfBoundsException e1) {
650                 throw new NotEnoughDataDecoderException(e1);
651             }
652             return;
653         }
654         SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
655         while (sao.pos < sao.limit) {
656             char c = (char) (sao.bytes[sao.pos++] & 0xFF);
657             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
658                 sao.setReadPosition(1);
659                 return;
660             }
661         }
662         throw new NotEnoughDataDecoderException("Access out of bounds");
663     }
664 
665     private static void skipControlCharactersStandard(ByteBuf undecodedChunk) {
666         for (;;) {
667             char c = (char) undecodedChunk.readUnsignedByte();
668             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
669                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
670                 break;
671             }
672         }
673     }
674 
675     /**
676      * Find the next Multipart Delimiter
677      *
678      * @param delimiter
679      *            delimiter to find
680      * @param dispositionStatus
681      *            the next getStatus if the delimiter is a start
682      * @param closeDelimiterStatus
683      *            the next getStatus if the delimiter is a close delimiter
684      * @return the next InterfaceHttpData if any
685      * @throws ErrorDataDecoderException
686      */
687     private InterfaceHttpData findMultipartDelimiter(String delimiter, MultiPartStatus dispositionStatus,
688             MultiPartStatus closeDelimiterStatus) {
689         // --AaB03x or --AaB03x--
690         int readerIndex = undecodedChunk.readerIndex();
691         try {
692             skipControlCharacters(undecodedChunk);
693         } catch (NotEnoughDataDecoderException ignored) {
694             undecodedChunk.readerIndex(readerIndex);
695             return null;
696         }
697         skipOneLine();
698         String newline;
699         try {
700             newline = readDelimiterOptimized(undecodedChunk, delimiter, charset);
701         } catch (NotEnoughDataDecoderException ignored) {
702             undecodedChunk.readerIndex(readerIndex);
703             return null;
704         }
705         if (newline.equals(delimiter)) {
706             currentStatus = dispositionStatus;
707             return decodeMultipart(dispositionStatus);
708         }
709         if (newline.equals(delimiter + "--")) {
710             // CLOSEDELIMITER or MIXED CLOSEDELIMITER found
711             currentStatus = closeDelimiterStatus;
712             if (currentStatus == MultiPartStatus.HEADERDELIMITER) {
713                 // MIXEDCLOSEDELIMITER
714                 // end of the Mixed part
715                 currentFieldAttributes = null;
716                 return decodeMultipart(MultiPartStatus.HEADERDELIMITER);
717             }
718             return null;
719         }
720         undecodedChunk.readerIndex(readerIndex);
721         throw new ErrorDataDecoderException("No Multipart delimiter found");
722     }
723 
724     /**
725      * Find the next Disposition
726      *
727      * @return the next InterfaceHttpData if any
728      * @throws ErrorDataDecoderException
729      */
730     private InterfaceHttpData findMultipartDisposition() {
731         int readerIndex = undecodedChunk.readerIndex();
732         if (currentStatus == MultiPartStatus.DISPOSITION) {
733             currentFieldAttributes = new TreeMap<CharSequence, Attribute>(CaseIgnoringComparator.INSTANCE);
734         }
735         // read many lines until empty line with newline found! Store all data
736         while (!skipOneLine()) {
737             String newline;
738             try {
739                 skipControlCharacters(undecodedChunk);
740                 newline = readLineOptimized(undecodedChunk, charset);
741             } catch (NotEnoughDataDecoderException ignored) {
742                 undecodedChunk.readerIndex(readerIndex);
743                 return null;
744             }
745             String[] contents = splitMultipartHeader(newline);
746             if (HttpHeaderNames.CONTENT_DISPOSITION.contentEqualsIgnoreCase(contents[0])) {
747                 boolean checkSecondArg;
748                 if (currentStatus == MultiPartStatus.DISPOSITION) {
749                     checkSecondArg = HttpHeaderValues.FORM_DATA.contentEqualsIgnoreCase(contents[1]);
750                 } else {
751                     checkSecondArg = HttpHeaderValues.ATTACHMENT.contentEqualsIgnoreCase(contents[1])
752                             || HttpHeaderValues.FILE.contentEqualsIgnoreCase(contents[1]);
753                 }
754                 if (checkSecondArg) {
755                     // read next values and store them in the map as Attribute
756                     for (int i = 2; i < contents.length; i++) {
757                         String[] values = contents[i].split("=", 2);
758                         Attribute attribute;
759                         try {
760                             attribute = getContentDispositionAttribute(values);
761                         } catch (NullPointerException e) {
762                             throw new ErrorDataDecoderException(e);
763                         } catch (IllegalArgumentException e) {
764                             throw new ErrorDataDecoderException(e);
765                         }
766                         currentFieldAttributes.put(attribute.getName(), attribute);
767                     }
768                 }
769             } else if (HttpHeaderNames.CONTENT_TRANSFER_ENCODING.contentEqualsIgnoreCase(contents[0])) {
770                 Attribute attribute;
771                 try {
772                     attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_TRANSFER_ENCODING.toString(),
773                             cleanString(contents[1]));
774                 } catch (NullPointerException e) {
775                     throw new ErrorDataDecoderException(e);
776                 } catch (IllegalArgumentException e) {
777                     throw new ErrorDataDecoderException(e);
778                 }
779 
780                 currentFieldAttributes.put(HttpHeaderNames.CONTENT_TRANSFER_ENCODING, attribute);
781             } else if (HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(contents[0])) {
782                 Attribute attribute;
783                 try {
784                     attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_LENGTH.toString(),
785                             cleanString(contents[1]));
786                 } catch (NullPointerException e) {
787                     throw new ErrorDataDecoderException(e);
788                 } catch (IllegalArgumentException e) {
789                     throw new ErrorDataDecoderException(e);
790                 }
791 
792                 currentFieldAttributes.put(HttpHeaderNames.CONTENT_LENGTH, attribute);
793             } else if (HttpHeaderNames.CONTENT_TYPE.contentEqualsIgnoreCase(contents[0])) {
794                 // Take care of possible "multipart/mixed"
795                 if (HttpHeaderValues.MULTIPART_MIXED.contentEqualsIgnoreCase(contents[1])) {
796                     if (currentStatus == MultiPartStatus.DISPOSITION) {
797                         String values = StringUtil.substringAfter(contents[2], '=');
798                         multipartMixedBoundary = "--" + values;
799                         currentStatus = MultiPartStatus.MIXEDDELIMITER;
800                         return decodeMultipart(MultiPartStatus.MIXEDDELIMITER);
801                     } else {
802                         throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart");
803                     }
804                 } else {
805                     for (int i = 1; i < contents.length; i++) {
806                         final String charsetHeader = HttpHeaderValues.CHARSET.toString();
807                         if (contents[i].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
808                             String values = StringUtil.substringAfter(contents[i], '=');
809                             Attribute attribute;
810                             try {
811                                 attribute = factory.createAttribute(request, charsetHeader, cleanString(values));
812                             } catch (NullPointerException e) {
813                                 throw new ErrorDataDecoderException(e);
814                             } catch (IllegalArgumentException e) {
815                                 throw new ErrorDataDecoderException(e);
816                             }
817                             currentFieldAttributes.put(HttpHeaderValues.CHARSET, attribute);
818                         } else if (contents[i].contains("=")) {
819                             String name = StringUtil.substringBefore(contents[i], '=');
820                             String values = StringUtil.substringAfter(contents[i], '=');
821                             Attribute attribute;
822                             try {
823                                 attribute = factory.createAttribute(request, cleanString(name), values);
824                             } catch (NullPointerException e) {
825                                 throw new ErrorDataDecoderException(e);
826                             } catch (IllegalArgumentException e) {
827                                 throw new ErrorDataDecoderException(e);
828                             }
829                             currentFieldAttributes.put(name, attribute);
830                         } else {
831                             Attribute attribute;
832                             try {
833                                 attribute = factory.createAttribute(request,
834                                         cleanString(contents[0]), contents[i]);
835                             } catch (NullPointerException e) {
836                                 throw new ErrorDataDecoderException(e);
837                             } catch (IllegalArgumentException e) {
838                                 throw new ErrorDataDecoderException(e);
839                             }
840                             currentFieldAttributes.put(attribute.getName(), attribute);
841                         }
842                     }
843                 }
844             }
845         }
846         // Is it a FileUpload
847         Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
848         if (currentStatus == MultiPartStatus.DISPOSITION) {
849             if (filenameAttribute != null) {
850                 // FileUpload
851                 currentStatus = MultiPartStatus.FILEUPLOAD;
852                 // do not change the buffer position
853                 return decodeMultipart(MultiPartStatus.FILEUPLOAD);
854             } else {
855                 // Field
856                 currentStatus = MultiPartStatus.FIELD;
857                 // do not change the buffer position
858                 return decodeMultipart(MultiPartStatus.FIELD);
859             }
860         } else {
861             if (filenameAttribute != null) {
862                 // FileUpload
863                 currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
864                 // do not change the buffer position
865                 return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
866             } else {
867                 // Field is not supported in MIXED mode
868                 throw new ErrorDataDecoderException("Filename not found");
869             }
870         }
871     }
872 
873     private static final String FILENAME_ENCODED = HttpHeaderValues.FILENAME.toString() + '*';
874 
875     private Attribute getContentDispositionAttribute(String... values) {
876         String name = cleanString(values[0]);
877         String value = values[1];
878 
879         // Filename can be token, quoted or encoded. See https://tools.ietf.org/html/rfc5987
880         if (HttpHeaderValues.FILENAME.contentEquals(name)) {
881             // Value is quoted or token. Strip if quoted:
882             int last = value.length() - 1;
883             if (last > 0 &&
884               value.charAt(0) == HttpConstants.DOUBLE_QUOTE &&
885               value.charAt(last) == HttpConstants.DOUBLE_QUOTE) {
886                 value = value.substring(1, last);
887             }
888         } else if (FILENAME_ENCODED.equals(name)) {
889             try {
890                 name = HttpHeaderValues.FILENAME.toString();
891                 String[] split = cleanString(value).split("'", 3);
892                 value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0]));
893             } catch (ArrayIndexOutOfBoundsException e) {
894                  throw new ErrorDataDecoderException(e);
895             } catch (UnsupportedCharsetException e) {
896                 throw new ErrorDataDecoderException(e);
897             }
898         } else {
899             // otherwise we need to clean the value
900             value = cleanString(value);
901         }
902         return factory.createAttribute(request, name, value);
903     }
904 
905     /**
906      * Get the FileUpload (new one or current one)
907      *
908      * @param delimiter
909      *            the delimiter to use
910      * @return the InterfaceHttpData if any
911      * @throws ErrorDataDecoderException
912      */
913     protected InterfaceHttpData getFileUpload(String delimiter) {
914         // eventually restart from existing FileUpload
915         // Now get value according to Content-Type and Charset
916         Attribute encoding = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
917         Charset localCharset = charset;
918         // Default
919         TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7;
920         if (encoding != null) {
921             String code;
922             try {
923                 // RFC 2045 Content-Transfer-Encoding values are case-insensitive ASCII tokens.
924                 // toLowerCase() without a Locale would corrupt them under Turkish (tr_TR) locale,
925                 // where 'I' lowercases to 'ı' (U+0131) and "BINARY" becomes "bınary" - never
926                 // matching the lowercase ASCII constants compared against below.
927                 code = encoding.getValue().toLowerCase(Locale.US);
928             } catch (IOException e) {
929                 throw new ErrorDataDecoderException(e);
930             }
931             if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value())) {
932                 localCharset = CharsetUtil.US_ASCII;
933             } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) {
934                 localCharset = CharsetUtil.ISO_8859_1;
935                 mechanism = TransferEncodingMechanism.BIT8;
936             } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
937                 // no real charset, so let the default
938                 mechanism = TransferEncodingMechanism.BINARY;
939             } else {
940                 throw new ErrorDataDecoderException("TransferEncoding Unknown: " + code);
941             }
942         }
943         Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
944         if (charsetAttribute != null) {
945             try {
946                 localCharset = Charset.forName(charsetAttribute.getValue());
947             } catch (IOException e) {
948                 throw new ErrorDataDecoderException(e);
949             } catch (UnsupportedCharsetException e) {
950                 throw new ErrorDataDecoderException(e);
951             }
952         }
953         if (currentFileUpload == null) {
954             Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
955             Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
956             Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TYPE);
957             Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_LENGTH);
958             long size;
959             try {
960                 size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L;
961             } catch (IOException e) {
962                 throw new ErrorDataDecoderException(e);
963             } catch (NumberFormatException ignored) {
964                 size = 0;
965             }
966             try {
967                 String contentType;
968                 if (contentTypeAttribute != null) {
969                     contentType = contentTypeAttribute.getValue();
970                 } else {
971                     contentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
972                 }
973                 currentFileUpload = factory.createFileUpload(request,
974                         cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()),
975                         contentType, mechanism.value(), localCharset,
976                         size);
977             } catch (NullPointerException e) {
978                 throw new ErrorDataDecoderException(e);
979             } catch (IllegalArgumentException e) {
980                 throw new ErrorDataDecoderException(e);
981             } catch (IOException e) {
982                 throw new ErrorDataDecoderException(e);
983             }
984         }
985         // load data as much as possible
986         if (!loadDataMultipartOptimized(undecodedChunk, delimiter, currentFileUpload)) {
987             // Delimiter is not found. Need more chunks.
988             return null;
989         }
990         if (currentFileUpload.isCompleted()) {
991             // ready to load the next one
992             if (currentStatus == MultiPartStatus.FILEUPLOAD) {
993                 currentStatus = MultiPartStatus.HEADERDELIMITER;
994                 currentFieldAttributes = null;
995             } else {
996                 currentStatus = MultiPartStatus.MIXEDDELIMITER;
997                 cleanMixedAttributes();
998             }
999             FileUpload fileUpload = currentFileUpload;
1000             currentFileUpload = null;
1001             return fileUpload;
1002         }
1003         // do not change the buffer position
1004         // since some can be already saved into FileUpload
1005         // So do not change the currentStatus
1006         return null;
1007     }
1008 
1009     /**
1010      * Destroy the {@link HttpPostMultipartRequestDecoder} and release all it resources. After this method
1011      * was called it is not possible to operate on it anymore.
1012      */
1013     @Override
1014     public void destroy() {
1015         // Release all data items, including those not yet pulled, only file based items
1016         cleanFiles();
1017         // Clean Memory based data
1018         for (InterfaceHttpData httpData : bodyListHttpData) {
1019             // Might have been already released by the user
1020             if (httpData.refCnt() > 0) {
1021                 httpData.release();
1022             }
1023         }
1024 
1025         destroyed = true;
1026 
1027         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
1028             undecodedChunk.release();
1029             undecodedChunk = null;
1030         }
1031     }
1032 
1033     /**
1034      * Clean all HttpDatas (on Disk) for the current request.
1035      */
1036     @Override
1037     public void cleanFiles() {
1038         checkDestroyed();
1039 
1040         factory.cleanRequestHttpData(request);
1041     }
1042 
1043     /**
1044      * Remove the given FileUpload from the list of FileUploads to clean
1045      */
1046     @Override
1047     public void removeHttpDataFromClean(InterfaceHttpData data) {
1048         checkDestroyed();
1049 
1050         factory.removeHttpDataFromClean(request, data);
1051     }
1052 
1053     /**
1054      * Remove all Attributes that should be cleaned between two FileUpload in
1055      * Mixed mode
1056      */
1057     private void cleanMixedAttributes() {
1058         currentFieldAttributes.remove(HttpHeaderValues.CHARSET);
1059         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_LENGTH);
1060         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
1061         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TYPE);
1062         currentFieldAttributes.remove(HttpHeaderValues.FILENAME);
1063     }
1064 
1065     /**
1066      * Read one line up to the CRLF or LF
1067      *
1068      * @return the String from one line
1069      * @throws NotEnoughDataDecoderException
1070      *             Need more chunks and reset the {@code readerIndex} to the previous
1071      *             value
1072      */
1073     private static String readLineOptimized(ByteBuf undecodedChunk, Charset charset) {
1074         int readerIndex = undecodedChunk.readerIndex();
1075         ByteBuf line = null;
1076         try {
1077             if (undecodedChunk.isReadable()) {
1078                 int posLfOrCrLf = HttpPostBodyUtil.findLineBreak(undecodedChunk, undecodedChunk.readerIndex());
1079                 if (posLfOrCrLf <= 0) {
1080                     throw new NotEnoughDataDecoderException();
1081                 }
1082                 try {
1083                     line = undecodedChunk.alloc().heapBuffer(posLfOrCrLf);
1084                     line.writeBytes(undecodedChunk, posLfOrCrLf);
1085 
1086                     byte nextByte = undecodedChunk.readByte();
1087                     if (nextByte == HttpConstants.CR) {
1088                         // force read next byte since LF is the following one
1089                         undecodedChunk.readByte();
1090                     }
1091                     return line.toString(charset);
1092                 } finally {
1093                     line.release();
1094                 }
1095             }
1096         } catch (IndexOutOfBoundsException e) {
1097             undecodedChunk.readerIndex(readerIndex);
1098             throw new NotEnoughDataDecoderException(e);
1099         }
1100         undecodedChunk.readerIndex(readerIndex);
1101         throw new NotEnoughDataDecoderException();
1102     }
1103 
1104     /**
1105      * Read one line up to --delimiter or --delimiter-- and if existing the CRLF
1106      * or LF Read one line up to --delimiter or --delimiter-- and if existing
1107      * the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter
1108      * (--delimiter) but not for closing delimiter (--delimiter--) since some
1109      * clients does not include CRLF in this case.
1110      *
1111      * @param delimiter
1112      *            of the form --string, such that '--' is already included
1113      * @return the String from one line as the delimiter searched (opening or
1114      *         closing)
1115      * @throws NotEnoughDataDecoderException
1116      *             Need more chunks and reset the {@code readerIndex} to the previous
1117      *             value
1118      */
1119     private static String readDelimiterOptimized(ByteBuf undecodedChunk, String delimiter, Charset charset) {
1120         final int readerIndex = undecodedChunk.readerIndex();
1121         final byte[] bdelimiter = delimiter.getBytes(charset);
1122         final int delimiterLength = bdelimiter.length;
1123         try {
1124             int delimiterPos = HttpPostBodyUtil.findDelimiter(undecodedChunk, readerIndex, bdelimiter, false);
1125             if (delimiterPos < 0) {
1126                 // delimiter not found so break here !
1127                 undecodedChunk.readerIndex(readerIndex);
1128                 throw new NotEnoughDataDecoderException();
1129             }
1130             StringBuilder sb = new StringBuilder(delimiter);
1131             undecodedChunk.readerIndex(readerIndex + delimiterPos + delimiterLength);
1132             // Now check if either opening delimiter or closing delimiter
1133             if (undecodedChunk.isReadable()) {
1134                 byte nextByte = undecodedChunk.readByte();
1135                 // first check for opening delimiter
1136                 if (nextByte == HttpConstants.CR) {
1137                     nextByte = undecodedChunk.readByte();
1138                     if (nextByte == HttpConstants.LF) {
1139                         return sb.toString();
1140                     } else {
1141                         // error since CR must be followed by LF
1142                         // delimiter not found so break here !
1143                         undecodedChunk.readerIndex(readerIndex);
1144                         throw new NotEnoughDataDecoderException();
1145                     }
1146                 } else if (nextByte == HttpConstants.LF) {
1147                     return sb.toString();
1148                 } else if (nextByte == '-') {
1149                     sb.append('-');
1150                     // second check for closing delimiter
1151                     nextByte = undecodedChunk.readByte();
1152                     if (nextByte == '-') {
1153                         sb.append('-');
1154                         // now try to find if CRLF or LF there
1155                         if (undecodedChunk.isReadable()) {
1156                             nextByte = undecodedChunk.readByte();
1157                             if (nextByte == HttpConstants.CR) {
1158                                 nextByte = undecodedChunk.readByte();
1159                                 if (nextByte == HttpConstants.LF) {
1160                                     return sb.toString();
1161                                 } else {
1162                                     // error CR without LF
1163                                     // delimiter not found so break here !
1164                                     undecodedChunk.readerIndex(readerIndex);
1165                                     throw new NotEnoughDataDecoderException();
1166                                 }
1167                             } else if (nextByte == HttpConstants.LF) {
1168                                 return sb.toString();
1169                             } else {
1170                                 // No CRLF but ok however (Adobe Flash uploader)
1171                                 // minus 1 since we read one char ahead but
1172                                 // should not
1173                                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1174                                 return sb.toString();
1175                             }
1176                         }
1177                         // FIXME what do we do here?
1178                         // either considering it is fine, either waiting for
1179                         // more data to come?
1180                         // lets try considering it is fine...
1181                         return sb.toString();
1182                     }
1183                     // only one '-' => not enough
1184                     // whatever now => error since incomplete
1185                 }
1186             }
1187         } catch (IndexOutOfBoundsException e) {
1188             undecodedChunk.readerIndex(readerIndex);
1189             throw new NotEnoughDataDecoderException(e);
1190         }
1191         undecodedChunk.readerIndex(readerIndex);
1192         throw new NotEnoughDataDecoderException();
1193     }
1194 
1195     /**
1196      * Rewrite buffer in order to skip lengthToSkip bytes from current readerIndex,
1197      * such that any readable bytes available after readerIndex + lengthToSkip (so before writerIndex)
1198      * are moved at readerIndex position,
1199      * therefore decreasing writerIndex of lengthToSkip at the end of the process.
1200      *
1201      * @param buffer the buffer to rewrite from current readerIndex
1202      * @param lengthToSkip the size to skip from readerIndex
1203      */
1204     private static void rewriteCurrentBuffer(ByteBuf buffer, int lengthToSkip) {
1205         if (lengthToSkip == 0) {
1206             return;
1207         }
1208         final int readerIndex = buffer.readerIndex();
1209         final int readableBytes = buffer.readableBytes();
1210         if (readableBytes == lengthToSkip) {
1211             buffer.readerIndex(readerIndex);
1212             buffer.writerIndex(readerIndex);
1213             return;
1214         }
1215         buffer.setBytes(readerIndex, buffer, readerIndex + lengthToSkip, readableBytes - lengthToSkip);
1216         buffer.readerIndex(readerIndex);
1217         buffer.writerIndex(readerIndex + readableBytes - lengthToSkip);
1218     }
1219 
1220     /**
1221      * Load the field value or file data from a Multipart request
1222      *
1223      * @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
1224      * @throws ErrorDataDecoderException
1225      */
1226     private static boolean loadDataMultipartOptimized(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
1227         if (!undecodedChunk.isReadable()) {
1228             return false;
1229         }
1230         final int startReaderIndex = undecodedChunk.readerIndex();
1231         final byte[] bdelimiter = delimiter.getBytes(httpData.getCharset());
1232         int posDelimiter = HttpPostBodyUtil.findDelimiter(undecodedChunk, startReaderIndex, bdelimiter, true);
1233         if (posDelimiter < 0) {
1234             // Not found but however perhaps because incomplete so search LF or CRLF from the end.
1235             // Possible last bytes contain partially delimiter
1236             // (delimiter is possibly partially there, at least 1 missing byte),
1237             // therefore searching last delimiter.length +1 (+1 for CRLF instead of LF)
1238             int readableBytes = undecodedChunk.readableBytes();
1239             int lastPosition = readableBytes - bdelimiter.length - 1;
1240             if (lastPosition < 0) {
1241                 // Not enough bytes, but at most delimiter.length bytes available so can still try to find CRLF there
1242                 lastPosition = 0;
1243             }
1244             posDelimiter = HttpPostBodyUtil.findLastLineBreak(undecodedChunk, startReaderIndex + lastPosition);
1245             // No LineBreak, however CR can be at the end of the buffer, LF not yet there (issue #11668)
1246             // Check if last CR (if any) shall not be in the content (definedLength vs actual length + buffer - 1)
1247             if (posDelimiter < 0 &&
1248                 httpData.definedLength() == httpData.length() + readableBytes - 1 &&
1249                 undecodedChunk.getByte(readableBytes + startReaderIndex - 1) == HttpConstants.CR) {
1250                 // Last CR shall precede a future LF
1251                 lastPosition = 0;
1252                 posDelimiter = readableBytes - 1;
1253             }
1254             if (posDelimiter < 0) {
1255                 // not found so this chunk can be fully added
1256                 ByteBuf content = undecodedChunk.copy();
1257                 try {
1258                     httpData.addContent(content, false);
1259                 } catch (IOException e) {
1260                     throw new ErrorDataDecoderException(e);
1261                 }
1262                 undecodedChunk.readerIndex(startReaderIndex);
1263                 undecodedChunk.writerIndex(startReaderIndex);
1264                 return false;
1265             }
1266             // posDelimiter is not from startReaderIndex but from startReaderIndex + lastPosition
1267             posDelimiter += lastPosition;
1268             if (posDelimiter == 0) {
1269                 // Nothing to add
1270                 return false;
1271             }
1272             // Not fully but still some bytes to provide: httpData is not yet finished since delimiter not found
1273             ByteBuf content = undecodedChunk.copy(startReaderIndex, posDelimiter);
1274             try {
1275                 httpData.addContent(content, false);
1276             } catch (IOException e) {
1277                 throw new ErrorDataDecoderException(e);
1278             }
1279             rewriteCurrentBuffer(undecodedChunk, posDelimiter);
1280             return false;
1281         }
1282         // Delimiter found at posDelimiter, including LF or CRLF, so httpData has its last chunk
1283         ByteBuf content = undecodedChunk.copy(startReaderIndex, posDelimiter);
1284         try {
1285             httpData.addContent(content, true);
1286         } catch (IOException e) {
1287             throw new ErrorDataDecoderException(e);
1288         }
1289         rewriteCurrentBuffer(undecodedChunk, posDelimiter);
1290         return true;
1291     }
1292 
1293     /**
1294      * Clean the String from any unallowed character
1295      *
1296      * @return the cleaned String
1297      */
1298     private static String cleanString(String field) {
1299         int size = field.length();
1300         StringBuilder sb = new StringBuilder(size);
1301         for (int i = 0; i < size; i++) {
1302             char nextChar = field.charAt(i);
1303             switch (nextChar) {
1304             case HttpConstants.COLON:
1305             case HttpConstants.COMMA:
1306             case HttpConstants.EQUALS:
1307             case HttpConstants.SEMICOLON:
1308             case HttpConstants.HT:
1309                 sb.append(HttpConstants.SP_CHAR);
1310                 break;
1311             case HttpConstants.DOUBLE_QUOTE:
1312                 // nothing added, just removes it
1313                 break;
1314             default:
1315                 sb.append(nextChar);
1316                 break;
1317             }
1318         }
1319         return sb.toString().trim();
1320     }
1321 
1322     /**
1323      * Skip one empty line
1324      *
1325      * @return True if one empty line was skipped
1326      */
1327     private boolean skipOneLine() {
1328         if (!undecodedChunk.isReadable()) {
1329             return false;
1330         }
1331         byte nextByte = undecodedChunk.readByte();
1332         if (nextByte == HttpConstants.CR) {
1333             if (!undecodedChunk.isReadable()) {
1334                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1335                 return false;
1336             }
1337             nextByte = undecodedChunk.readByte();
1338             if (nextByte == HttpConstants.LF) {
1339                 return true;
1340             }
1341             undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2);
1342             return false;
1343         }
1344         if (nextByte == HttpConstants.LF) {
1345             return true;
1346         }
1347         undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1348         return false;
1349     }
1350 
1351     /**
1352      * Split one header in Multipart
1353      *
1354      * @return an array of String where rank 0 is the name of the header,
1355      *         follows by several values that were separated by ';' or ','
1356      */
1357     private static String[] splitMultipartHeader(String sb) {
1358         ArrayList<String> headers = new ArrayList<String>(1);
1359         int nameStart;
1360         int nameEnd;
1361         int colonEnd;
1362         int valueStart;
1363         int valueEnd;
1364         nameStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
1365         for (nameEnd = nameStart; nameEnd < sb.length(); nameEnd++) {
1366             char ch = sb.charAt(nameEnd);
1367             if (ch == ':' || Character.isWhitespace(ch)) {
1368                 break;
1369             }
1370         }
1371         for (colonEnd = nameEnd; colonEnd < sb.length(); colonEnd++) {
1372             if (sb.charAt(colonEnd) == ':') {
1373                 colonEnd++;
1374                 break;
1375             }
1376         }
1377         valueStart = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd);
1378         valueEnd = HttpPostBodyUtil.findEndOfString(sb);
1379         headers.add(sb.substring(nameStart, nameEnd));
1380         String svalue = (valueStart >= valueEnd) ? StringUtil.EMPTY_STRING : sb.substring(valueStart, valueEnd);
1381         String[] values;
1382         if (svalue.indexOf(';') >= 0) {
1383             values = splitMultipartHeaderValues(svalue);
1384         } else {
1385             values = svalue.split(",");
1386         }
1387         for (String value : values) {
1388             headers.add(value.trim());
1389         }
1390         String[] array = new String[headers.size()];
1391         for (int i = 0; i < headers.size(); i++) {
1392             array[i] = headers.get(i);
1393         }
1394         return array;
1395     }
1396 
1397     /**
1398      * Split one header value in Multipart
1399      * @return an array of String where values that were separated by ';' or ','
1400      */
1401     private static String[] splitMultipartHeaderValues(String svalue) {
1402         List<String> values = InternalThreadLocalMap.get().arrayList(1);
1403         boolean inQuote = false;
1404         boolean escapeNext = false;
1405         int start = 0;
1406         for (int i = 0; i < svalue.length(); i++) {
1407             char c = svalue.charAt(i);
1408             if (inQuote) {
1409                 if (escapeNext) {
1410                     escapeNext = false;
1411                 } else {
1412                     if (c == '\\') {
1413                         escapeNext = true;
1414                     } else if (c == '"') {
1415                         inQuote = false;
1416                     }
1417                 }
1418             } else {
1419                 if (c == '"') {
1420                     inQuote = true;
1421                 } else if (c == ';') {
1422                     values.add(svalue.substring(start, i));
1423                     start = i + 1;
1424                 }
1425             }
1426         }
1427         values.add(svalue.substring(start));
1428         return values.toArray(EmptyArrays.EMPTY_STRINGS);
1429     }
1430 
1431     /**
1432      * This method is package private intentionally in order to allow during tests
1433      * to access to the amount of memory allocated (capacity) within the private
1434      * ByteBuf undecodedChunk
1435      *
1436      * @return the number of bytes the internal buffer can contain
1437      */
1438     int getCurrentAllocatedCapacity() {
1439         return undecodedChunk.capacity();
1440     }
1441 }