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