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 | UnsupportedCharsetException e) {
568                     throw new ErrorDataDecoderException(e);
569                 }
570             }
571             Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
572             if (currentAttribute == null) {
573                 Attribute lengthAttribute = currentFieldAttributes
574                         .get(HttpHeaderNames.CONTENT_LENGTH);
575                 long size;
576                 try {
577                     size = lengthAttribute != null? Long.parseLong(lengthAttribute
578                             .getValue()) : 0L;
579                 } catch (IOException e) {
580                     throw new ErrorDataDecoderException(e);
581                 } catch (NumberFormatException ignored) {
582                     size = 0;
583                 }
584                 try {
585                     if (size > 0) {
586                         currentAttribute = factory.createAttribute(request,
587                                 cleanString(nameAttribute.getValue()), size);
588                     } else {
589                         currentAttribute = factory.createAttribute(request,
590                                 cleanString(nameAttribute.getValue()));
591                     }
592                 } catch (NullPointerException | IllegalArgumentException | IOException e) {
593                     throw new ErrorDataDecoderException(e);
594                 }
595                 if (localCharset != null) {
596                     currentAttribute.setCharset(localCharset);
597                 }
598             }
599             // load data
600             if (!loadDataMultipartOptimized(undecodedChunk, multipartDataBoundary, currentAttribute)) {
601                 // Delimiter is not found. Need more chunks.
602                 return null;
603             }
604             Attribute finalAttribute = currentAttribute;
605             currentAttribute = null;
606             currentFieldAttributes = null;
607             // ready to load the next one
608             currentStatus = MultiPartStatus.HEADERDELIMITER;
609             return finalAttribute;
610         }
611         case FILEUPLOAD: {
612             // eventually restart from existing FileUpload
613             return getFileUpload(multipartDataBoundary);
614         }
615         case MIXEDDELIMITER: {
616             // --AaB03x or --AaB03x--
617             // Note that currentFieldAttributes exists
618             return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION,
619                     MultiPartStatus.HEADERDELIMITER);
620         }
621         case MIXEDDISPOSITION: {
622             return findMultipartDisposition();
623         }
624         case MIXEDFILEUPLOAD: {
625             // eventually restart from existing FileUpload
626             return getFileUpload(multipartMixedBoundary);
627         }
628         case PREEPILOGUE:
629             return null;
630         case EPILOGUE:
631             return null;
632         default:
633             throw new ErrorDataDecoderException("Shouldn't reach here.");
634         }
635     }
636 
637     /**
638      * Skip control Characters
639      *
640      * @throws NotEnoughDataDecoderException
641      */
642     private static void skipControlCharacters(ByteBuf undecodedChunk) {
643         if (!undecodedChunk.hasArray()) {
644             try {
645                 skipControlCharactersStandard(undecodedChunk);
646             } catch (IndexOutOfBoundsException e1) {
647                 throw new NotEnoughDataDecoderException(e1);
648             }
649             return;
650         }
651         SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
652         while (sao.pos < sao.limit) {
653             char c = (char) (sao.bytes[sao.pos++] & 0xFF);
654             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
655                 sao.setReadPosition(1);
656                 return;
657             }
658         }
659         throw new NotEnoughDataDecoderException("Access out of bounds");
660     }
661 
662     private static void skipControlCharactersStandard(ByteBuf undecodedChunk) {
663         for (;;) {
664             char c = (char) undecodedChunk.readUnsignedByte();
665             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
666                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
667                 break;
668             }
669         }
670     }
671 
672     /**
673      * Find the next Multipart Delimiter
674      *
675      * @param delimiter
676      *            delimiter to find
677      * @param dispositionStatus
678      *            the next getStatus if the delimiter is a start
679      * @param closeDelimiterStatus
680      *            the next getStatus if the delimiter is a close delimiter
681      * @return the next InterfaceHttpData if any
682      * @throws ErrorDataDecoderException
683      */
684     private InterfaceHttpData findMultipartDelimiter(String delimiter, MultiPartStatus dispositionStatus,
685             MultiPartStatus closeDelimiterStatus) {
686         // --AaB03x or --AaB03x--
687         int readerIndex = undecodedChunk.readerIndex();
688         try {
689             skipControlCharacters(undecodedChunk);
690         } catch (NotEnoughDataDecoderException ignored) {
691             undecodedChunk.readerIndex(readerIndex);
692             return null;
693         }
694         skipOneLine();
695         String newline;
696         try {
697             newline = readDelimiterOptimized(undecodedChunk, delimiter, charset);
698         } catch (NotEnoughDataDecoderException ignored) {
699             undecodedChunk.readerIndex(readerIndex);
700             return null;
701         }
702         if (newline.equals(delimiter)) {
703             currentStatus = dispositionStatus;
704             return decodeMultipart(dispositionStatus);
705         }
706         if (newline.equals(delimiter + "--")) {
707             // CLOSEDELIMITER or MIXED CLOSEDELIMITER found
708             currentStatus = closeDelimiterStatus;
709             if (currentStatus == MultiPartStatus.HEADERDELIMITER) {
710                 // MIXEDCLOSEDELIMITER
711                 // end of the Mixed part
712                 currentFieldAttributes = null;
713                 return decodeMultipart(MultiPartStatus.HEADERDELIMITER);
714             }
715             return null;
716         }
717         undecodedChunk.readerIndex(readerIndex);
718         throw new ErrorDataDecoderException("No Multipart delimiter found");
719     }
720 
721     /**
722      * Find the next Disposition
723      *
724      * @return the next InterfaceHttpData if any
725      * @throws ErrorDataDecoderException
726      */
727     private InterfaceHttpData findMultipartDisposition() {
728         int readerIndex = undecodedChunk.readerIndex();
729         if (currentStatus == MultiPartStatus.DISPOSITION) {
730             currentFieldAttributes = new TreeMap<CharSequence, Attribute>(CaseIgnoringComparator.INSTANCE);
731         }
732         // read many lines until empty line with newline found! Store all data
733         while (!skipOneLine()) {
734             String newline;
735             try {
736                 skipControlCharacters(undecodedChunk);
737                 newline = readLineOptimized(undecodedChunk, charset);
738             } catch (NotEnoughDataDecoderException ignored) {
739                 undecodedChunk.readerIndex(readerIndex);
740                 return null;
741             }
742             String[] contents = splitMultipartHeader(newline);
743             if (HttpHeaderNames.CONTENT_DISPOSITION.contentEqualsIgnoreCase(contents[0])) {
744                 boolean checkSecondArg;
745                 if (currentStatus == MultiPartStatus.DISPOSITION) {
746                     checkSecondArg = HttpHeaderValues.FORM_DATA.contentEqualsIgnoreCase(contents[1]);
747                 } else {
748                     checkSecondArg = HttpHeaderValues.ATTACHMENT.contentEqualsIgnoreCase(contents[1])
749                             || HttpHeaderValues.FILE.contentEqualsIgnoreCase(contents[1]);
750                 }
751                 if (checkSecondArg) {
752                     // read next values and store them in the map as Attribute
753                     for (int i = 2; i < contents.length; i++) {
754                         String[] values = contents[i].split("=", 2);
755                         Attribute attribute;
756                         try {
757                             attribute = getContentDispositionAttribute(values);
758                         } catch (NullPointerException | IllegalArgumentException e) {
759                             throw new ErrorDataDecoderException(e);
760                         }
761                         currentFieldAttributes.put(attribute.getName(), attribute);
762                     }
763                 }
764             } else if (HttpHeaderNames.CONTENT_TRANSFER_ENCODING.contentEqualsIgnoreCase(contents[0])) {
765                 Attribute attribute;
766                 try {
767                     attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_TRANSFER_ENCODING.toString(),
768                             cleanString(contents[1]));
769                 } catch (NullPointerException | IllegalArgumentException e) {
770                     throw new ErrorDataDecoderException(e);
771                 }
772 
773                 currentFieldAttributes.put(HttpHeaderNames.CONTENT_TRANSFER_ENCODING, attribute);
774             } else if (HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(contents[0])) {
775                 Attribute attribute;
776                 try {
777                     attribute = factory.createAttribute(request, HttpHeaderNames.CONTENT_LENGTH.toString(),
778                             cleanString(contents[1]));
779                 } catch (NullPointerException | IllegalArgumentException e) {
780                     throw new ErrorDataDecoderException(e);
781                 }
782 
783                 currentFieldAttributes.put(HttpHeaderNames.CONTENT_LENGTH, attribute);
784             } else if (HttpHeaderNames.CONTENT_TYPE.contentEqualsIgnoreCase(contents[0])) {
785                 // Take care of possible "multipart/mixed"
786                 if (HttpHeaderValues.MULTIPART_MIXED.contentEqualsIgnoreCase(contents[1])) {
787                     if (currentStatus == MultiPartStatus.DISPOSITION) {
788                         String values = StringUtil.substringAfter(contents[2], '=');
789                         multipartMixedBoundary = "--" + values;
790                         currentStatus = MultiPartStatus.MIXEDDELIMITER;
791                         return decodeMultipart(MultiPartStatus.MIXEDDELIMITER);
792                     } else {
793                         throw new ErrorDataDecoderException("Mixed Multipart found in a previous Mixed Multipart");
794                     }
795                 } else {
796                     for (int i = 1; i < contents.length; i++) {
797                         final String charsetHeader = HttpHeaderValues.CHARSET.toString();
798                         if (contents[i].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) {
799                             String values = StringUtil.substringAfter(contents[i], '=');
800                             Attribute attribute;
801                             try {
802                                 attribute = factory.createAttribute(request, charsetHeader, cleanString(values));
803                             } catch (NullPointerException | IllegalArgumentException e) {
804                                 throw new ErrorDataDecoderException(e);
805                             }
806                             currentFieldAttributes.put(HttpHeaderValues.CHARSET, attribute);
807                         } else if (contents[i].contains("=")) {
808                             String name = StringUtil.substringBefore(contents[i], '=');
809                             String values = StringUtil.substringAfter(contents[i], '=');
810                             Attribute attribute;
811                             try {
812                                 attribute = factory.createAttribute(request, cleanString(name), values);
813                             } catch (NullPointerException | IllegalArgumentException e) {
814                                 throw new ErrorDataDecoderException(e);
815                             }
816                             currentFieldAttributes.put(name, attribute);
817                         } else {
818                             Attribute attribute;
819                             try {
820                                 attribute = factory.createAttribute(request,
821                                         cleanString(contents[0]), contents[i]);
822                             } catch (NullPointerException | IllegalArgumentException e) {
823                                 throw new ErrorDataDecoderException(e);
824                             }
825                             currentFieldAttributes.put(attribute.getName(), attribute);
826                         }
827                     }
828                 }
829             }
830         }
831         // Is it a FileUpload
832         Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
833         if (currentStatus == MultiPartStatus.DISPOSITION) {
834             if (filenameAttribute != null) {
835                 // FileUpload
836                 currentStatus = MultiPartStatus.FILEUPLOAD;
837                 // do not change the buffer position
838                 return decodeMultipart(MultiPartStatus.FILEUPLOAD);
839             } else {
840                 // Field
841                 currentStatus = MultiPartStatus.FIELD;
842                 // do not change the buffer position
843                 return decodeMultipart(MultiPartStatus.FIELD);
844             }
845         } else {
846             if (filenameAttribute != null) {
847                 // FileUpload
848                 currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
849                 // do not change the buffer position
850                 return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
851             } else {
852                 // Field is not supported in MIXED mode
853                 throw new ErrorDataDecoderException("Filename not found");
854             }
855         }
856     }
857 
858     private static final String FILENAME_ENCODED = HttpHeaderValues.FILENAME.toString() + '*';
859 
860     private Attribute getContentDispositionAttribute(String... values) {
861         String name = cleanString(values[0]);
862         String value = values[1];
863 
864         // Filename can be token, quoted or encoded. See https://tools.ietf.org/html/rfc5987
865         if (HttpHeaderValues.FILENAME.contentEquals(name)) {
866             // Value is quoted or token. Strip if quoted:
867             int last = value.length() - 1;
868             if (last > 0 &&
869               value.charAt(0) == HttpConstants.DOUBLE_QUOTE &&
870               value.charAt(last) == HttpConstants.DOUBLE_QUOTE) {
871                 value = value.substring(1, last);
872             }
873         } else if (FILENAME_ENCODED.equals(name)) {
874             try {
875                 name = HttpHeaderValues.FILENAME.toString();
876                 String[] split = cleanString(value).split("'", 3);
877                 value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0]));
878             } catch (ArrayIndexOutOfBoundsException | UnsupportedCharsetException e) {
879                  throw new ErrorDataDecoderException(e);
880             }
881         } else {
882             // otherwise we need to clean the value
883             value = cleanString(value);
884         }
885         return factory.createAttribute(request, name, value);
886     }
887 
888     /**
889      * Get the FileUpload (new one or current one)
890      *
891      * @param delimiter
892      *            the delimiter to use
893      * @return the InterfaceHttpData if any
894      * @throws ErrorDataDecoderException
895      */
896     protected InterfaceHttpData getFileUpload(String delimiter) {
897         // eventually restart from existing FileUpload
898         // Now get value according to Content-Type and Charset
899         Attribute encoding = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
900         Charset localCharset = charset;
901         // Default
902         TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7;
903         if (encoding != null) {
904             String code;
905             try {
906                 // RFC 2045 Content-Transfer-Encoding values are case-insensitive ASCII tokens.
907                 // toLowerCase() without a Locale would corrupt them under Turkish (tr_TR) locale,
908                 // where 'I' lowercases to 'ı' (U+0131) and "BINARY" becomes "bınary" - never
909                 // matching the lowercase ASCII constants compared against below.
910                 code = encoding.getValue().toLowerCase(Locale.US);
911             } catch (IOException e) {
912                 throw new ErrorDataDecoderException(e);
913             }
914             if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value())) {
915                 localCharset = CharsetUtil.US_ASCII;
916             } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) {
917                 localCharset = CharsetUtil.ISO_8859_1;
918                 mechanism = TransferEncodingMechanism.BIT8;
919             } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
920                 // no real charset, so let the default
921                 mechanism = TransferEncodingMechanism.BINARY;
922             } else {
923                 throw new ErrorDataDecoderException("TransferEncoding Unknown: " + code);
924             }
925         }
926         Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
927         if (charsetAttribute != null) {
928             try {
929                 localCharset = Charset.forName(charsetAttribute.getValue());
930             } catch (IOException | UnsupportedCharsetException e) {
931                 throw new ErrorDataDecoderException(e);
932             }
933         }
934         if (currentFileUpload == null) {
935             Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
936             Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
937             Attribute contentTypeAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_TYPE);
938             Attribute lengthAttribute = currentFieldAttributes.get(HttpHeaderNames.CONTENT_LENGTH);
939             long size;
940             try {
941                 size = lengthAttribute != null ? Long.parseLong(lengthAttribute.getValue()) : 0L;
942             } catch (IOException e) {
943                 throw new ErrorDataDecoderException(e);
944             } catch (NumberFormatException ignored) {
945                 size = 0;
946             }
947             try {
948                 String contentType;
949                 if (contentTypeAttribute != null) {
950                     contentType = contentTypeAttribute.getValue();
951                 } else {
952                     contentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
953                 }
954                 currentFileUpload = factory.createFileUpload(request,
955                         cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()),
956                         cleanMimeContentType(contentType), mechanism.value(), localCharset,
957                         size);
958             } catch (NullPointerException | IllegalArgumentException | IOException e) {
959                 throw new ErrorDataDecoderException(e);
960             }
961         }
962         // load data as much as possible
963         if (!loadDataMultipartOptimized(undecodedChunk, delimiter, currentFileUpload)) {
964             // Delimiter is not found. Need more chunks.
965             return null;
966         }
967         if (currentFileUpload.isCompleted()) {
968             // ready to load the next one
969             if (currentStatus == MultiPartStatus.FILEUPLOAD) {
970                 currentStatus = MultiPartStatus.HEADERDELIMITER;
971                 currentFieldAttributes = null;
972             } else {
973                 currentStatus = MultiPartStatus.MIXEDDELIMITER;
974                 cleanMixedAttributes();
975             }
976             FileUpload fileUpload = currentFileUpload;
977             currentFileUpload = null;
978             return fileUpload;
979         }
980         // do not change the buffer position
981         // since some can be already saved into FileUpload
982         // So do not change the currentStatus
983         return null;
984     }
985 
986     /**
987      * Destroy the {@link HttpPostMultipartRequestDecoder} and release all it resources. After this method
988      * was called it is not possible to operate on it anymore.
989      */
990     @Override
991     public void destroy() {
992         // Release all data items, including those not yet pulled, only file based items
993         cleanFiles();
994         // Clean Memory based data
995         for (InterfaceHttpData httpData : bodyListHttpData) {
996             // Might have been already released by the user
997             if (httpData.refCnt() > 0) {
998                 httpData.release();
999             }
1000         }
1001 
1002         destroyed = true;
1003 
1004         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
1005             undecodedChunk.release();
1006             undecodedChunk = null;
1007         }
1008     }
1009 
1010     /**
1011      * Clean all HttpDatas (on Disk) for the current request.
1012      */
1013     @Override
1014     public void cleanFiles() {
1015         checkDestroyed();
1016 
1017         factory.cleanRequestHttpData(request);
1018     }
1019 
1020     /**
1021      * Remove the given FileUpload from the list of FileUploads to clean
1022      */
1023     @Override
1024     public void removeHttpDataFromClean(InterfaceHttpData data) {
1025         checkDestroyed();
1026 
1027         factory.removeHttpDataFromClean(request, data);
1028     }
1029 
1030     /**
1031      * Remove all Attributes that should be cleaned between two FileUpload in
1032      * Mixed mode
1033      */
1034     private void cleanMixedAttributes() {
1035         currentFieldAttributes.remove(HttpHeaderValues.CHARSET);
1036         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_LENGTH);
1037         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TRANSFER_ENCODING);
1038         currentFieldAttributes.remove(HttpHeaderNames.CONTENT_TYPE);
1039         currentFieldAttributes.remove(HttpHeaderValues.FILENAME);
1040     }
1041 
1042     /**
1043      * Read one line up to the CRLF or LF
1044      *
1045      * @return the String from one line
1046      * @throws NotEnoughDataDecoderException
1047      *             Need more chunks and reset the {@code readerIndex} to the previous
1048      *             value
1049      */
1050     private static String readLineOptimized(ByteBuf undecodedChunk, Charset charset) {
1051         int readerIndex = undecodedChunk.readerIndex();
1052         ByteBuf line = null;
1053         try {
1054             if (undecodedChunk.isReadable()) {
1055                 int posLfOrCrLf = HttpPostBodyUtil.findLineBreak(undecodedChunk, undecodedChunk.readerIndex());
1056                 if (posLfOrCrLf <= 0) {
1057                     throw new NotEnoughDataDecoderException();
1058                 }
1059                 try {
1060                     line = undecodedChunk.alloc().heapBuffer(posLfOrCrLf);
1061                     line.writeBytes(undecodedChunk, posLfOrCrLf);
1062 
1063                     byte nextByte = undecodedChunk.readByte();
1064                     if (nextByte == HttpConstants.CR) {
1065                         // force read next byte since LF is the following one
1066                         undecodedChunk.readByte();
1067                     }
1068                     return line.toString(charset);
1069                 } finally {
1070                     line.release();
1071                 }
1072             }
1073         } catch (IndexOutOfBoundsException e) {
1074             undecodedChunk.readerIndex(readerIndex);
1075             throw new NotEnoughDataDecoderException(e);
1076         }
1077         undecodedChunk.readerIndex(readerIndex);
1078         throw new NotEnoughDataDecoderException();
1079     }
1080 
1081     /**
1082      * Read one line up to --delimiter or --delimiter-- and if existing the CRLF
1083      * or LF Read one line up to --delimiter or --delimiter-- and if existing
1084      * the CRLF or LF. Note that CRLF or LF are mandatory for opening delimiter
1085      * (--delimiter) but not for closing delimiter (--delimiter--) since some
1086      * clients does not include CRLF in this case.
1087      *
1088      * @param delimiter
1089      *            of the form --string, such that '--' is already included
1090      * @return the String from one line as the delimiter searched (opening or
1091      *         closing)
1092      * @throws NotEnoughDataDecoderException
1093      *             Need more chunks and reset the {@code readerIndex} to the previous
1094      *             value
1095      */
1096     private static String readDelimiterOptimized(ByteBuf undecodedChunk, String delimiter, Charset charset) {
1097         final int readerIndex = undecodedChunk.readerIndex();
1098         final byte[] bdelimiter = delimiter.getBytes(charset);
1099         final int delimiterLength = bdelimiter.length;
1100         try {
1101             int delimiterPos = HttpPostBodyUtil.findDelimiter(undecodedChunk, readerIndex, bdelimiter, false);
1102             if (delimiterPos < 0) {
1103                 // delimiter not found so break here !
1104                 undecodedChunk.readerIndex(readerIndex);
1105                 throw new NotEnoughDataDecoderException();
1106             }
1107             StringBuilder sb = new StringBuilder(delimiter);
1108             undecodedChunk.readerIndex(readerIndex + delimiterPos + delimiterLength);
1109             // Now check if either opening delimiter or closing delimiter
1110             if (undecodedChunk.isReadable()) {
1111                 byte nextByte = undecodedChunk.readByte();
1112                 // first check for opening delimiter
1113                 if (nextByte == HttpConstants.CR) {
1114                     nextByte = undecodedChunk.readByte();
1115                     if (nextByte == HttpConstants.LF) {
1116                         return sb.toString();
1117                     } else {
1118                         // error since CR must be followed by LF
1119                         // delimiter not found so break here !
1120                         undecodedChunk.readerIndex(readerIndex);
1121                         throw new NotEnoughDataDecoderException();
1122                     }
1123                 } else if (nextByte == HttpConstants.LF) {
1124                     return sb.toString();
1125                 } else if (nextByte == '-') {
1126                     sb.append('-');
1127                     // second check for closing delimiter
1128                     nextByte = undecodedChunk.readByte();
1129                     if (nextByte == '-') {
1130                         sb.append('-');
1131                         // now try to find if CRLF or LF there
1132                         if (undecodedChunk.isReadable()) {
1133                             nextByte = undecodedChunk.readByte();
1134                             if (nextByte == HttpConstants.CR) {
1135                                 nextByte = undecodedChunk.readByte();
1136                                 if (nextByte == HttpConstants.LF) {
1137                                     return sb.toString();
1138                                 } else {
1139                                     // error CR without LF
1140                                     // delimiter not found so break here !
1141                                     undecodedChunk.readerIndex(readerIndex);
1142                                     throw new NotEnoughDataDecoderException();
1143                                 }
1144                             } else if (nextByte == HttpConstants.LF) {
1145                                 return sb.toString();
1146                             } else {
1147                                 // No CRLF but ok however (Adobe Flash uploader)
1148                                 // minus 1 since we read one char ahead but
1149                                 // should not
1150                                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1151                                 return sb.toString();
1152                             }
1153                         }
1154                         // FIXME what do we do here?
1155                         // either considering it is fine, either waiting for
1156                         // more data to come?
1157                         // lets try considering it is fine...
1158                         return sb.toString();
1159                     }
1160                     // only one '-' => not enough
1161                     // whatever now => error since incomplete
1162                 }
1163             }
1164         } catch (IndexOutOfBoundsException e) {
1165             undecodedChunk.readerIndex(readerIndex);
1166             throw new NotEnoughDataDecoderException(e);
1167         }
1168         undecodedChunk.readerIndex(readerIndex);
1169         throw new NotEnoughDataDecoderException();
1170     }
1171 
1172     /**
1173      * Rewrite buffer in order to skip lengthToSkip bytes from current readerIndex,
1174      * such that any readable bytes available after readerIndex + lengthToSkip (so before writerIndex)
1175      * are moved at readerIndex position,
1176      * therefore decreasing writerIndex of lengthToSkip at the end of the process.
1177      *
1178      * @param buffer the buffer to rewrite from current readerIndex
1179      * @param lengthToSkip the size to skip from readerIndex
1180      */
1181     private static void rewriteCurrentBuffer(ByteBuf buffer, int lengthToSkip) {
1182         if (lengthToSkip == 0) {
1183             return;
1184         }
1185         final int readerIndex = buffer.readerIndex();
1186         final int readableBytes = buffer.readableBytes();
1187         if (readableBytes == lengthToSkip) {
1188             buffer.readerIndex(readerIndex);
1189             buffer.writerIndex(readerIndex);
1190             return;
1191         }
1192         buffer.setBytes(readerIndex, buffer, readerIndex + lengthToSkip, readableBytes - lengthToSkip);
1193         buffer.readerIndex(readerIndex);
1194         buffer.writerIndex(readerIndex + readableBytes - lengthToSkip);
1195     }
1196 
1197     /**
1198      * Load the field value or file data from a Multipart request
1199      *
1200      * @return {@code true} if the last chunk is loaded (boundary delimiter found), {@code false} if need more chunks
1201      * @throws ErrorDataDecoderException
1202      */
1203     private static boolean loadDataMultipartOptimized(ByteBuf undecodedChunk, String delimiter, HttpData httpData) {
1204         if (!undecodedChunk.isReadable()) {
1205             return false;
1206         }
1207         final int startReaderIndex = undecodedChunk.readerIndex();
1208         final byte[] bdelimiter = delimiter.getBytes(httpData.getCharset());
1209         int posDelimiter = HttpPostBodyUtil.findDelimiter(undecodedChunk, startReaderIndex, bdelimiter, true);
1210         if (posDelimiter < 0) {
1211             // Not found but however perhaps because incomplete so search LF or CRLF from the end.
1212             // Possible last bytes contain partially delimiter
1213             // (delimiter is possibly partially there, at least 1 missing byte),
1214             // therefore searching last delimiter.length +1 (+1 for CRLF instead of LF)
1215             int readableBytes = undecodedChunk.readableBytes();
1216             int lastPosition = readableBytes - bdelimiter.length - 1;
1217             if (lastPosition < 0) {
1218                 // Not enough bytes, but at most delimiter.length bytes available so can still try to find CRLF there
1219                 lastPosition = 0;
1220             }
1221             posDelimiter = HttpPostBodyUtil.findLastLineBreak(undecodedChunk, startReaderIndex + lastPosition);
1222             // No LineBreak, however CR can be at the end of the buffer, LF not yet there (issue #11668)
1223             // Check if last CR (if any) shall not be in the content (definedLength vs actual length + buffer - 1)
1224             if (posDelimiter < 0 &&
1225                 httpData.definedLength() == httpData.length() + readableBytes - 1 &&
1226                 undecodedChunk.getByte(readableBytes + startReaderIndex - 1) == HttpConstants.CR) {
1227                 // Last CR shall precede a future LF
1228                 lastPosition = 0;
1229                 posDelimiter = readableBytes - 1;
1230             }
1231             if (posDelimiter < 0) {
1232                 // not found so this chunk can be fully added
1233                 ByteBuf content = undecodedChunk.copy();
1234                 try {
1235                     httpData.addContent(content, false);
1236                 } catch (IOException e) {
1237                     throw new ErrorDataDecoderException(e);
1238                 }
1239                 undecodedChunk.readerIndex(startReaderIndex);
1240                 undecodedChunk.writerIndex(startReaderIndex);
1241                 return false;
1242             }
1243             // posDelimiter is not from startReaderIndex but from startReaderIndex + lastPosition
1244             posDelimiter += lastPosition;
1245             if (posDelimiter == 0) {
1246                 // Nothing to add
1247                 return false;
1248             }
1249             // Not fully but still some bytes to provide: httpData is not yet finished since delimiter not found
1250             ByteBuf content = undecodedChunk.copy(startReaderIndex, posDelimiter);
1251             try {
1252                 httpData.addContent(content, false);
1253             } catch (IOException e) {
1254                 throw new ErrorDataDecoderException(e);
1255             }
1256             rewriteCurrentBuffer(undecodedChunk, posDelimiter);
1257             return false;
1258         }
1259         // Delimiter found at posDelimiter, including LF or CRLF, so httpData has its last chunk
1260         ByteBuf content = undecodedChunk.copy(startReaderIndex, posDelimiter);
1261         try {
1262             httpData.addContent(content, true);
1263         } catch (IOException e) {
1264             throw new ErrorDataDecoderException(e);
1265         }
1266         rewriteCurrentBuffer(undecodedChunk, posDelimiter);
1267         return true;
1268     }
1269 
1270     /**
1271      * Clean the String from any unallowed character
1272      *
1273      * @return the cleaned String
1274      */
1275     private static String cleanString(String field) {
1276         int size = field.length();
1277         StringBuilder sb = new StringBuilder(size);
1278         for (int i = 0; i < size; i++) {
1279             char nextChar = field.charAt(i);
1280             switch (nextChar) {
1281             case HttpConstants.COLON:
1282             case HttpConstants.COMMA:
1283             case HttpConstants.EQUALS:
1284             case HttpConstants.SEMICOLON:
1285             case HttpConstants.HT:
1286                 sb.append(HttpConstants.SP_CHAR);
1287                 break;
1288             case HttpConstants.DOUBLE_QUOTE:
1289             case HttpConstants.BACKSLASH:
1290                 // nothing added, just removes double quote and backslash
1291                 break;
1292             default:
1293                 if (nextChar < HttpConstants.SP || nextChar == HttpConstants.DEL) {
1294                     sb.append(HttpConstants.SP_CHAR);
1295                 } else {
1296                     sb.append(nextChar);
1297                 }
1298                 break;
1299             }
1300         }
1301         return sb.toString().trim();
1302     }
1303 
1304     /**
1305      * The Content-Type values in part-headers are defined by RFC 2045, aka. MIME, which effectively means
1306      * we use a default content type if the given content type is empty or malformed.
1307      * @param contentType The Content-Type part header value.
1308      * @return A cleaned up or replaced Content-Type header value that passes validation.
1309      */
1310     private static String cleanMimeContentType(String contentType) {
1311         if (!FileUploadUtil.isContentTypeForMultiPartValid(contentType)) {
1312             // RFC 2045 section 5.2 recommends falling back to a default content type,
1313             // when a "syntactically invalid Content-Type header field is encountered".
1314             return HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
1315         }
1316         return contentType;
1317     }
1318 
1319     /**
1320      * Skip one empty line
1321      *
1322      * @return True if one empty line was skipped
1323      */
1324     private boolean skipOneLine() {
1325         if (!undecodedChunk.isReadable()) {
1326             return false;
1327         }
1328         byte nextByte = undecodedChunk.readByte();
1329         if (nextByte == HttpConstants.CR) {
1330             if (!undecodedChunk.isReadable()) {
1331                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1332                 return false;
1333             }
1334             nextByte = undecodedChunk.readByte();
1335             if (nextByte == HttpConstants.LF) {
1336                 return true;
1337             }
1338             undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2);
1339             return false;
1340         }
1341         if (nextByte == HttpConstants.LF) {
1342             return true;
1343         }
1344         undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1345         return false;
1346     }
1347 
1348     /**
1349      * Split one header in Multipart
1350      *
1351      * @return an array of String where rank 0 is the name of the header,
1352      *         follows by several values that were separated by ';' or ','
1353      */
1354     private static String[] splitMultipartHeader(String sb) {
1355         ArrayList<String> headers = new ArrayList<String>(1);
1356         int nameStart;
1357         int nameEnd;
1358         int colonEnd;
1359         int valueStart;
1360         int valueEnd;
1361         nameStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
1362         for (nameEnd = nameStart; nameEnd < sb.length(); nameEnd++) {
1363             char ch = sb.charAt(nameEnd);
1364             if (ch == ':' || Character.isWhitespace(ch)) {
1365                 break;
1366             }
1367         }
1368         for (colonEnd = nameEnd; colonEnd < sb.length(); colonEnd++) {
1369             if (sb.charAt(colonEnd) == ':') {
1370                 colonEnd++;
1371                 break;
1372             }
1373         }
1374         valueStart = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd);
1375         valueEnd = HttpPostBodyUtil.findEndOfString(sb);
1376         headers.add(sb.substring(nameStart, nameEnd));
1377         String svalue = (valueStart >= valueEnd) ? StringUtil.EMPTY_STRING : sb.substring(valueStart, valueEnd);
1378         String[] values;
1379         if (svalue.indexOf(';') >= 0) {
1380             values = splitMultipartHeaderValues(svalue);
1381         } else {
1382             values = svalue.split(",");
1383         }
1384         for (String value : values) {
1385             headers.add(value.trim());
1386         }
1387         String[] array = new String[headers.size()];
1388         for (int i = 0; i < headers.size(); i++) {
1389             array[i] = headers.get(i);
1390         }
1391         return array;
1392     }
1393 
1394     /**
1395      * Split one header value in Multipart
1396      * @return an array of String where values that were separated by ';' or ','
1397      */
1398     private static String[] splitMultipartHeaderValues(String svalue) {
1399         List<String> values = InternalThreadLocalMap.get().arrayList(1);
1400         boolean inQuote = false;
1401         boolean escapeNext = false;
1402         int start = 0;
1403         for (int i = 0; i < svalue.length(); i++) {
1404             char c = svalue.charAt(i);
1405             if (inQuote) {
1406                 if (escapeNext) {
1407                     escapeNext = false;
1408                 } else {
1409                     if (c == '\\') {
1410                         escapeNext = true;
1411                     } else if (c == '"') {
1412                         inQuote = false;
1413                     }
1414                 }
1415             } else {
1416                 if (c == '"') {
1417                     inQuote = true;
1418                 } else if (c == ';') {
1419                     values.add(svalue.substring(start, i));
1420                     start = i + 1;
1421                 }
1422             }
1423         }
1424         values.add(svalue.substring(start));
1425         return values.toArray(EmptyArrays.EMPTY_STRINGS);
1426     }
1427 
1428     /**
1429      * This method is package private intentionally in order to allow during tests
1430      * to access to the amount of memory allocated (capacity) within the private
1431      * ByteBuf undecodedChunk
1432      *
1433      * @return the number of bytes the internal buffer can contain
1434      */
1435     int getCurrentAllocatedCapacity() {
1436         return undecodedChunk.capacity();
1437     }
1438 }