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