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 }