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.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 }