View Javadoc
1   /*
2    * Copyright 2025 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.lzf.ChunkDecoder;
19  import com.ning.compress.lzf.LZFChunk;
20  import com.ning.compress.lzf.LZFException;
21  import com.ning.compress.lzf.util.ChunkDecoderFactory;
22  import io.netty.buffer.ByteBuf;
23  import io.netty.buffer.ByteBufAllocator;
24  import io.netty.util.internal.UnstableApi;
25  
26  import static com.ning.compress.lzf.LZFChunk.BLOCK_TYPE_COMPRESSED;
27  import static com.ning.compress.lzf.LZFChunk.BLOCK_TYPE_NON_COMPRESSED;
28  import static com.ning.compress.lzf.LZFChunk.BYTE_V;
29  import static com.ning.compress.lzf.LZFChunk.BYTE_Z;
30  import static com.ning.compress.lzf.LZFChunk.HEADER_LEN_NOT_COMPRESSED;
31  
32  /**
33   * Uncompresses a {@link ByteBuf} encoded with the LZF format.
34   * <p>
35   * See original <a href="http://oldhome.schmorp.de/marc/liblzf.html">LZF package</a>
36   * and <a href="https://github.com/ning/compress/wiki/LZFFormat">LZF format</a> for full description.
37   */
38  @UnstableApi
39  public final class LzfDecompressor extends InputBufferingDecompressor {
40      /**
41       * Current state of decompression.
42       */
43      private enum State {
44          INIT_BLOCK,
45          INIT_ORIGINAL_LENGTH,
46          DECOMPRESS_DATA,
47          END,
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      private final ChunkDecoder decoder;
58  
59      /**
60       * Length of current received chunk of data.
61       */
62      private int chunkLength;
63  
64      /**
65       * Original length of current received chunk of data.
66       * It is equal to {@link #chunkLength} for non compressed chunks.
67       */
68      private int originalLength;
69  
70      /**
71       * Indicates whether this chunk is compressed.
72       */
73      private boolean isCompressed;
74  
75      LzfDecompressor(Builder builder, ByteBufAllocator allocator) {
76          super(allocator);
77          decoder = builder.safeInstance ?
78                  ChunkDecoderFactory.safeInstance()
79                  : ChunkDecoderFactory.optimalInstance();
80      }
81  
82      @Override
83      void processInput(ByteBuf buf) throws DecompressionException {
84          switch (currentState) {
85              case INIT_BLOCK:
86                  if (buf.readableBytes() < HEADER_LEN_NOT_COMPRESSED) {
87                      break;
88                  }
89                  final int magic = buf.readUnsignedShort();
90                  if (magic != MAGIC_NUMBER) {
91                      throw new DecompressionException("unexpected block identifier");
92                  }
93  
94                  final int type = buf.readByte();
95                  switch (type) {
96                      case BLOCK_TYPE_NON_COMPRESSED:
97                          isCompressed = false;
98                          currentState = State.DECOMPRESS_DATA;
99                          break;
100                     case BLOCK_TYPE_COMPRESSED:
101                         isCompressed = true;
102                         currentState = State.INIT_ORIGINAL_LENGTH;
103                         break;
104                     default:
105                         throw new DecompressionException(String.format(
106                                 "unknown type of chunk: %d (expected: %d or %d)",
107                                 type, BLOCK_TYPE_NON_COMPRESSED, BLOCK_TYPE_COMPRESSED));
108                 }
109                 chunkLength = buf.readUnsignedShort();
110 
111                 // chunkLength can never exceed MAX_CHUNK_LEN as MAX_CHUNK_LEN is 64kb and readUnsignedShort can
112                 // never return anything bigger as well. Let's add a check anyway to make things easier in terms
113                 // of debugging if we ever hit this because of a bug.
114                 if (chunkLength > LZFChunk.MAX_CHUNK_LEN) {
115                     throw new DecompressionException(String.format(
116                             "chunk length exceeds maximum: %d (expected: =< %d)",
117                             chunkLength, LZFChunk.MAX_CHUNK_LEN));
118                 }
119 
120                 if (type != BLOCK_TYPE_COMPRESSED) {
121                     break;
122                 }
123                 // fall through
124             case INIT_ORIGINAL_LENGTH:
125                 if (buf.readableBytes() < 2) {
126                     break;
127                 }
128                 originalLength = buf.readUnsignedShort();
129 
130                 // originalLength can never exceed MAX_CHUNK_LEN as MAX_CHUNK_LEN is 64kb and readUnsignedShort can
131                 // never return anything bigger as well. Let's add a check anyway to make things easier in terms
132                 // of debugging if we ever hit this because of a bug.
133                 if (originalLength > LZFChunk.MAX_CHUNK_LEN) {
134                     throw new DecompressionException(String.format(
135                             "original length exceeds maximum: %d (expected: =< %d)",
136                             originalLength, LZFChunk.MAX_CHUNK_LEN));
137                 }
138 
139                 currentState = State.DECOMPRESS_DATA;
140                 // fall through
141             case DECOMPRESS_DATA:
142 
143                 break;
144             default:
145                 throw new IllegalStateException();
146         }
147     }
148 
149     @Override
150     public Status status() throws DecompressionException {
151         switch (currentState) {
152             case INIT_BLOCK:
153             case INIT_ORIGINAL_LENGTH:
154                 return Status.NEED_INPUT;
155             case DECOMPRESS_DATA:
156                 return available() < chunkLength ? Status.NEED_INPUT : Status.NEED_OUTPUT;
157             case END:
158                 return Status.COMPLETE;
159             default:
160                 throw new AssertionError("Unknown state: " + currentState);
161         }
162     }
163 
164     @Override
165     public void endOfInput() throws DecompressionException {
166         if (currentState != State.INIT_BLOCK || available() != 0) {
167             throw new DecompressionException("Incomplete block");
168         }
169         currentState = State.END;
170     }
171 
172     @Override
173     ByteBuf processOutput(ByteBuf in) throws DecompressionException {
174         final int chunkLength = this.chunkLength;
175         if (in.readableBytes() < chunkLength) {
176             throw new IllegalStateException("Not in state NEED_OUTPUT");
177         }
178         final int originalLength = this.originalLength;
179 
180         if (isCompressed) {
181             ByteBuf arrayView;
182             if (!in.hasArray()) {
183                 arrayView = allocator.heapBuffer(chunkLength, chunkLength);
184                 arrayView.writeBytes(in, in.readerIndex(), chunkLength);
185             } else {
186                 arrayView = in;
187             }
188             final byte[] inputArray = arrayView.array();
189             final int inPos = arrayView.arrayOffset() + arrayView.readerIndex();
190 
191             ByteBuf uncompressed = null;
192             try {
193                 uncompressed = allocator.heapBuffer(originalLength, originalLength);
194                 final byte[] outputArray = uncompressed.array();
195                 final int outPos = uncompressed.arrayOffset() + uncompressed.writerIndex();
196                 decoder.decodeChunk(
197                         inputArray, inPos, inPos + chunkLength,
198                         outputArray, outPos, outPos + originalLength);
199                 uncompressed.writerIndex(uncompressed.writerIndex() + originalLength);
200                 in.skipBytes(chunkLength);
201                 currentState = State.INIT_BLOCK;
202                 ByteBuf output = uncompressed;
203                 uncompressed = null;
204                 return output;
205             } catch (LZFException e) {
206                 throw new DecompressionException(e);
207             } finally {
208                 if (uncompressed != null) {
209                     uncompressed.release();
210                 }
211                 if (arrayView != in) {
212                     arrayView.release();
213                 }
214             }
215         } else {
216             currentState = State.INIT_BLOCK;
217             return in.readRetainedSlice(chunkLength);
218         }
219     }
220 
221     @UnstableApi
222     public static Builder builder() {
223         return new Builder();
224     }
225 
226     @UnstableApi
227     public static final class Builder extends AbstractDecompressorBuilder {
228         private boolean safeInstance;
229 
230         Builder() {
231         }
232 
233         /**
234          * If {@code true} decoder will use {@link ChunkDecoder} that only uses standard JDK access methods,
235          * and should work on all Java platforms and JVMs.
236          * Otherwise decoder will try to use highly optimized {@link ChunkDecoder} implementation that uses
237          * Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
238          *
239          * @param safeInstance Whether to use the safe instance only
240          * @return This builder
241          */
242         @UnstableApi
243         public Builder safeInstance(boolean safeInstance) {
244             this.safeInstance = safeInstance;
245             return this;
246         }
247 
248         @Override
249         @UnstableApi
250         public Decompressor build(ByteBufAllocator allocator) throws DecompressionException {
251             return new DefensiveDecompressor(new LzfDecompressor(this, allocator));
252         }
253     }
254 }