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