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 io.netty5.buffer.api.Buffer;
19  import io.netty5.buffer.api.BufferAllocator;
20  
21  import java.util.function.Supplier;
22  import java.util.zip.Adler32;
23  import java.util.zip.Checksum;
24  
25  import static io.netty5.handler.codec.compression.FastLz.BLOCK_TYPE_COMPRESSED;
26  import static io.netty5.handler.codec.compression.FastLz.BLOCK_TYPE_NON_COMPRESSED;
27  import static io.netty5.handler.codec.compression.FastLz.BLOCK_WITHOUT_CHECKSUM;
28  import static io.netty5.handler.codec.compression.FastLz.BLOCK_WITH_CHECKSUM;
29  import static io.netty5.handler.codec.compression.FastLz.CHECKSUM_OFFSET;
30  import static io.netty5.handler.codec.compression.FastLz.LEVEL_1;
31  import static io.netty5.handler.codec.compression.FastLz.LEVEL_2;
32  import static io.netty5.handler.codec.compression.FastLz.LEVEL_AUTO;
33  import static io.netty5.handler.codec.compression.FastLz.MAGIC_NUMBER;
34  import static io.netty5.handler.codec.compression.FastLz.MAX_CHUNK_LENGTH;
35  import static io.netty5.handler.codec.compression.FastLz.MIN_LENGTH_TO_COMPRESSION;
36  import static io.netty5.handler.codec.compression.FastLz.OPTIONS_OFFSET;
37  import static io.netty5.handler.codec.compression.FastLz.calculateOutputBufferLength;
38  
39  /**
40   * Compresses a {@link Buffer} using the FastLZ algorithm.
41   *
42   * See <a href="https://github.com/netty/netty/issues/2750">FastLZ format</a>.
43   */
44  public final class FastLzCompressor implements Compressor {
45      /**
46       * Compression level.
47       */
48      private final int level;
49  
50      /**
51       * Underlying checksum calculator in use.
52       */
53      private final BufferChecksum checksum;
54  
55      private enum State {
56          PROCESSING,
57          FINISHED,
58          CLOSED
59      }
60  
61      private State state = State.PROCESSING;
62  
63      /**
64       * Creates a FastLZ encoder with specified compression level and checksum calculator.
65       *
66       * @param level supports only these values:
67       *        0 - Encoder will choose level automatically depending on the length of the input buffer.
68       *        1 - Level 1 is the fastest compression and generally useful for short data.
69       *        2 - Level 2 is slightly slower but it gives better compression ratio.
70       * @param checksum
71       *        the {@link Checksum} instance to use to check data for integrity.
72       *        You may set {@code null} if you don't want to validate checksum of each block.
73       */
74      private FastLzCompressor(int level, Checksum checksum) {
75          this.level = level;
76          this.checksum = checksum == null ? null : new BufferChecksum(checksum);
77      }
78  
79      /**
80       * Creates a FastLZ compressor factory without checksum calculator and with auto detection of compression level.
81       *
82       * @return the factory.
83       */
84      public static Supplier<FastLzCompressor> newFactory() {
85          return newFactory(LEVEL_AUTO, null);
86      }
87  
88      /**
89       * Creates a FastLZ compressor factory with specified compression level and without checksum calculator.
90       *
91       * @param level supports only these values:
92       *        0 - Encoder will choose level automatically depending on the length of the input buffer.
93       *        1 - Level 1 is the fastest compression and generally useful for short data.
94       *        2 - Level 2 is slightly slower but it gives better compression ratio.
95       * @return the factory.
96       */
97      public static Supplier<FastLzCompressor> newFactory(int level) {
98          return newFactory(level, null);
99      }
100 
101     /**
102      * Creates a FastLZ compressor factory with auto detection of compression
103      * level and calculation of checksums as specified.
104      *
105      * @param validateChecksums
106      *        If true, the checksum of each block will be calculated and this value
107      *        will be added to the header of block.
108      *        By default {@link FastLzCompressor} uses {@link java.util.zip.Adler32}
109      *        for checksum calculation.
110      * @return the factory.
111      */
112     public static Supplier<FastLzCompressor> newFactory(boolean validateChecksums) {
113         return newFactory(LEVEL_AUTO, validateChecksums ? new Adler32() : null);
114     }
115 
116     /**
117      * Creates a FastLZ compressor factory with specified compression level and checksum calculator.
118      *
119      * @param level supports only these values:
120      *        0 - Encoder will choose level automatically depending on the length of the input buffer.
121      *        1 - Level 1 is the fastest compression and generally useful for short data.
122      *        2 - Level 2 is slightly slower but it gives better compression ratio.
123      * @param checksum
124      *        the {@link Checksum} instance to use to check data for integrity.
125      *        You may set {@code null} if you don't want to validate checksum of each block.
126      * @return the factory.
127      */
128     public static Supplier<FastLzCompressor> newFactory(int level, Checksum checksum) {
129         if (level != LEVEL_AUTO && level != LEVEL_1 && level != LEVEL_2) {
130             throw new IllegalArgumentException(String.format(
131                     "level: %d (expected: %d or %d or %d)", level, LEVEL_AUTO, LEVEL_1, LEVEL_2));
132         }
133         return () -> new FastLzCompressor(level, checksum);
134     }
135 
136     @Override
137     public Buffer compress(Buffer in, BufferAllocator allocator) throws CompressionException {
138         switch (state) {
139             case CLOSED:
140                 throw new CompressionException("Compressor closed");
141             case FINISHED:
142                 return allocator.allocate(0);
143             case PROCESSING:
144                 return compressData(in, allocator);
145             default:
146                 throw new IllegalStateException();
147         }
148     }
149 
150     private Buffer compressData(Buffer in, BufferAllocator allocator) {
151         final BufferChecksum checksum = this.checksum;
152         // for text compression it can at max half the size, lets try to keep a bit of head-room.
153         Buffer out = allocator.allocate((int) ((double) in.readableBytes() / 1.5));
154         for (;;) {
155             if (in.readableBytes() == 0) {
156                 return out;
157             }
158             final int idx = in.readerOffset();
159             final int length = Math.min(in.readableBytes(), MAX_CHUNK_LENGTH);
160 
161             final int outputIdx = out.writerOffset();
162             out.ensureWritable(4);
163             out.setMedium(outputIdx, MAGIC_NUMBER);
164             int outputOffset = outputIdx + CHECKSUM_OFFSET + (checksum != null ? 4 : 0);
165 
166             final byte blockType;
167             final int chunkLength;
168             if (length < MIN_LENGTH_TO_COMPRESSION) {
169                 blockType = BLOCK_TYPE_NON_COMPRESSED;
170 
171                 out.ensureWritable(outputOffset + 2 + length);
172                 final int outputPtr = outputOffset + 2;
173 
174                 if (checksum != null) {
175                     checksum.reset();
176                     checksum.update(in, idx, length);
177                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
178                 }
179                 in.copyInto(idx, out, outputPtr, length);
180                 chunkLength = length;
181             } else {
182                 // try to compress
183                 if (checksum != null) {
184                     checksum.reset();
185                     checksum.update(in, idx, length);
186                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
187                 }
188 
189                 final int maxOutputLength = calculateOutputBufferLength(length);
190                 out.ensureWritable(outputOffset + 4 + maxOutputLength);
191                 final int outputPtr = outputOffset + 4;
192 
193                 final int compressedLength =
194                         FastLz.compress(in, in.readerOffset(), length, out, outputPtr, level);
195 
196                 if (compressedLength < length) {
197                     blockType = BLOCK_TYPE_COMPRESSED;
198                     chunkLength = compressedLength;
199 
200                     out.setShort(outputOffset, (short) chunkLength);
201                     outputOffset += 2;
202                 } else {
203                     blockType = BLOCK_TYPE_NON_COMPRESSED;
204                     in.copyInto(idx, out, outputOffset + 2, length);
205 
206                     chunkLength = length;
207                 }
208             }
209             out.setShort(outputOffset, (short) length);
210 
211             out.setByte(outputIdx + OPTIONS_OFFSET,
212                     (byte) (blockType | (checksum != null ? BLOCK_WITH_CHECKSUM : BLOCK_WITHOUT_CHECKSUM)));
213             out.writerOffset(outputOffset + 2 + chunkLength);
214             in.skipReadableBytes(length);
215         }
216     }
217 
218     @Override
219     public Buffer finish(BufferAllocator allocator) {
220         switch (state) {
221             case CLOSED:
222                 throw new CompressionException("Compressor closed");
223             case FINISHED:
224             case PROCESSING:
225                 state = State.FINISHED;
226                 return allocator.allocate(0);
227             default:
228                 throw new IllegalStateException();
229         }
230     }
231 
232     @Override
233     public boolean isFinished() {
234         return state != State.PROCESSING;
235     }
236 
237     @Override
238     public boolean isClosed() {
239         return state == State.CLOSED;
240     }
241 
242     @Override
243     public void close() {
244         state = State.CLOSED;
245     }
246 }