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