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 0000000000..4740d359e1 --- /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 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; + } + do { + int b = in.readUnsignedByte(); + crc.update(b); + if (b == 0x00) { + break; + } + } while (in.isReadable()); + } + gzipState = GzipState.SKIP_COMMENT; + case SKIP_COMMENT: + if ((flags & FCOMMENT) != 0) { + if (!in.isReadable()) { + return false; + } + do { + int b = in.readUnsignedByte(); + crc.update(b); + if (b == 0x00) { + break; + } + } while (in.isReadable()); + } + 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 979a124694..60f51073b0 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 final class ZlibCodecFactory { } 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() { diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java index faecedd859..44ebef4441 100644 --- a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java +++ b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.compression; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.CharsetUtil; import org.junit.Test; import static org.junit.Assert.*; @@ -26,7 +27,7 @@ public class JZlibTest { @Test public void testZLIB() throws Exception { - ByteBuf data = Unpooled.wrappedBuffer("test".getBytes()); + ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8); EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.ZLIB)); @@ -52,7 +53,7 @@ public class JZlibTest { @Test public void testNONE() throws Exception { - ByteBuf data = Unpooled.wrappedBuffer("test".getBytes()); + ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8); EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.NONE)); @@ -79,7 +80,7 @@ public class JZlibTest { @Test public void testGZIP() throws Exception { - ByteBuf data = Unpooled.wrappedBuffer("test".getBytes()); + ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8); EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.GZIP)); 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 0000000000..40af2226c1 --- /dev/null +++ b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java @@ -0,0 +1,90 @@ +/* + * 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 io.netty.util.CharsetUtil; +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.copiedBuffer("test", CharsetUtil.UTF_8); + + 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.copiedBuffer("test", CharsetUtil.UTF_8); + + 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.copiedBuffer("test", CharsetUtil.UTF_8); + + 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()); + } +}