View Javadoc
1   /*
2    * Copyright 2012 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.nio.ByteBuffer;
22  import java.util.function.Supplier;
23  import java.util.zip.CRC32;
24  import java.util.zip.Deflater;
25  
26  import static io.netty5.util.internal.ObjectUtil.checkInRange;
27  import static java.util.Objects.requireNonNull;
28  
29  /**
30   * Compresses a {@link Buffer} using the deflate algorithm.
31   */
32  public final class ZlibCompressor implements Compressor {
33      private final ZlibWrapper wrapper;
34      private final Deflater deflater;
35  
36      /*
37       * GZIP support
38       */
39      private final CRC32 crc = new CRC32();
40      private static final byte[] gzipHeader = {0x1f, (byte) 0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0};
41  
42      private enum State {
43          PROCESSING,
44          FINISHED,
45          CLOSED
46      }
47      private State state = State.PROCESSING;
48      private boolean writeHeader = true;
49  
50      private ZlibCompressor(ZlibWrapper wrapper, int compressionLevel) {
51          this.wrapper = wrapper;
52          deflater = new Deflater(compressionLevel, wrapper != ZlibWrapper.ZLIB);
53      }
54  
55      private ZlibCompressor(int compressionLevel, byte[] dictionary) {
56          wrapper = ZlibWrapper.ZLIB;
57          deflater = new Deflater(compressionLevel);
58          deflater.setDictionary(dictionary);
59      }
60  
61      /**
62       * Creates a zlib compressor factory with the default compression level ({@code 6})
63       * and the default wrapper ({@link ZlibWrapper#ZLIB}).
64       *
65       * @return the factory.
66       * @throws CompressionException if failed to initialize zlib
67       */
68      public static Supplier<ZlibCompressor> newFactory() {
69          return newFactory(6);
70      }
71  
72      /**
73       * Creates a zlib compressor factory with the specified {@code compressionLevel}
74       * and the default wrapper ({@link ZlibWrapper#ZLIB}).
75       *
76       * @param compressionLevel
77       *        {@code 1} yields the fastest compression and {@code 9} yields the
78       *        best compression.  {@code 0} means no compression.  The default
79       *        compression level is {@code 6}.
80       *
81       * @throws CompressionException if failed to initialize zlib
82       */
83      public static Supplier<ZlibCompressor> newFactory(int compressionLevel) {
84          return newFactory(ZlibWrapper.ZLIB, compressionLevel);
85      }
86  
87      /**
88       * Creates a zlib compressor factory with the default compression level ({@code 6})
89       * and the specified wrapper.
90       *
91       * @return the factory.
92       * @throws CompressionException if failed to initialize zlib
93       */
94      public static Supplier<ZlibCompressor> newFactory(ZlibWrapper wrapper) {
95          return newFactory(wrapper, 6);
96      }
97  
98      /**
99       * Creates a zlib compressor factory with the specified {@code compressionLevel}
100      * and the specified wrapper.
101      *
102      * @param compressionLevel
103      *        {@code 1} yields the fastest compression and {@code 9} yields the
104      *        best compression.  {@code 0} means no compression.  The default
105      *        compression level is {@code 6}.
106      * @return the factory.
107      * @throws CompressionException if failed to initialize zlib
108      */
109     public static Supplier<ZlibCompressor> newFactory(ZlibWrapper wrapper, int compressionLevel) {
110         checkInRange(compressionLevel, 0, 9 , "compressionLevel");
111         requireNonNull(wrapper, "wrapper");
112         if (wrapper == ZlibWrapper.ZLIB_OR_NONE) {
113             throw new IllegalArgumentException(
114                     "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " +
115                             "allowed for compression.");
116         }
117 
118         return () -> new ZlibCompressor(wrapper, compressionLevel);
119     }
120 
121     /**
122      * Creates a zlib compressor factory with the default compression level ({@code 6})
123      * and the specified preset dictionary.  The wrapper is always
124      * {@link ZlibWrapper#ZLIB} because it is the only format that supports
125      * the preset dictionary.
126      *
127      * @param dictionary  the preset dictionary
128      * @return the factory.
129      * @throws CompressionException if failed to initialize zlib
130      */
131     public static Supplier<ZlibCompressor> newFactory(byte[] dictionary) {
132         return newFactory(6, dictionary);
133     }
134 
135     /**
136      * Creates a zlib compressor factory with the specified {@code compressionLevel}
137      * and the specified preset dictionary.  The wrapper is always
138      * {@link ZlibWrapper#ZLIB} because it is the only format that supports
139      * the preset dictionary.
140      *
141      * @param compressionLevel
142      *        {@code 1} yields the fastest compression and {@code 9} yields the
143      *        best compression.  {@code 0} means no compression.  The default
144      *        compression level is {@code 6}.
145      * @param dictionary  the preset dictionary
146      * @return the factory.
147      * @throws CompressionException if failed to initialize zlib
148      */
149     public static Supplier<ZlibCompressor> newFactory(int compressionLevel, byte[] dictionary) {
150         if (compressionLevel < 0 || compressionLevel > 9) {
151             throw new IllegalArgumentException(
152                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
153         }
154         requireNonNull(dictionary, "dictionary");
155 
156         return () -> new ZlibCompressor(compressionLevel, dictionary);
157     }
158 
159     @Override
160     public Buffer compress(Buffer uncompressed, BufferAllocator allocator) throws CompressionException {
161         switch (state) {
162             case CLOSED:
163                 throw new CompressionException("Compressor closed");
164             case FINISHED:
165                 return allocator.allocate(0);
166             case PROCESSING:
167                 return compressData(uncompressed, allocator);
168             default:
169                 throw new IllegalStateException();
170         }
171     }
172 
173     private Buffer compressData(Buffer uncompressed, BufferAllocator allocator) {
174         int len = uncompressed.readableBytes();
175         if (len == 0) {
176             return allocator.allocate(0);
177         }
178 
179         int sizeEstimate = (int) Math.ceil(len * 1.001) + 12;
180         if (writeHeader) {
181             switch (wrapper) {
182                 case GZIP:
183                     sizeEstimate += gzipHeader.length;
184                     break;
185                 case ZLIB:
186                     sizeEstimate += 2; // first two magic bytes
187                     break;
188                 default:
189                     // no op
190             }
191         }
192         Buffer out = allocator.allocate(sizeEstimate);
193 
194         try (var readableIteration = uncompressed.forEachReadable()) {
195             for (var readableComponent = readableIteration.first();
196                  readableComponent != null; readableComponent = readableComponent.next()) {
197                 compressData(readableComponent.readableBuffer(), out);
198             }
199         }
200 
201         return out;
202     }
203 
204     private void compressData(ByteBuffer in, Buffer out) {
205         try {
206             if (writeHeader) {
207                 writeHeader = false;
208                 if (wrapper == ZlibWrapper.GZIP) {
209                     out.writeBytes(gzipHeader);
210                 }
211             }
212 
213             if (wrapper == ZlibWrapper.GZIP) {
214                 int position = in.position();
215                 crc.update(in);
216                 in.position(position);
217             }
218 
219             deflater.setInput(in);
220             for (;;) {
221                 deflate(out);
222                 if (deflater.needsInput()) {
223                     // Consumed everything
224                     break;
225                 } else {
226                     if (out.writableBytes() == 0) {
227                         // We did not consume everything but the buffer is not writable anymore. Increase the
228                         // capacity to make more room.
229                         out.ensureWritable(out.writerOffset());
230                     }
231                 }
232             }
233         } catch (Throwable cause) {
234             out.close();
235             throw cause;
236         }
237     }
238 
239     @Override
240     public Buffer finish(BufferAllocator allocator) {
241         switch (state) {
242             case CLOSED:
243                 throw new CompressionException("Compressor closed");
244             case FINISHED:
245             case PROCESSING:
246                 state = State.FINISHED;
247                 Buffer footer = allocator.allocate(256);
248                 try {
249                     if (writeHeader && wrapper == ZlibWrapper.GZIP) {
250                         // Write the GZIP header first if not written yet. (i.e. user wrote nothing.)
251                         writeHeader = false;
252                         footer.writeBytes(gzipHeader);
253                     }
254 
255                     deflater.finish();
256 
257                     while (!deflater.finished()) {
258                         deflate(footer);
259                     }
260                     if (wrapper == ZlibWrapper.GZIP) {
261                         int crcValue = (int) crc.getValue();
262                         int uncBytes = deflater.getTotalIn();
263                         footer.writeByte((byte) crcValue);
264                         footer.writeByte((byte) (crcValue >>> 8));
265                         footer.writeByte((byte) (crcValue >>> 16));
266                         footer.writeByte((byte) (crcValue >>> 24));
267                         footer.writeByte((byte) uncBytes);
268                         footer.writeByte((byte) (uncBytes >>> 8));
269                         footer.writeByte((byte) (uncBytes >>> 16));
270                         footer.writeByte((byte) (uncBytes >>> 24));
271                     }
272                     deflater.end();
273                     return footer;
274                 } catch (Throwable cause) {
275                     footer.close();
276                     throw cause;
277                 }
278             default:
279                 throw new IllegalStateException();
280         }
281     }
282 
283     @Override
284     public boolean isFinished() {
285         return state != State.PROCESSING;
286     }
287 
288     @Override
289     public boolean isClosed() {
290         return state == State.CLOSED;
291     }
292 
293     @Override
294     public void close() {
295         if (state == State.PROCESSING) {
296             deflater.end();
297         }
298         state = State.CLOSED;
299     }
300 
301     private void deflate(Buffer out) {
302         try (var writableIteration = out.forEachWritable()) {
303             for (var writableComponent = writableIteration.first();
304                  writableComponent != null; writableComponent = writableComponent.next()) {
305                 if (writableComponent.hasWritableArray()) {
306                     for (;;) {
307                         int numBytes = deflater.deflate(
308                                 writableComponent.writableArray(), writableComponent.writableArrayOffset(),
309                                 writableComponent.writableBytes(), Deflater.SYNC_FLUSH);
310                         if (numBytes <= 0) {
311                             break;
312                         }
313                         writableComponent.skipWritableBytes(numBytes);
314                     }
315                 } else {
316                     for (;;) {
317                         int numBytes = deflater.deflate(writableComponent.writableBuffer(), Deflater.SYNC_FLUSH);
318                         if (numBytes <= 0) {
319                             break;
320                         }
321                         writableComponent.skipWritableBytes(numBytes);
322                     }
323                 }
324             }
325         }
326     }
327 }