Repository: netty/netty
Issue: JDK zlib decompression support
Labels: improvement
feature

With the changes implemented in JDK 7 I'm a bit uncertain as to whether a dependency on JZLib is still required.

Currently Netty implements `JdkZlibEncoder` but doesn't implement a corresponding `JdkZlibDecoder`. I believe this is a historical omission based on the inability of previous versions of the JDK.

Source code for [ZlibCodecFactory](http://netty.io/4.0/xref/io/netty/handler/codec/compression/ZlibCodecFactory.html) gives some idea as to what is currently going on but no comments as to why there's no `JdkZlibDecoder`.

In other words: The proposal here is to implement `JdkZlibDecoder` in Netty and make it the default for decoding when using Java 7.


Issue comments:
I found [this example ](http://www.jcraft.com/jsch/examples/CompressionJUZ.java) which explains how to 
**solely** use JDK for compression/decompression of a byte array as opposed to relying on external library JZlib.

The example is actually made by the author of JZlib. 

Let us try to do this for 4.1

@phansson pull-req welcome ;)

@phansson can you please test latest master, it now ship the requested decoder :)

Made a simple test. No errors. Looks good.

Thanks for testing

Am 19.08.2013 um 08:43 schrieb phansson notifications@github.com:

> Made a simple test. No errors. Looks good.
> 
> —
> Reply to this email directly or view it on GitHub.


Pull request review comments:
`Unpooled.copiedBuffer()`?

Same for similar `wrapperBuffer()` calls

Better use do-while because we already know `in` is readable at this point.  Same applies for similar loops.


Patch that resolved the issue:
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java
new file mode 100644
index 00000000000..5d860a15202
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.codec.compression;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.DataFormatException;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+
+
+/**
+ * Decompress a {@link ByteBuf} using the inflate algorithm.
+ */
+public class JdkZlibDecoder extends ZlibDecoder {
+    private static final int FHCRC = 0x02;
+    private static final int FEXTRA = 0x04;
+    private static final int FNAME = 0x08;
+    private static final int FCOMMENT = 0x10;
+    private static final int FRESERVED = 0xE0;
+
+    private final Inflater inflater;
+    private final byte[] dictionary;
+
+    // GZIP related
+    private final CRC32 crc;
+
+    private enum GzipState {
+        HEADER_START,
+        HEADER_END,
+        FLG_READ,
+        XLEN_READ,
+        SKIP_FNAME,
+        SKIP_COMMENT,
+        PROCESS_FHCRC,
+        FOOTER_START,
+    }
+
+    private GzipState gzipState = GzipState.HEADER_START;
+    private int flags = -1;
+    private int xlen = -1;
+
+    private volatile boolean finished;
+
+    /**
+     * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}).
+     */
+    public JdkZlibDecoder() {
+        this(ZlibWrapper.ZLIB, null);
+    }
+
+    /**
+     * Creates a new instance with the specified preset dictionary. The wrapper
+     * is always {@link ZlibWrapper#ZLIB} because it is the only format that
+     * supports the preset dictionary.
+     */
+    public JdkZlibDecoder(byte[] dictionary) {
+        this(ZlibWrapper.ZLIB, dictionary);
+    }
+
+    /**
+     * Creates a new instance with the specified wrapper.
+     * Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are
+     * supported atm.
+     */
+    public JdkZlibDecoder(ZlibWrapper wrapper) {
+        this(wrapper, null);
+    }
+
+    private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary) {
+        if (wrapper == null) {
+            throw new NullPointerException("wrapper");
+        }
+        switch (wrapper) {
+            case GZIP:
+                inflater = new Inflater(true);
+                crc = new CRC32();
+                break;
+            case NONE:
+                inflater = new Inflater(true);
+                crc = null;
+                break;
+            case ZLIB:
+                inflater = new Inflater();
+                crc = null;
+                break;
+            default:
+                throw new IllegalArgumentException("Only GZIP or ZLIB is supported, but you used " + wrapper);
+        }
+        this.dictionary = dictionary;
+    }
+
+    @Override
+    public boolean isClosed() {
+        return finished;
+    }
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        if (!in.isReadable() && finished) {
+            return;
+        }
+
+        if (crc != null) {
+            switch (gzipState) {
+                case FOOTER_START:
+                    if (readGZIPFooter(in)) {
+                        finished = true;
+                    }
+                    return;
+                default:
+                    if (gzipState != GzipState.HEADER_END) {
+                        if (!readGZIPHeader(in)) {
+                            return;
+                        }
+                    }
+            }
+        }
+
+        int readableBytes = in.readableBytes();
+        if (in.hasArray()) {
+            inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), in.readableBytes());
+        } else {
+            byte[] array = new byte[in.readableBytes()];
+            in.getBytes(in.readerIndex(), array);
+            inflater.setInput(array);
+        }
+
+        int maxOutputLength = inflater.getRemaining() << 1;
+        ByteBuf decompressed = ctx.alloc().heapBuffer(maxOutputLength);
+        try {
+            boolean readFooter = false;
+            while (!inflater.needsInput()) {
+                byte[] outArray = decompressed.array();
+                int outIndex = decompressed.arrayOffset() + decompressed.writerIndex();
+                int length = outArray.length - outIndex;
+
+                int outputLength =  inflater.inflate(outArray, outIndex, length);
+
+                if (outputLength > 0) {
+                    decompressed.writerIndex(decompressed.writerIndex() + outputLength);
+                    if (crc != null) {
+                        crc.update(outArray, outIndex, length);
+                    }
+                } else {
+                    if (inflater.needsDictionary()) {
+                        if (dictionary == null) {
+                            throw new DecompressionException(
+                                    "decompression failure, unable to set dictionary as non was specified");
+                        }
+                        inflater.setDictionary(dictionary);
+                    }
+                }
+
+                if (inflater.finished()) {
+                    if (crc == null) {
+                        finished = true; // Do not decode anymore.
+                    } else {
+                        readFooter = true;
+                    }
+                    break;
+                }
+            }
+
+            in.skipBytes(readableBytes - inflater.getRemaining());
+
+            if (readFooter) {
+                gzipState = GzipState.FOOTER_START;
+                if (readGZIPFooter(in)) {
+                    finished = true;
+                }
+            }
+        } catch (DataFormatException e) {
+            throw new DecompressionException("decompression failure", e);
+        } finally {
+
+            if (decompressed.isReadable()) {
+                out.add(decompressed);
+            } else {
+                decompressed.release();
+            }
+        }
+    }
+
+    @Override
+    protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
+        super.handlerRemoved0(ctx);
+        inflater.end();
+    }
+
+    private boolean readGZIPHeader(ByteBuf in) {
+        switch (gzipState) {
+            case HEADER_START:
+                if (in.readableBytes() < 10) {
+                    return false;
+                }
+                // read magic numbers
+                int magic0 = in.readByte();
+                int magic1 = in.readByte();
+
+                if (magic0 != 31) {
+                    throw new CompressionException("Input is not in the GZIP format");
+                }
+                crc.update(magic0);
+                crc.update(magic1);
+
+                int method = in.readUnsignedByte();
+                if (method != Deflater.DEFLATED) {
+                    throw new CompressionException("Unsupported compression method "
+                            + method + " in the GZIP header");
+                }
+                crc.update(method);
+
+                flags = in.readUnsignedByte();
+                crc.update(flags);
+
+                if ((flags & FRESERVED) != 0) {
+                    throw new CompressionException(
+                            "Reserved flags are set in the GZIP header");
+                }
+
+                // mtime (int)
+                crc.update(in.readByte());
+                crc.update(in.readByte());
+                crc.update(in.readByte());
+                crc.update(in.readByte());
+
+                crc.update(in.readUnsignedByte()); // extra flags
+                crc.update(in.readUnsignedByte()); // operating system
+
+                gzipState = GzipState.FLG_READ;
+            case FLG_READ:
+                if ((flags & FEXTRA) != 0) {
+                    if (in.readableBytes() < 2) {
+                        return false;
+                    }
+                    int xlen1 = in.readUnsignedByte();
+                    int xlen2 = in.readUnsignedByte();
+                    crc.update(xlen1);
+                    crc.update(xlen2);
+
+                    xlen |= xlen1 << 8 | xlen2;
+                }
+                gzipState = GzipState.XLEN_READ;
+            case XLEN_READ:
+                if (xlen != -1) {
+                    if (in.readableBytes() < xlen) {
+                        return false;
+                    }
+                    byte[] xtra = new byte[xlen];
+                    in.readBytes(xtra);
+                    crc.update(xtra);
+                }
+                gzipState = GzipState.SKIP_FNAME;
+            case SKIP_FNAME:
+                if ((flags & FNAME) != 0) {
+                    if (!in.isReadable()) {
+                        return false;
+                    }
+                    while (in.isReadable()) {
+                        int b = in.readUnsignedByte();
+                        crc.update(b);
+                        if (b == 0x00) {
+                            break;
+                        }
+                    }
+                }
+                gzipState = GzipState.SKIP_COMMENT;
+            case SKIP_COMMENT:
+                if ((flags & FCOMMENT) != 0) {
+                    if (!in.isReadable()) {
+                        return false;
+                    }
+                    while (in.isReadable()) {
+                        int b = in.readUnsignedByte();
+                        crc.update(b);
+                        if (b == 0x00) {
+                            break;
+                        }
+                    }
+                }
+                gzipState = GzipState.PROCESS_FHCRC;
+            case PROCESS_FHCRC:
+                if ((flags & FHCRC) != 0) {
+                    if (!in.isReadable()) {
+                        return false;
+                    }
+                    int headerCrc = in.readShort();
+                    int readCrc = (int) crc.getValue() & 0xffff;
+                    if (headerCrc != readCrc) {
+                        throw new CompressionException(
+                                "Header CRC value missmatch. Expected: " + headerCrc + ", Got: " + readCrc);
+                    }
+                }
+                crc.reset();
+                gzipState = GzipState.HEADER_END;
+            case HEADER_END:
+                return true;
+            default:
+                throw new IllegalStateException();
+        }
+    }
+
+    private boolean readGZIPFooter(ByteBuf buf) {
+        if (buf.readableBytes() < 8) {
+            return false;
+        }
+        int dataCrc = buf.readInt();
+        int readCrc = (int) crc.getValue() & 0xffff;
+        if (dataCrc != readCrc) {
+            throw new CompressionException(
+                    "Data CRC value missmatch. Expected: " + dataCrc + ", Got: " + readCrc);
+        }
+
+        int dataLength = buf.readInt();
+        int readLength = inflater.getTotalOut();
+        if (dataLength != readLength) {
+            throw new CompressionException(
+                    "Number of bytes missmatch. Expected: " + dataLength + ", Got: " + readLength);
+        }
+        return true;
+    }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java b/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java
index 979a124694d..60f51073b02 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java
@@ -79,15 +79,32 @@ public static ZlibEncoder newZlibEncoder(int compressionLevel, int windowBits, i
     }
 
     public static ZlibDecoder newZlibDecoder() {
-        return new JZlibDecoder();
+        if (PlatformDependent.javaVersion() < 7) {
+            return new JZlibDecoder();
+        } else {
+            return new JdkZlibDecoder();
+        }
     }
 
     public static ZlibDecoder newZlibDecoder(ZlibWrapper wrapper) {
-        return new JZlibDecoder(wrapper);
+        switch (wrapper) {
+            case ZLIB_OR_NONE:
+                return new JZlibDecoder(wrapper);
+            default:
+                if (PlatformDependent.javaVersion() < 7) {
+                    return new JZlibDecoder(wrapper);
+                } else {
+                    return new JdkZlibDecoder(wrapper);
+                }
+        }
     }
 
     public static ZlibDecoder newZlibDecoder(byte[] dictionary) {
-        return new JZlibDecoder(dictionary);
+        if (PlatformDependent.javaVersion() < 7) {
+            return new JZlibDecoder(dictionary);
+        } else {
+            return new JdkZlibDecoder(dictionary);
+        }
     }
 
     private ZlibCodecFactory() {


Test patch:
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java
new file mode 100644
index 00000000000..9a93aa8fb2f
--- /dev/null
+++ b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.codec.compression;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JdkZlibTest {
+
+    @Test
+    public void testZLIB() throws Exception {
+        ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
+
+        EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.ZLIB));
+
+        chEncoder.writeOutbound(data.copy());
+        assertTrue(chEncoder.finish());
+
+        ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
+
+        EmbeddedChannel chDecoderZlib = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.ZLIB));
+
+        chDecoderZlib.writeInbound(deflatedData.copy());
+        assertTrue(chDecoderZlib.finish());
+
+        assertEquals(data, chDecoderZlib.readInbound());
+    }
+
+    @Test
+    public void testNONE() throws Exception {
+        ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
+
+        EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.NONE));
+
+        chEncoder.writeOutbound(data.copy());
+        assertTrue(chEncoder.finish());
+
+        ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
+
+        EmbeddedChannel chDecoderZlibNone = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.NONE));
+
+        chDecoderZlibNone.writeInbound(deflatedData.copy());
+        assertTrue(chDecoderZlibNone.finish());
+
+        assertEquals(data, chDecoderZlibNone.readInbound());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testZLIB_OR_NONE() throws Exception {
+        new JdkZlibDecoder(ZlibWrapper.ZLIB_OR_NONE);
+    }
+
+    @Test
+    public void testGZIP() throws Exception {
+        ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
+
+        EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.GZIP));
+
+        chEncoder.writeOutbound(data.copy());
+        assertTrue(chEncoder.finish());
+
+        ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
+
+        EmbeddedChannel chDecoderGZip = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.GZIP));
+
+        chDecoderGZip.writeInbound(deflatedData.copy());
+        assertTrue(chDecoderGZip.finish());
+
+        assertEquals(data, chDecoderGZip.readInbound());
+    }
+}