View Javadoc
1   /*
2    * Copyright 2014 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 com.ning.compress.BufferRecycler;
19  import com.ning.compress.lzf.ChunkDecoder;
20  import com.ning.compress.lzf.util.ChunkDecoderFactory;
21  import io.netty.buffer.ByteBuf;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.handler.codec.ByteToMessageDecoder;
24  
25  import java.util.List;
26  
27  import static com.ning.compress.lzf.LZFChunk.BLOCK_TYPE_COMPRESSED;
28  import static com.ning.compress.lzf.LZFChunk.BLOCK_TYPE_NON_COMPRESSED;
29  import static com.ning.compress.lzf.LZFChunk.BYTE_V;
30  import static com.ning.compress.lzf.LZFChunk.BYTE_Z;
31  import static com.ning.compress.lzf.LZFChunk.HEADER_LEN_NOT_COMPRESSED;
32  
33  /**
34   * Uncompresses a {@link ByteBuf} encoded with the LZF format.
35   *
36   * See original <a href="http://oldhome.schmorp.de/marc/liblzf.html">LZF package</a>
37   * and <a href="https://github.com/ning/compress/wiki/LZFFormat">LZF format</a> for full description.
38   */
39  public class LzfDecoder extends ByteToMessageDecoder {
40      /**
41       * Current state of decompression.
42       */
43      private enum State {
44          INIT_BLOCK,
45          INIT_ORIGINAL_LENGTH,
46          DECOMPRESS_DATA,
47          CORRUPTED
48      }
49  
50      private State currentState = State.INIT_BLOCK;
51  
52      /**
53       * Magic number of LZF chunk.
54       */
55      private static final short MAGIC_NUMBER = BYTE_Z << 8 | BYTE_V;
56  
57      /**
58       * Underlying decoder in use.
59       */
60      private ChunkDecoder decoder;
61  
62      /**
63       * Object that handles details of buffer recycling.
64       */
65      private BufferRecycler recycler;
66  
67      /**
68       * Length of current received chunk of data.
69       */
70      private int chunkLength;
71  
72      /**
73       * Original length of current received chunk of data.
74       * It is equal to {@link #chunkLength} for non compressed chunks.
75       */
76      private int originalLength;
77  
78      /**
79       * Indicates is this chunk compressed or not.
80       */
81      private boolean isCompressed;
82  
83      /**
84       * Creates a new LZF decoder with the most optimal available methods for underlying data access.
85       * It will "unsafe" instance if one can be used on current JVM.
86       * It should be safe to call this constructor as implementations are dynamically loaded; however, on some
87       * non-standard platforms it may be necessary to use {@link #LzfDecoder(boolean)} with {@code true} param.
88       */
89      public LzfDecoder() {
90          this(false);
91      }
92  
93      /**
94       * Creates a new LZF decoder with specified decoding instance.
95       *
96       * @param safeInstance
97       *        If {@code true} decoder will use {@link ChunkDecoder} that only uses standard JDK access methods,
98       *        and should work on all Java platforms and JVMs.
99       *        Otherwise decoder will try to use highly optimized {@link ChunkDecoder} implementation that uses
100      *        Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
101      */
102     public LzfDecoder(boolean safeInstance) {
103         decoder = safeInstance ?
104                 ChunkDecoderFactory.safeInstance()
105               : ChunkDecoderFactory.optimalInstance();
106 
107         recycler = BufferRecycler.instance();
108     }
109 
110     @Override
111     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
112         try {
113             switch (currentState) {
114             case INIT_BLOCK:
115                 if (in.readableBytes() < HEADER_LEN_NOT_COMPRESSED) {
116                     break;
117                 }
118                 final int magic = in.readUnsignedShort();
119                 if (magic != MAGIC_NUMBER) {
120                     throw new DecompressionException("unexpected block identifier");
121                 }
122 
123                 final int type = in.readByte();
124                 switch (type) {
125                 case BLOCK_TYPE_NON_COMPRESSED:
126                     isCompressed = false;
127                     currentState = State.DECOMPRESS_DATA;
128                     break;
129                 case BLOCK_TYPE_COMPRESSED:
130                     isCompressed = true;
131                     currentState = State.INIT_ORIGINAL_LENGTH;
132                     break;
133                 default:
134                     throw new DecompressionException(String.format(
135                             "unknown type of chunk: %d (expected: %d or %d)",
136                             type, BLOCK_TYPE_NON_COMPRESSED, BLOCK_TYPE_COMPRESSED));
137                 }
138                 chunkLength = in.readUnsignedShort();
139 
140                 if (type != BLOCK_TYPE_COMPRESSED) {
141                     break;
142                 }
143                 // fall through
144             case INIT_ORIGINAL_LENGTH:
145                 if (in.readableBytes() < 2) {
146                     break;
147                 }
148                 originalLength = in.readUnsignedShort();
149 
150                 currentState = State.DECOMPRESS_DATA;
151                 // fall through
152             case DECOMPRESS_DATA:
153                 final int chunkLength = this.chunkLength;
154                 if (in.readableBytes() < chunkLength) {
155                     break;
156                 }
157                 final int originalLength = this.originalLength;
158 
159                 if (isCompressed) {
160                     final int idx = in.readerIndex();
161 
162                     final byte[] inputArray;
163                     final int inPos;
164                     if (in.hasArray()) {
165                         inputArray = in.array();
166                         inPos = in.arrayOffset() + idx;
167                     } else {
168                         inputArray = recycler.allocInputBuffer(chunkLength);
169                         in.getBytes(idx, inputArray, 0, chunkLength);
170                         inPos = 0;
171                     }
172 
173                     ByteBuf uncompressed = ctx.alloc().heapBuffer(originalLength, originalLength);
174                     final byte[] outputArray = uncompressed.array();
175                     final int outPos = uncompressed.arrayOffset() + uncompressed.writerIndex();
176 
177                     boolean success = false;
178                     try {
179                         decoder.decodeChunk(inputArray, inPos, outputArray, outPos, outPos + originalLength);
180                         uncompressed.writerIndex(uncompressed.writerIndex() + originalLength);
181                         out.add(uncompressed);
182                         in.skipBytes(chunkLength);
183                         success = true;
184                     } finally {
185                         if (!success) {
186                             uncompressed.release();
187                         }
188                     }
189 
190                     if (!in.hasArray()) {
191                         recycler.releaseInputBuffer(inputArray);
192                     }
193                 } else if (chunkLength > 0) {
194                     out.add(in.readRetainedSlice(chunkLength));
195                 }
196 
197                 currentState = State.INIT_BLOCK;
198                 break;
199             case CORRUPTED:
200                 in.skipBytes(in.readableBytes());
201                 break;
202             default:
203                 throw new IllegalStateException();
204             }
205         } catch (Exception e) {
206             currentState = State.CORRUPTED;
207             decoder = null;
208             recycler = null;
209             throw e;
210         }
211     }
212 }