View Javadoc

1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.compression;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  
21  import java.util.List;
22  import java.util.zip.CRC32;
23  import java.util.zip.DataFormatException;
24  import java.util.zip.Deflater;
25  import java.util.zip.Inflater;
26  
27  /**
28   * Decompress a {@link ByteBuf} using the inflate algorithm.
29   */
30  public class JdkZlibDecoder extends ZlibDecoder {
31      private static final int FHCRC = 0x02;
32      private static final int FEXTRA = 0x04;
33      private static final int FNAME = 0x08;
34      private static final int FCOMMENT = 0x10;
35      private static final int FRESERVED = 0xE0;
36  
37      private Inflater inflater;
38      private final byte[] dictionary;
39  
40      // GZIP related
41      private final CRC32 crc;
42      private final boolean decompressConcatenated;
43  
44      private enum GzipState {
45          HEADER_START,
46          HEADER_END,
47          FLG_READ,
48          XLEN_READ,
49          SKIP_FNAME,
50          SKIP_COMMENT,
51          PROCESS_FHCRC,
52          FOOTER_START,
53      }
54  
55      private GzipState gzipState = GzipState.HEADER_START;
56      private int flags = -1;
57      private int xlen = -1;
58  
59      private volatile boolean finished;
60  
61      private boolean decideZlibOrNone;
62  
63      /**
64       * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}).
65       */
66      public JdkZlibDecoder() {
67          this(ZlibWrapper.ZLIB, null, false);
68      }
69  
70      /**
71       * Creates a new instance with the specified preset dictionary. The wrapper
72       * is always {@link ZlibWrapper#ZLIB} because it is the only format that
73       * supports the preset dictionary.
74       */
75      public JdkZlibDecoder(byte[] dictionary) {
76          this(ZlibWrapper.ZLIB, dictionary, false);
77      }
78  
79      /**
80       * Creates a new instance with the specified wrapper.
81       * Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are
82       * supported atm.
83       */
84      public JdkZlibDecoder(ZlibWrapper wrapper) {
85          this(wrapper, null, false);
86      }
87  
88      public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated) {
89          this(wrapper, null, decompressConcatenated);
90      }
91  
92      public JdkZlibDecoder(boolean decompressConcatenated) {
93          this(ZlibWrapper.GZIP, null, decompressConcatenated);
94      }
95  
96      private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated) {
97          if (wrapper == null) {
98              throw new NullPointerException("wrapper");
99          }
100         this.decompressConcatenated = decompressConcatenated;
101         switch (wrapper) {
102             case GZIP:
103                 inflater = new Inflater(true);
104                 crc = new CRC32();
105                 break;
106             case NONE:
107                 inflater = new Inflater(true);
108                 crc = null;
109                 break;
110             case ZLIB:
111                 inflater = new Inflater();
112                 crc = null;
113                 break;
114             case ZLIB_OR_NONE:
115                 // Postpone the decision until decode(...) is called.
116                 decideZlibOrNone = true;
117                 crc = null;
118                 break;
119             default:
120                 throw new IllegalArgumentException("Only GZIP or ZLIB is supported, but you used " + wrapper);
121         }
122         this.dictionary = dictionary;
123     }
124 
125     @Override
126     public boolean isClosed() {
127         return finished;
128     }
129 
130     @Override
131     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
132         if (finished) {
133             // Skip data received after finished.
134             in.skipBytes(in.readableBytes());
135             return;
136         }
137 
138         int readableBytes = in.readableBytes();
139         if (readableBytes == 0) {
140             return;
141         }
142 
143         if (decideZlibOrNone) {
144             // First two bytes are needed to decide if it's a ZLIB stream.
145             if (readableBytes < 2) {
146                 return;
147             }
148 
149             boolean nowrap = !looksLikeZlib(in.getShort(in.readerIndex()));
150             inflater = new Inflater(nowrap);
151             decideZlibOrNone = false;
152         }
153 
154         if (crc != null) {
155             switch (gzipState) {
156                 case FOOTER_START:
157                     if (readGZIPFooter(in)) {
158                         finished = true;
159                     }
160                     return;
161                 default:
162                     if (gzipState != GzipState.HEADER_END) {
163                         if (!readGZIPHeader(in)) {
164                             return;
165                         }
166                     }
167             }
168             // Some bytes may have been consumed, and so we must re-set the number of readable bytes.
169             readableBytes = in.readableBytes();
170         }
171 
172         if (in.hasArray()) {
173             inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), readableBytes);
174         } else {
175             byte[] array = new byte[readableBytes];
176             in.getBytes(in.readerIndex(), array);
177             inflater.setInput(array);
178         }
179 
180         ByteBuf decompressed = ctx.alloc().heapBuffer(inflater.getRemaining() << 1);
181         try {
182             boolean readFooter = false;
183             while (!inflater.needsInput()) {
184                 byte[] outArray = decompressed.array();
185                 int writerIndex = decompressed.writerIndex();
186                 int outIndex = decompressed.arrayOffset() + writerIndex;
187                 int outputLength = inflater.inflate(outArray, outIndex, decompressed.writableBytes());
188                 if (outputLength > 0) {
189                     decompressed.writerIndex(writerIndex + outputLength);
190                     if (crc != null) {
191                         crc.update(outArray, outIndex, outputLength);
192                     }
193                 } else {
194                     if (inflater.needsDictionary()) {
195                         if (dictionary == null) {
196                             throw new DecompressionException(
197                                     "decompression failure, unable to set dictionary as non was specified");
198                         }
199                         inflater.setDictionary(dictionary);
200                     }
201                 }
202 
203                 if (inflater.finished()) {
204                     if (crc == null) {
205                         finished = true; // Do not decode anymore.
206                     } else {
207                         readFooter = true;
208                     }
209                     break;
210                 } else {
211                     decompressed.ensureWritable(inflater.getRemaining() << 1);
212                 }
213             }
214 
215             in.skipBytes(readableBytes - inflater.getRemaining());
216 
217             if (readFooter) {
218                 gzipState = GzipState.FOOTER_START;
219                 if (readGZIPFooter(in)) {
220                     finished = !decompressConcatenated;
221 
222                     if (!finished) {
223                         inflater.reset();
224                         crc.reset();
225                         gzipState = GzipState.HEADER_START;
226                     }
227                 }
228             }
229         } catch (DataFormatException e) {
230             throw new DecompressionException("decompression failure", e);
231         } finally {
232 
233             if (decompressed.isReadable()) {
234                 out.add(decompressed);
235             } else {
236                 decompressed.release();
237             }
238         }
239     }
240 
241     @Override
242     protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
243         super.handlerRemoved0(ctx);
244         if (inflater != null) {
245             inflater.end();
246         }
247     }
248 
249     private boolean readGZIPHeader(ByteBuf in) {
250         switch (gzipState) {
251             case HEADER_START:
252                 if (in.readableBytes() < 10) {
253                     return false;
254                 }
255                 // read magic numbers
256                 int magic0 = in.readByte();
257                 int magic1 = in.readByte();
258 
259                 if (magic0 != 31) {
260                     throw new DecompressionException("Input is not in the GZIP format");
261                 }
262                 crc.update(magic0);
263                 crc.update(magic1);
264 
265                 int method = in.readUnsignedByte();
266                 if (method != Deflater.DEFLATED) {
267                     throw new DecompressionException("Unsupported compression method "
268                             + method + " in the GZIP header");
269                 }
270                 crc.update(method);
271 
272                 flags = in.readUnsignedByte();
273                 crc.update(flags);
274 
275                 if ((flags & FRESERVED) != 0) {
276                     throw new DecompressionException(
277                             "Reserved flags are set in the GZIP header");
278                 }
279 
280                 // mtime (int)
281                 crc.update(in.readByte());
282                 crc.update(in.readByte());
283                 crc.update(in.readByte());
284                 crc.update(in.readByte());
285 
286                 crc.update(in.readUnsignedByte()); // extra flags
287                 crc.update(in.readUnsignedByte()); // operating system
288 
289                 gzipState = GzipState.FLG_READ;
290                 // fall through
291             case FLG_READ:
292                 if ((flags & FEXTRA) != 0) {
293                     if (in.readableBytes() < 2) {
294                         return false;
295                     }
296                     int xlen1 = in.readUnsignedByte();
297                     int xlen2 = in.readUnsignedByte();
298                     crc.update(xlen1);
299                     crc.update(xlen2);
300 
301                     xlen |= xlen1 << 8 | xlen2;
302                 }
303                 gzipState = GzipState.XLEN_READ;
304                 // fall through
305             case XLEN_READ:
306                 if (xlen != -1) {
307                     if (in.readableBytes() < xlen) {
308                         return false;
309                     }
310                     byte[] xtra = new byte[xlen];
311                     in.readBytes(xtra);
312                     crc.update(xtra);
313                 }
314                 gzipState = GzipState.SKIP_FNAME;
315                 // fall through
316             case SKIP_FNAME:
317                 if ((flags & FNAME) != 0) {
318                     if (!in.isReadable()) {
319                         return false;
320                     }
321                     do {
322                         int b = in.readUnsignedByte();
323                         crc.update(b);
324                         if (b == 0x00) {
325                             break;
326                         }
327                     } while (in.isReadable());
328                 }
329                 gzipState = GzipState.SKIP_COMMENT;
330                 // fall through
331             case SKIP_COMMENT:
332                 if ((flags & FCOMMENT) != 0) {
333                     if (!in.isReadable()) {
334                         return false;
335                     }
336                     do {
337                         int b = in.readUnsignedByte();
338                         crc.update(b);
339                         if (b == 0x00) {
340                             break;
341                         }
342                     } while (in.isReadable());
343                 }
344                 gzipState = GzipState.PROCESS_FHCRC;
345                 // fall through
346             case PROCESS_FHCRC:
347                 if ((flags & FHCRC) != 0) {
348                     if (in.readableBytes() < 4) {
349                         return false;
350                     }
351                     verifyCrc(in);
352                 }
353                 crc.reset();
354                 gzipState = GzipState.HEADER_END;
355                 // fall through
356             case HEADER_END:
357                 return true;
358             default:
359                 throw new IllegalStateException();
360         }
361     }
362 
363     private boolean readGZIPFooter(ByteBuf buf) {
364         if (buf.readableBytes() < 8) {
365             return false;
366         }
367 
368         verifyCrc(buf);
369 
370         // read ISIZE and verify
371         int dataLength = 0;
372         for (int i = 0; i < 4; ++i) {
373             dataLength |= buf.readUnsignedByte() << i * 8;
374         }
375         int readLength = inflater.getTotalOut();
376         if (dataLength != readLength) {
377             throw new DecompressionException(
378                     "Number of bytes mismatch. Expected: " + dataLength + ", Got: " + readLength);
379         }
380         return true;
381     }
382 
383     private void verifyCrc(ByteBuf in) {
384         long crcValue = 0;
385         for (int i = 0; i < 4; ++i) {
386             crcValue |= (long) in.readUnsignedByte() << i * 8;
387         }
388         long readCrc = crc.getValue();
389         if (crcValue != readCrc) {
390             throw new DecompressionException(
391                     "CRC value mismatch. Expected: " + crcValue + ", Got: " + readCrc);
392         }
393     }
394 
395     /*
396      * Returns true if the cmf_flg parameter (think: first two bytes of a zlib stream)
397      * indicates that this is a zlib stream.
398      * <p>
399      * You can lookup the details in the ZLIB RFC:
400      * <a href="http://tools.ietf.org/html/rfc1950#section-2.2">RFC 1950</a>.
401      */
402     private static boolean looksLikeZlib(short cmf_flg) {
403         return (cmf_flg & 0x7800) == 0x7800 &&
404                 cmf_flg % 31 == 0;
405     }
406 }