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