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