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.netty5.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.LZFException;
22 import com.ning.compress.lzf.util.ChunkDecoderFactory;
23 import io.netty5.buffer.api.Buffer;
24 import io.netty5.buffer.api.BufferAllocator;
25
26 import java.util.function.Supplier;
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 Buffer} 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 final class LzfDecompressor implements Decompressor {
41
42 /**
43 * Current state of decompression.
44 */
45 private enum State {
46 INIT_BLOCK,
47 INIT_ORIGINAL_LENGTH,
48 DECOMPRESS_DATA,
49 CORRUPTED,
50 FINISHED,
51 CLOSED
52 }
53
54 private State currentState = State.INIT_BLOCK;
55
56 /**
57 * Magic number of LZF chunk.
58 */
59 private static final short MAGIC_NUMBER = BYTE_Z << 8 | BYTE_V;
60
61 /**
62 * Underlying decoder in use.
63 */
64 private ChunkDecoder decoder;
65
66 /**
67 * Object that handles details of buffer recycling.
68 */
69 private BufferRecycler recycler;
70
71 /**
72 * Length of current received chunk of data.
73 */
74 private int chunkLength;
75
76 /**
77 * Original length of current received chunk of data.
78 * It is equal to {@link #chunkLength} for non compressed chunks.
79 */
80 private int originalLength;
81
82 /**
83 * Indicates is this chunk compressed or not.
84 */
85 private boolean isCompressed;
86
87 /**
88 * Creates a new LZF decompressor with specified decoding instance.
89 *
90 * @param safeInstance
91 * If {@code true} decoder will use {@link ChunkDecoder} that only uses standard JDK access methods,
92 * and should work on all Java platforms and JVMs.
93 * Otherwise decoder will try to use highly optimized {@link ChunkDecoder} implementation that uses
94 * Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
95 */
96 private LzfDecompressor(boolean safeInstance) {
97 decoder = safeInstance ?
98 ChunkDecoderFactory.safeInstance()
99 : ChunkDecoderFactory.optimalInstance();
100
101 recycler = BufferRecycler.instance();
102 }
103
104 /**
105 * Creates a new LZF decompressor factory with the most optimal available methods for underlying data access.
106 * It will "unsafe" instance if one can be used on current JVM.
107 * It should be safe to call this constructor as implementations are dynamically loaded; however, on some
108 * non-standard platforms it may be necessary to use {@link #LzfDecompressor(boolean)} with {@code true} param.
109 *
110 * @return the factory.
111 */
112 public static Supplier<LzfDecompressor> newFactory() {
113 return newFactory(false);
114 }
115
116 /**
117 * Creates a new LZF decompressor factory with specified decoding instance.
118 *
119 * @param safeInstance
120 * If {@code true} decoder will use {@link ChunkDecoder} that only uses standard JDK access methods,
121 * and should work on all Java platforms and JVMs.
122 * Otherwise decoder will try to use highly optimized {@link ChunkDecoder} implementation that uses
123 * Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
124 * @return the factory.
125 */
126 public static Supplier<LzfDecompressor> newFactory(boolean safeInstance) {
127 return () -> new LzfDecompressor(safeInstance);
128 }
129
130 @Override
131 public Buffer decompress(Buffer in, BufferAllocator allocator) throws DecompressionException {
132 try {
133 switch (currentState) {
134 case FINISHED:
135 case CORRUPTED:
136 return allocator.allocate(0);
137 case CLOSED:
138 throw new DecompressionException("Decompressor closed");
139 case INIT_BLOCK:
140 if (in.readableBytes() < HEADER_LEN_NOT_COMPRESSED) {
141 return null;
142 }
143 final int magic = in.readUnsignedShort();
144 if (magic != MAGIC_NUMBER) {
145 streamCorrupted("unexpected block identifier");
146 }
147
148 final int type = in.readByte();
149 switch (type) {
150 case BLOCK_TYPE_NON_COMPRESSED:
151 isCompressed = false;
152 currentState = State.DECOMPRESS_DATA;
153 break;
154 case BLOCK_TYPE_COMPRESSED:
155 isCompressed = true;
156 currentState = State.INIT_ORIGINAL_LENGTH;
157 break;
158 default:
159 streamCorrupted(String.format(
160 "unknown type of chunk: %d (expected: %d or %d)",
161 type, BLOCK_TYPE_NON_COMPRESSED, BLOCK_TYPE_COMPRESSED));
162 }
163 chunkLength = in.readUnsignedShort();
164
165 // chunkLength can never exceed MAX_CHUNK_LEN as MAX_CHUNK_LEN is 64kb and readUnsignedShort can
166 // never return anything bigger as well. Let's add some check any way to make things easier in
167 // terms of debugging if we ever hit this because of an bug.
168 if (chunkLength > LZFChunk.MAX_CHUNK_LEN) {
169 streamCorrupted(String.format(
170 "chunk length exceeds maximum: %d (expected: =< %d)",
171 chunkLength, LZFChunk.MAX_CHUNK_LEN));
172 }
173
174 if (type != BLOCK_TYPE_COMPRESSED) {
175 return null;
176 }
177 // fall through
178 case INIT_ORIGINAL_LENGTH:
179 if (in.readableBytes() < 2) {
180 return null;
181 }
182 originalLength = in.readUnsignedShort();
183
184 // originalLength can never exceed MAX_CHUNK_LEN as MAX_CHUNK_LEN is 64kb and readUnsignedShort
185 // can never return anything bigger as well. Let's add some check any way to make things easier
186 // in terms of debugging if we ever hit this because of an bug.
187 if (originalLength > LZFChunk.MAX_CHUNK_LEN) {
188 streamCorrupted(String.format(
189 "original length exceeds maximum: %d (expected: =< %d)",
190 chunkLength, LZFChunk.MAX_CHUNK_LEN));
191 }
192
193 currentState = State.DECOMPRESS_DATA;
194 // fall through
195 case DECOMPRESS_DATA:
196 final int chunkLength = this.chunkLength;
197 if (in.readableBytes() < chunkLength) {
198 return null;
199 }
200 final int originalLength = this.originalLength;
201
202 if (isCompressed) {
203 if (in.countReadableComponents() == 1) {
204 try (var readableIteration = in.forEachReadable()) {
205 var readableComponent = readableIteration.first();
206 if (readableComponent.hasReadableArray()) {
207 byte[] inputArray = readableComponent.readableArray();
208 int inPos = readableComponent.readableArrayOffset();
209 try {
210 Buffer out = decompress(allocator, inputArray, inPos, originalLength);
211 in.skipReadableBytes(chunkLength);
212 currentState = State.INIT_BLOCK;
213 return out;
214 } finally {
215 if (!readableComponent.hasReadableArray()) {
216 recycler.releaseInputBuffer(inputArray);
217 }
218 }
219 }
220 }
221 }
222 final int idx = in.readerOffset();
223 byte[] inputArray = recycler.allocInputBuffer(chunkLength);
224 in.copyInto(idx, inputArray, 0, chunkLength);
225 try {
226 Buffer out = decompress(allocator, inputArray, 0, originalLength);
227 in.skipReadableBytes(chunkLength);
228 currentState = State.INIT_BLOCK;
229 return out;
230 } finally {
231 recycler.releaseInputBuffer(inputArray);
232 }
233 } else if (chunkLength > 0) {
234 currentState = State.INIT_BLOCK;
235 return in.readSplit(chunkLength);
236 } else {
237 currentState = State.INIT_BLOCK;
238 }
239
240 return null;
241 default:
242 throw new IllegalStateException();
243 }
244 } catch (Exception e) {
245 currentState = State.CORRUPTED;
246 decoder = null;
247 recycler = null;
248 if (e instanceof DecompressionException) {
249 throw (DecompressionException) e;
250 }
251 throw new DecompressionException(e);
252 }
253 }
254
255 private Buffer decompress(BufferAllocator allocator, byte[] inputArray, int offset, int len)
256 throws LZFException {
257 byte[] outputArray = recycler.allocOutputBuffer(len);
258 try {
259 decoder.decodeChunk(inputArray, offset,
260 outputArray, 0, len);
261 return allocator.allocate(len)
262 .writeBytes(outputArray, 0, len);
263 } finally {
264 recycler.releaseOutputBuffer(outputArray);
265 }
266 }
267
268 private void streamCorrupted(String message) {
269 currentState = State.CORRUPTED;
270 throw new DecompressionException(message);
271 }
272
273 @Override
274 public boolean isFinished() {
275 switch (currentState) {
276 case FINISHED:
277 case CLOSED:
278 case CORRUPTED:
279 return true;
280 default:
281 return false;
282 }
283 }
284
285 @Override
286 public boolean isClosed() {
287 return currentState == State.CLOSED;
288 }
289
290 @Override
291 public void close() {
292 currentState = State.CLOSED;
293 }
294 }