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