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.netty.handler.codec.compression;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.handler.codec.MessageToByteEncoder;
21  
22  import java.util.zip.Adler32;
23  import java.util.zip.Checksum;
24  
25  import static io.netty.handler.codec.compression.FastLz.BLOCK_TYPE_COMPRESSED;
26  import static io.netty.handler.codec.compression.FastLz.BLOCK_TYPE_NON_COMPRESSED;
27  import static io.netty.handler.codec.compression.FastLz.BLOCK_WITHOUT_CHECKSUM;
28  import static io.netty.handler.codec.compression.FastLz.BLOCK_WITH_CHECKSUM;
29  import static io.netty.handler.codec.compression.FastLz.CHECKSUM_OFFSET;
30  import static io.netty.handler.codec.compression.FastLz.LEVEL_1;
31  import static io.netty.handler.codec.compression.FastLz.LEVEL_2;
32  import static io.netty.handler.codec.compression.FastLz.LEVEL_AUTO;
33  import static io.netty.handler.codec.compression.FastLz.MAGIC_NUMBER;
34  import static io.netty.handler.codec.compression.FastLz.MAX_CHUNK_LENGTH;
35  import static io.netty.handler.codec.compression.FastLz.MIN_LENGTH_TO_COMPRESSION;
36  import static io.netty.handler.codec.compression.FastLz.OPTIONS_OFFSET;
37  import static io.netty.handler.codec.compression.FastLz.calculateOutputBufferLength;
38  import static io.netty.handler.codec.compression.FastLz.compress;
39  
40  /**
41   * Compresses a {@link ByteBuf} using the FastLZ algorithm.
42   *
43   * See <a href="https://github.com/netty/netty/issues/2750">FastLZ format</a>.
44   */
45  public class FastLzFrameEncoder extends MessageToByteEncoder<ByteBuf> {
46      /**
47       * Compression level.
48       */
49      private final int level;
50  
51      /**
52       * Underlying checksum calculator in use.
53       */
54      private final ByteBufChecksum checksum;
55  
56      /**
57       * Creates a FastLZ encoder without checksum calculator and with auto detection of compression level.
58       */
59      public FastLzFrameEncoder() {
60          this(LEVEL_AUTO, null);
61      }
62  
63      /**
64       * Creates a FastLZ encoder with specified compression level and without 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       */
71      public FastLzFrameEncoder(int level) {
72          this(level, null);
73      }
74  
75      /**
76       * Creates a FastLZ encoder with auto detection of compression
77       * level and calculation of checksums as specified.
78       *
79       * @param validateChecksums
80       *        If true, the checksum of each block will be calculated and this value
81       *        will be added to the header of block.
82       *        By default {@link FastLzFrameEncoder} uses {@link java.util.zip.Adler32}
83       *        for checksum calculation.
84       */
85      public FastLzFrameEncoder(boolean validateChecksums) {
86          this(LEVEL_AUTO, validateChecksums ? new Adler32() : null);
87      }
88  
89      /**
90       * Creates a FastLZ encoder with specified compression level and checksum calculator.
91       *
92       * @param level supports only these values:
93       *        0 - Encoder will choose level automatically depending on the length of the input buffer.
94       *        1 - Level 1 is the fastest compression and generally useful for short data.
95       *        2 - Level 2 is slightly slower but it gives better compression ratio.
96       * @param checksum
97       *        the {@link Checksum} instance to use to check data for integrity.
98       *        You may set {@code null} if you don't want to validate checksum of each block.
99       */
100     public FastLzFrameEncoder(int level, Checksum checksum) {
101         if (level != LEVEL_AUTO && level != LEVEL_1 && level != LEVEL_2) {
102             throw new IllegalArgumentException(String.format(
103                     "level: %d (expected: %d or %d or %d)", level, LEVEL_AUTO, LEVEL_1, LEVEL_2));
104         }
105         this.level = level;
106         this.checksum = checksum == null ? null : ByteBufChecksum.wrapChecksum(checksum);
107     }
108 
109     @Override
110     protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception {
111         final ByteBufChecksum checksum = this.checksum;
112 
113         for (;;) {
114             if (!in.isReadable()) {
115                 return;
116             }
117             final int idx = in.readerIndex();
118             final int length = Math.min(in.readableBytes(), MAX_CHUNK_LENGTH);
119 
120             final int outputIdx = out.writerIndex();
121             out.setMedium(outputIdx, MAGIC_NUMBER);
122             int outputOffset = outputIdx + CHECKSUM_OFFSET + (checksum != null ? 4 : 0);
123 
124             final byte blockType;
125             final int chunkLength;
126             if (length < MIN_LENGTH_TO_COMPRESSION) {
127                 blockType = BLOCK_TYPE_NON_COMPRESSED;
128 
129                 out.ensureWritable(outputOffset + 2 + length);
130                 final int outputPtr = outputOffset + 2;
131 
132                 if (checksum != null) {
133                     checksum.reset();
134                     checksum.update(in, idx, length);
135                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
136                 }
137                 out.setBytes(outputPtr, in, idx, length);
138                 chunkLength = length;
139             } else {
140                 // try to compress
141                 if (checksum != null) {
142                     checksum.reset();
143                     checksum.update(in, idx, length);
144                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
145                 }
146 
147                 final int maxOutputLength = calculateOutputBufferLength(length);
148                 out.ensureWritable(outputOffset + 4 + maxOutputLength);
149                 final int outputPtr = outputOffset + 4;
150                 final int compressedLength = compress(in, in.readerIndex(), length, out, outputPtr, level);
151 
152                 if (compressedLength < length) {
153                     blockType = BLOCK_TYPE_COMPRESSED;
154                     chunkLength = compressedLength;
155 
156                     out.setShort(outputOffset, chunkLength);
157                     outputOffset += 2;
158                 } else {
159                     blockType = BLOCK_TYPE_NON_COMPRESSED;
160                     out.setBytes(outputOffset + 2, in, idx, length);
161                     chunkLength = length;
162                 }
163             }
164             out.setShort(outputOffset, length);
165 
166             out.setByte(outputIdx + OPTIONS_OFFSET,
167                     blockType | (checksum != null ? BLOCK_WITH_CHECKSUM : BLOCK_WITHOUT_CHECKSUM));
168             out.writerIndex(outputOffset + 2 + chunkLength);
169             in.skipBytes(length);
170         }
171     }
172 }