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