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.buffer.Unpooled;
20  import io.netty.handler.codec.http.HttpConstants;
21  import io.netty.handler.codec.http.HttpContent;
22  import io.netty.handler.codec.http.HttpRequest;
23  import io.netty.handler.codec.http.LastHttpContent;
24  import io.netty.handler.codec.http.QueryStringDecoder;
25  import io.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
26  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
27  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
28  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
29  import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
30  import io.netty.util.ByteProcessor;
31  import io.netty.util.internal.PlatformDependent;
32  import io.netty.util.internal.StringUtil;
33  
34  import java.io.IOException;
35  import java.nio.charset.Charset;
36  import java.util.ArrayList;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.TreeMap;
40  
41  import static io.netty.util.internal.ObjectUtil.*;
42  
43  /**
44   * This decoder will decode Body and can handle POST BODY.
45   *
46   * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
47   *
48   */
49  public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestDecoder {
50  
51      /**
52       * Factory used to create InterfaceHttpData
53       */
54      private final HttpDataFactory factory;
55  
56      /**
57       * Request to decode
58       */
59      private final HttpRequest request;
60  
61      /**
62       * Default charset to use
63       */
64      private final Charset charset;
65  
66      /**
67       * Does the last chunk already received
68       */
69      private boolean isLastChunk;
70  
71      /**
72       * HttpDatas from Body
73       */
74      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
75  
76      /**
77       * HttpDatas as Map from Body
78       */
79      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
80              CaseIgnoringComparator.INSTANCE);
81  
82      /**
83       * The current channelBuffer
84       */
85      private ByteBuf undecodedChunk;
86  
87      /**
88       * Body HttpDatas current position
89       */
90      private int bodyListHttpDataRank;
91  
92      /**
93       * Current getStatus
94       */
95      private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
96  
97      /**
98       * The current Attribute that is currently in decode process
99       */
100     private Attribute currentAttribute;
101 
102     private boolean destroyed;
103 
104     private int discardThreshold = HttpPostRequestDecoder.DEFAULT_DISCARD_THRESHOLD;
105 
106     /**
107      *
108      * @param request
109      *            the request to decode
110      * @throws NullPointerException
111      *             for request
112      * @throws ErrorDataDecoderException
113      *             if the default charset was wrong when decoding or other
114      *             errors
115      */
116     public HttpPostStandardRequestDecoder(HttpRequest request) {
117         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET);
118     }
119 
120     /**
121      *
122      * @param factory
123      *            the factory used to create InterfaceHttpData
124      * @param request
125      *            the request to decode
126      * @throws NullPointerException
127      *             for request or factory
128      * @throws ErrorDataDecoderException
129      *             if the default charset was wrong when decoding or other
130      *             errors
131      */
132     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) {
133         this(factory, request, HttpConstants.DEFAULT_CHARSET);
134     }
135 
136     /**
137      *
138      * @param factory
139      *            the factory used to create InterfaceHttpData
140      * @param request
141      *            the request to decode
142      * @param charset
143      *            the charset to use as default
144      * @throws NullPointerException
145      *             for request or charset or factory
146      * @throws ErrorDataDecoderException
147      *             if the default charset was wrong when decoding or other
148      *             errors
149      */
150     public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
151         this.request = checkNotNull(request, "request");
152         this.charset = checkNotNull(charset, "charset");
153         this.factory = checkNotNull(factory, "factory");
154         try {
155             if (request instanceof HttpContent) {
156                 // Offer automatically if the given request is as type of HttpContent
157                 // See #1089
158                 offer((HttpContent) request);
159             } else {
160                 parseBody();
161             }
162         } catch (Throwable e) {
163             destroy();
164             PlatformDependent.throwException(e);
165         }
166     }
167 
168     private void checkDestroyed() {
169         if (destroyed) {
170             throw new IllegalStateException(HttpPostStandardRequestDecoder.class.getSimpleName()
171                     + " was destroyed already");
172         }
173     }
174 
175     /**
176      * True if this request is a Multipart request
177      *
178      * @return True if this request is a Multipart request
179      */
180     @Override
181     public boolean isMultipart() {
182         checkDestroyed();
183         return false;
184     }
185 
186     /**
187      * Set the amount of bytes after which read bytes in the buffer should be discarded.
188      * Setting this lower gives lower memory usage but with the overhead of more memory copies.
189      * Use {@code 0} to disable it.
190      */
191     @Override
192     public void setDiscardThreshold(int discardThreshold) {
193         this.discardThreshold = checkPositiveOrZero(discardThreshold, "discardThreshold");
194     }
195 
196     /**
197      * Return the threshold in bytes after which read data in the buffer should be discarded.
198      */
199     @Override
200     public int getDiscardThreshold() {
201         return discardThreshold;
202     }
203 
204     /**
205      * This getMethod returns a List of all HttpDatas from body.<br>
206      *
207      * If chunked, all chunks must have been offered using offer() getMethod. If
208      * not, NotEnoughDataDecoderException will be raised.
209      *
210      * @return the list of HttpDatas from Body part for POST getMethod
211      * @throws NotEnoughDataDecoderException
212      *             Need more chunks
213      */
214     @Override
215     public List<InterfaceHttpData> getBodyHttpDatas() {
216         checkDestroyed();
217 
218         if (!isLastChunk) {
219             throw new NotEnoughDataDecoderException();
220         }
221         return bodyListHttpData;
222     }
223 
224     /**
225      * This getMethod returns a List of all HttpDatas with the given name from
226      * body.<br>
227      *
228      * If chunked, all chunks must have been offered using offer() getMethod. If
229      * not, NotEnoughDataDecoderException will be raised.
230      *
231      * @return All Body HttpDatas with the given name (ignore case)
232      * @throws NotEnoughDataDecoderException
233      *             need more chunks
234      */
235     @Override
236     public List<InterfaceHttpData> getBodyHttpDatas(String name) {
237         checkDestroyed();
238 
239         if (!isLastChunk) {
240             throw new NotEnoughDataDecoderException();
241         }
242         return bodyMapHttpData.get(name);
243     }
244 
245     /**
246      * This getMethod returns the first InterfaceHttpData with the given name from
247      * body.<br>
248      *
249      * If chunked, all chunks must have been offered using offer() getMethod. If
250      * not, NotEnoughDataDecoderException will be raised.
251      *
252      * @return The first Body InterfaceHttpData with the given name (ignore
253      *         case)
254      * @throws NotEnoughDataDecoderException
255      *             need more chunks
256      */
257     @Override
258     public InterfaceHttpData getBodyHttpData(String name) {
259         checkDestroyed();
260 
261         if (!isLastChunk) {
262             throw new NotEnoughDataDecoderException();
263         }
264         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
265         if (list != null) {
266             return list.get(0);
267         }
268         return null;
269     }
270 
271     /**
272      * Initialized the internals from a new chunk
273      *
274      * @param content
275      *            the new received chunk
276      * @throws ErrorDataDecoderException
277      *             if there is a problem with the charset decoding or other
278      *             errors
279      */
280     @Override
281     public HttpPostStandardRequestDecoder offer(HttpContent content) {
282         checkDestroyed();
283 
284         if (content instanceof LastHttpContent) {
285             isLastChunk = true;
286         }
287 
288         ByteBuf buf = content.content();
289         if (undecodedChunk == null) {
290             undecodedChunk =
291                     // Since the Handler will release the incoming later on, we need to copy it
292                     //
293                     // We are explicit allocate a buffer and NOT calling copy() as otherwise it may set a maxCapacity
294                     // which is not really usable for us as we may exceed it once we add more bytes.
295                     buf.alloc().buffer(buf.readableBytes()).writeBytes(buf);
296         } else {
297             undecodedChunk.writeBytes(buf);
298         }
299         parseBody();
300         if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
301             if (undecodedChunk.refCnt() == 1) {
302                 // It's safe to call discardBytes() as we are the only owner of the buffer.
303                 undecodedChunk.discardReadBytes();
304             } else {
305                 // There seems to be multiple references of the buffer. Let's copy the data and release the buffer to
306                 // ensure we can give back memory to the system.
307                 ByteBuf buffer = undecodedChunk.alloc().buffer(undecodedChunk.readableBytes());
308                 buffer.writeBytes(undecodedChunk);
309                 undecodedChunk.release();
310                 undecodedChunk = buffer;
311             }
312         }
313         return this;
314     }
315 
316     /**
317      * True if at current getStatus, there is an available decoded
318      * InterfaceHttpData from the Body.
319      *
320      * This getMethod works for chunked and not chunked request.
321      *
322      * @return True if at current getStatus, there is a decoded InterfaceHttpData
323      * @throws EndOfDataDecoderException
324      *             No more data will be available
325      */
326     @Override
327     public boolean hasNext() {
328         checkDestroyed();
329 
330         if (currentStatus == MultiPartStatus.EPILOGUE) {
331             // OK except if end of list
332             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
333                 throw new EndOfDataDecoderException();
334             }
335         }
336         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
337     }
338 
339     /**
340      * Returns the next available InterfaceHttpData or null if, at the time it
341      * is called, there is no more available InterfaceHttpData. A subsequent
342      * call to offer(httpChunk) could enable more data.
343      *
344      * Be sure to call {@link InterfaceHttpData#release()} after you are done
345      * with processing to make sure to not leak any resources
346      *
347      * @return the next available InterfaceHttpData or null if none
348      * @throws EndOfDataDecoderException
349      *             No more data will be available
350      */
351     @Override
352     public InterfaceHttpData next() {
353         checkDestroyed();
354 
355         if (hasNext()) {
356             return bodyListHttpData.get(bodyListHttpDataRank++);
357         }
358         return null;
359     }
360 
361     @Override
362     public InterfaceHttpData currentPartialHttpData() {
363         return currentAttribute;
364     }
365 
366     /**
367      * This getMethod will parse as much as possible data and fill the list and map
368      *
369      * @throws ErrorDataDecoderException
370      *             if there is a problem with the charset decoding or other
371      *             errors
372      */
373     private void parseBody() {
374         if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
375             if (isLastChunk) {
376                 currentStatus = MultiPartStatus.EPILOGUE;
377             }
378             return;
379         }
380         parseBodyAttributes();
381     }
382 
383     /**
384      * Utility function to add a new decoded data
385      */
386     protected void addHttpData(InterfaceHttpData data) {
387         if (data == null) {
388             return;
389         }
390         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
391         if (datas == null) {
392             datas = new ArrayList<InterfaceHttpData>(1);
393             bodyMapHttpData.put(data.getName(), datas);
394         }
395         datas.add(data);
396         bodyListHttpData.add(data);
397     }
398 
399     /**
400      * This getMethod fill the map and list with as much Attribute as possible from
401      * Body in not Multipart mode.
402      *
403      * @throws ErrorDataDecoderException
404      *             if there is a problem with the charset decoding or other
405      *             errors
406      */
407     private void parseBodyAttributesStandard() {
408         int firstpos = undecodedChunk.readerIndex();
409         int currentpos = firstpos;
410         int equalpos;
411         int ampersandpos;
412         if (currentStatus == MultiPartStatus.NOTSTARTED) {
413             currentStatus = MultiPartStatus.DISPOSITION;
414         }
415         boolean contRead = true;
416         try {
417             while (undecodedChunk.isReadable() && contRead) {
418                 char read = (char) undecodedChunk.readUnsignedByte();
419                 currentpos++;
420                 switch (currentStatus) {
421                 case DISPOSITION:// search '='
422                     if (read == '=') {
423                         currentStatus = MultiPartStatus.FIELD;
424                         equalpos = currentpos - 1;
425                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
426                                 charset);
427                         currentAttribute = factory.createAttribute(request, key);
428                         firstpos = currentpos;
429                     } else if (read == '&') { // special empty FIELD
430                         currentStatus = MultiPartStatus.DISPOSITION;
431                         ampersandpos = currentpos - 1;
432                         String key = decodeAttribute(
433                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
434                         currentAttribute = factory.createAttribute(request, key);
435                         currentAttribute.setValue(""); // empty
436                         addHttpData(currentAttribute);
437                         currentAttribute = null;
438                         firstpos = currentpos;
439                         contRead = true;
440                     }
441                     break;
442                 case FIELD:// search '&' or end of line
443                     if (read == '&') {
444                         currentStatus = MultiPartStatus.DISPOSITION;
445                         ampersandpos = currentpos - 1;
446                         setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
447                         firstpos = currentpos;
448                         contRead = true;
449                     } else if (read == HttpConstants.CR) {
450                         if (undecodedChunk.isReadable()) {
451                             read = (char) undecodedChunk.readUnsignedByte();
452                             currentpos++;
453                             if (read == HttpConstants.LF) {
454                                 currentStatus = MultiPartStatus.PREEPILOGUE;
455                                 ampersandpos = currentpos - 2;
456                                 setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
457                                 firstpos = currentpos;
458                                 contRead = false;
459                             } else {
460                                 // Error
461                                 throw new ErrorDataDecoderException("Bad end of line");
462                             }
463                         } else {
464                             currentpos--;
465                         }
466                     } else if (read == HttpConstants.LF) {
467                         currentStatus = MultiPartStatus.PREEPILOGUE;
468                         ampersandpos = currentpos - 1;
469                         setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
470                         firstpos = currentpos;
471                         contRead = false;
472                     }
473                     break;
474                 default:
475                     // just stop
476                     contRead = false;
477                 }
478             }
479             if (isLastChunk && currentAttribute != null) {
480                 // special case
481                 ampersandpos = currentpos;
482                 if (ampersandpos > firstpos) {
483                     setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
484                 } else if (!currentAttribute.isCompleted()) {
485                     setFinalBuffer(Unpooled.EMPTY_BUFFER);
486                 }
487                 firstpos = currentpos;
488                 currentStatus = MultiPartStatus.EPILOGUE;
489             } else if (contRead && currentAttribute != null && currentStatus == MultiPartStatus.FIELD) {
490                 // reset index except if to continue in case of FIELD getStatus
491                 currentAttribute.addContent(undecodedChunk.retainedSlice(firstpos, currentpos - firstpos),
492                                             false);
493                 firstpos = currentpos;
494             }
495             undecodedChunk.readerIndex(firstpos);
496         } catch (ErrorDataDecoderException e) {
497             // error while decoding
498             undecodedChunk.readerIndex(firstpos);
499             throw e;
500         } catch (IOException e) {
501             // error while decoding
502             undecodedChunk.readerIndex(firstpos);
503             throw new ErrorDataDecoderException(e);
504         } catch (IllegalArgumentException e) {
505             // error while decoding
506             undecodedChunk.readerIndex(firstpos);
507             throw new ErrorDataDecoderException(e);
508         }
509     }
510 
511     /**
512      * This getMethod fill the map and list with as much Attribute as possible from
513      * Body in not Multipart mode.
514      *
515      * @throws ErrorDataDecoderException
516      *             if there is a problem with the charset decoding or other
517      *             errors
518      */
519     private void parseBodyAttributes() {
520         if (undecodedChunk == null) {
521             return;
522         }
523         if (!undecodedChunk.hasArray()) {
524             parseBodyAttributesStandard();
525             return;
526         }
527         SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
528         int firstpos = undecodedChunk.readerIndex();
529         int currentpos = firstpos;
530         int equalpos;
531         int ampersandpos;
532         if (currentStatus == MultiPartStatus.NOTSTARTED) {
533             currentStatus = MultiPartStatus.DISPOSITION;
534         }
535         boolean contRead = true;
536         try {
537             loop: while (sao.pos < sao.limit) {
538                 char read = (char) (sao.bytes[sao.pos++] & 0xFF);
539                 currentpos++;
540                 switch (currentStatus) {
541                 case DISPOSITION:// search '='
542                     if (read == '=') {
543                         currentStatus = MultiPartStatus.FIELD;
544                         equalpos = currentpos - 1;
545                         String key = decodeAttribute(undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
546                                 charset);
547                         currentAttribute = factory.createAttribute(request, key);
548                         firstpos = currentpos;
549                     } else if (read == '&') { // special empty FIELD
550                         currentStatus = MultiPartStatus.DISPOSITION;
551                         ampersandpos = currentpos - 1;
552                         String key = decodeAttribute(
553                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
554                         currentAttribute = factory.createAttribute(request, key);
555                         currentAttribute.setValue(""); // empty
556                         addHttpData(currentAttribute);
557                         currentAttribute = null;
558                         firstpos = currentpos;
559                         contRead = true;
560                     }
561                     break;
562                 case FIELD:// search '&' or end of line
563                     if (read == '&') {
564                         currentStatus = MultiPartStatus.DISPOSITION;
565                         ampersandpos = currentpos - 1;
566                         setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
567                         firstpos = currentpos;
568                         contRead = true;
569                     } else if (read == HttpConstants.CR) {
570                         if (sao.pos < sao.limit) {
571                             read = (char) (sao.bytes[sao.pos++] & 0xFF);
572                             currentpos++;
573                             if (read == HttpConstants.LF) {
574                                 currentStatus = MultiPartStatus.PREEPILOGUE;
575                                 ampersandpos = currentpos - 2;
576                                 sao.setReadPosition(0);
577                                 setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
578                                 firstpos = currentpos;
579                                 contRead = false;
580                                 break loop;
581                             } else {
582                                 // Error
583                                 sao.setReadPosition(0);
584                                 throw new ErrorDataDecoderException("Bad end of line");
585                             }
586                         } else {
587                             if (sao.limit > 0) {
588                                 currentpos--;
589                             }
590                         }
591                     } else if (read == HttpConstants.LF) {
592                         currentStatus = MultiPartStatus.PREEPILOGUE;
593                         ampersandpos = currentpos - 1;
594                         sao.setReadPosition(0);
595                         setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
596                         firstpos = currentpos;
597                         contRead = false;
598                         break loop;
599                     }
600                     break;
601                 default:
602                     // just stop
603                     sao.setReadPosition(0);
604                     contRead = false;
605                     break loop;
606                 }
607             }
608             if (isLastChunk && currentAttribute != null) {
609                 // special case
610                 ampersandpos = currentpos;
611                 if (ampersandpos > firstpos) {
612                     setFinalBuffer(undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos));
613                 } else if (!currentAttribute.isCompleted()) {
614                     setFinalBuffer(Unpooled.EMPTY_BUFFER);
615                 }
616                 firstpos = currentpos;
617                 currentStatus = MultiPartStatus.EPILOGUE;
618             } else if (contRead && currentAttribute != null && currentStatus == MultiPartStatus.FIELD) {
619                 // reset index except if to continue in case of FIELD getStatus
620                 currentAttribute.addContent(undecodedChunk.retainedSlice(firstpos, currentpos - firstpos),
621                                             false);
622                 firstpos = currentpos;
623             }
624             undecodedChunk.readerIndex(firstpos);
625         } catch (ErrorDataDecoderException e) {
626             // error while decoding
627             undecodedChunk.readerIndex(firstpos);
628             throw e;
629         } catch (IOException e) {
630             // error while decoding
631             undecodedChunk.readerIndex(firstpos);
632             throw new ErrorDataDecoderException(e);
633         } catch (IllegalArgumentException e) {
634             // error while decoding
635             undecodedChunk.readerIndex(firstpos);
636             throw new ErrorDataDecoderException(e);
637         }
638     }
639 
640     private void setFinalBuffer(ByteBuf buffer) throws IOException {
641         currentAttribute.addContent(buffer, true);
642         ByteBuf decodedBuf = decodeAttribute(currentAttribute.getByteBuf(), charset);
643         if (decodedBuf != null) { // override content only when ByteBuf needed decoding
644             currentAttribute.setContent(decodedBuf);
645         }
646         addHttpData(currentAttribute);
647         currentAttribute = null;
648     }
649 
650     /**
651      * Decode component
652      *
653      * @return the decoded component
654      */
655     private static String decodeAttribute(String s, Charset charset) {
656         try {
657             return QueryStringDecoder.decodeComponent(s, charset);
658         } catch (IllegalArgumentException e) {
659             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
660         }
661     }
662 
663     private static ByteBuf decodeAttribute(ByteBuf b, Charset charset) {
664         int firstEscaped = b.forEachByte(new UrlEncodedDetector());
665         if (firstEscaped == -1) {
666             return null; // nothing to decode
667         }
668 
669         ByteBuf buf = b.alloc().buffer(b.readableBytes());
670         UrlDecoder urlDecode = new UrlDecoder(buf);
671         int idx = b.forEachByte(urlDecode);
672         if (urlDecode.nextEscapedIdx != 0) { // incomplete hex byte
673             if (idx == -1) {
674                 idx = b.readableBytes() - 1;
675             }
676             idx -= urlDecode.nextEscapedIdx - 1;
677             buf.release();
678             throw new ErrorDataDecoderException(
679                 String.format("Invalid hex byte at index '%d' in string: '%s'", idx, b.toString(charset)));
680         }
681 
682         return buf;
683     }
684 
685     /**
686      * Destroy the {@link HttpPostStandardRequestDecoder} and release all it resources. After this method
687      * was called it is not possible to operate on it anymore.
688      */
689     @Override
690     public void destroy() {
691         // Release all data items, including those not yet pulled, only file based items
692         cleanFiles();
693         // Clean Memory based data
694         for (InterfaceHttpData httpData : bodyListHttpData) {
695             // Might have been already released by the user
696             if (httpData.refCnt() > 0) {
697                 httpData.release();
698             }
699         }
700 
701         destroyed = true;
702 
703         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
704             undecodedChunk.release();
705             undecodedChunk = null;
706         }
707     }
708 
709     /**
710      * Clean all {@link HttpData}s for the current request.
711      */
712     @Override
713     public void cleanFiles() {
714         checkDestroyed();
715 
716         factory.cleanRequestHttpData(request);
717     }
718 
719     /**
720      * Remove the given FileUpload from the list of FileUploads to clean
721      */
722     @Override
723     public void removeHttpDataFromClean(InterfaceHttpData data) {
724         checkDestroyed();
725 
726         factory.removeHttpDataFromClean(request, data);
727     }
728 
729     private static final class UrlEncodedDetector implements ByteProcessor {
730         @Override
731         public boolean process(byte value) throws Exception {
732             return value != '%' && value != '+';
733         }
734     }
735 
736     private static final class UrlDecoder implements ByteProcessor {
737 
738         private final ByteBuf output;
739         private int nextEscapedIdx;
740         private byte hiByte;
741 
742         UrlDecoder(ByteBuf output) {
743             this.output = output;
744         }
745 
746         @Override
747         public boolean process(byte value) {
748             if (nextEscapedIdx != 0) {
749                 if (nextEscapedIdx == 1) {
750                     hiByte = value;
751                     ++nextEscapedIdx;
752                 } else {
753                     int hi = StringUtil.decodeHexNibble((char) hiByte);
754                     int lo = StringUtil.decodeHexNibble((char) value);
755                     if (hi == -1 || lo == -1) {
756                         ++nextEscapedIdx;
757                         return false;
758                     }
759                     output.writeByte((hi << 4) + lo);
760                     nextEscapedIdx = 0;
761                 }
762             } else if (value == '%') {
763                 nextEscapedIdx = 1;
764             } else if (value == '+') {
765                 output.writeByte(' ');
766             } else {
767                 output.writeByte(value);
768             }
769             return true;
770         }
771     }
772 }