Skip to content

Commit 39cabcd

Browse files
author
Norman Maurer
committedAug 14, 2013
[netty#1481] Add a JdkZlibDecoder which has no dependencies
1 parent 9acf130 commit 39cabcd

File tree

4 files changed

+453
-6
lines changed

4 files changed

+453
-6
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* Copyright 2013 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+
* http://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+
21+
import java.util.List;
22+
import java.util.zip.CRC32;
23+
import java.util.zip.DataFormatException;
24+
import java.util.zip.Deflater;
25+
import java.util.zip.Inflater;
26+
27+
28+
/**
29+
* Decompress a {@link ByteBuf} using the inflate algorithm.
30+
*/
31+
public class JdkZlibDecoder extends ZlibDecoder {
32+
private static final int FHCRC = 0x02;
33+
private static final int FEXTRA = 0x04;
34+
private static final int FNAME = 0x08;
35+
private static final int FCOMMENT = 0x10;
36+
private static final int FRESERVED = 0xE0;
37+
38+
private final Inflater inflater;
39+
private final byte[] dictionary;
40+
41+
// GZIP related
42+
private final CRC32 crc;
43+
44+
private enum GzipState {
45+
HEADER_START,
46+
HEADER_END,
47+
FLG_READ,
48+
XLEN_READ,
49+
SKIP_FNAME,
50+
SKIP_COMMENT,
51+
PROCESS_FHCRC,
52+
FOOTER_START,
53+
}
54+
55+
private GzipState gzipState = GzipState.HEADER_START;
56+
private int flags = -1;
57+
private int xlen = -1;
58+
59+
private volatile boolean finished;
60+
61+
/**
62+
* Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}).
63+
*/
64+
public JdkZlibDecoder() {
65+
this(ZlibWrapper.ZLIB, null);
66+
}
67+
68+
/**
69+
* Creates a new instance with the specified preset dictionary. The wrapper
70+
* is always {@link ZlibWrapper#ZLIB} because it is the only format that
71+
* supports the preset dictionary.
72+
*/
73+
public JdkZlibDecoder(byte[] dictionary) {
74+
this(ZlibWrapper.ZLIB, dictionary);
75+
}
76+
77+
/**
78+
* Creates a new instance with the specified wrapper.
79+
* Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are
80+
* supported atm.
81+
*/
82+
public JdkZlibDecoder(ZlibWrapper wrapper) {
83+
this(wrapper, null);
84+
}
85+
86+
private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary) {
87+
if (wrapper == null) {
88+
throw new NullPointerException("wrapper");
89+
}
90+
switch (wrapper) {
91+
case GZIP:
92+
inflater = new Inflater(true);
93+
crc = new CRC32();
94+
break;
95+
case NONE:
96+
inflater = new Inflater(true);
97+
crc = null;
98+
break;
99+
case ZLIB:
100+
inflater = new Inflater();
101+
crc = null;
102+
break;
103+
default:
104+
throw new IllegalArgumentException("Only GZIP or ZLIB is supported, but you used " + wrapper);
105+
}
106+
this.dictionary = dictionary;
107+
}
108+
109+
@Override
110+
public boolean isClosed() {
111+
return finished;
112+
}
113+
114+
@Override
115+
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
116+
if (!in.isReadable() && finished) {
117+
return;
118+
}
119+
120+
if (crc != null) {
121+
switch (gzipState) {
122+
case FOOTER_START:
123+
if (readGZIPFooter(in)) {
124+
finished = true;
125+
}
126+
return;
127+
default:
128+
if (gzipState != GzipState.HEADER_END) {
129+
if (!readGZIPHeader(in)) {
130+
return;
131+
}
132+
}
133+
}
134+
}
135+
136+
int readableBytes = in.readableBytes();
137+
if (in.hasArray()) {
138+
inflater.setInput(in.array(), in.arrayOffset() + in.readerIndex(), in.readableBytes());
139+
} else {
140+
byte[] array = new byte[in.readableBytes()];
141+
in.getBytes(in.readerIndex(), array);
142+
inflater.setInput(array);
143+
}
144+
145+
int maxOutputLength = inflater.getRemaining() << 1;
146+
ByteBuf decompressed = ctx.alloc().heapBuffer(maxOutputLength);
147+
try {
148+
boolean readFooter = false;
149+
while (!inflater.needsInput()) {
150+
byte[] outArray = decompressed.array();
151+
int outIndex = decompressed.arrayOffset() + decompressed.writerIndex();
152+
int length = outArray.length - outIndex;
153+
154+
int outputLength = inflater.inflate(outArray, outIndex, length);
155+
156+
if (outputLength > 0) {
157+
decompressed.writerIndex(decompressed.writerIndex() + outputLength);
158+
if (crc != null) {
159+
crc.update(outArray, outIndex, length);
160+
}
161+
} else {
162+
if (inflater.needsDictionary()) {
163+
if (dictionary == null) {
164+
throw new DecompressionException(
165+
"decompression failure, unable to set dictionary as non was specified");
166+
}
167+
inflater.setDictionary(dictionary);
168+
}
169+
}
170+
171+
if (inflater.finished()) {
172+
if (crc == null) {
173+
finished = true; // Do not decode anymore.
174+
} else {
175+
readFooter = true;
176+
}
177+
break;
178+
}
179+
}
180+
181+
in.skipBytes(readableBytes - inflater.getRemaining());
182+
183+
if (readFooter) {
184+
gzipState = GzipState.FOOTER_START;
185+
if (readGZIPFooter(in)) {
186+
finished = true;
187+
}
188+
}
189+
} catch (DataFormatException e) {
190+
throw new DecompressionException("decompression failure", e);
191+
} finally {
192+
193+
if (decompressed.isReadable()) {
194+
out.add(decompressed);
195+
} else {
196+
decompressed.release();
197+
}
198+
}
199+
}
200+
201+
@Override
202+
protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
203+
super.handlerRemoved0(ctx);
204+
inflater.end();
205+
}
206+
207+
private boolean readGZIPHeader(ByteBuf in) {
208+
switch (gzipState) {
209+
case HEADER_START:
210+
if (in.readableBytes() < 10) {
211+
return false;
212+
}
213+
// read magic numbers
214+
int magic0 = in.readByte();
215+
int magic1 = in.readByte();
216+
217+
if (magic0 != 31) {
218+
throw new CompressionException("Input is not in the GZIP format");
219+
}
220+
crc.update(magic0);
221+
crc.update(magic1);
222+
223+
int method = in.readUnsignedByte();
224+
if (method != Deflater.DEFLATED) {
225+
throw new CompressionException("Unsupported compression method "
226+
+ method + " in the GZIP header");
227+
}
228+
crc.update(method);
229+
230+
flags = in.readUnsignedByte();
231+
crc.update(flags);
232+
233+
if ((flags & FRESERVED) != 0) {
234+
throw new CompressionException(
235+
"Reserved flags are set in the GZIP header");
236+
}
237+
238+
// mtime (int)
239+
crc.update(in.readByte());
240+
crc.update(in.readByte());
241+
crc.update(in.readByte());
242+
crc.update(in.readByte());
243+
244+
crc.update(in.readUnsignedByte()); // extra flags
245+
crc.update(in.readUnsignedByte()); // operating system
246+
247+
gzipState = GzipState.FLG_READ;
248+
case FLG_READ:
249+
if ((flags & FEXTRA) != 0) {
250+
if (in.readableBytes() < 2) {
251+
return false;
252+
}
253+
int xlen1 = in.readUnsignedByte();
254+
int xlen2 = in.readUnsignedByte();
255+
crc.update(xlen1);
256+
crc.update(xlen2);
257+
258+
xlen |= xlen1 << 8 | xlen2;
259+
}
260+
gzipState = GzipState.XLEN_READ;
261+
case XLEN_READ:
262+
if (xlen != -1) {
263+
if (in.readableBytes() < xlen) {
264+
return false;
265+
}
266+
byte[] xtra = new byte[xlen];
267+
in.readBytes(xtra);
268+
crc.update(xtra);
269+
}
270+
gzipState = GzipState.SKIP_FNAME;
271+
case SKIP_FNAME:
272+
if ((flags & FNAME) != 0) {
273+
if (!in.isReadable()) {
274+
return false;
275+
}
276+
do {
277+
int b = in.readUnsignedByte();
278+
crc.update(b);
279+
if (b == 0x00) {
280+
break;
281+
}
282+
} while (in.isReadable());
283+
}
284+
gzipState = GzipState.SKIP_COMMENT;
285+
case SKIP_COMMENT:
286+
if ((flags & FCOMMENT) != 0) {
287+
if (!in.isReadable()) {
288+
return false;
289+
}
290+
do {
291+
int b = in.readUnsignedByte();
292+
crc.update(b);
293+
if (b == 0x00) {
294+
break;
295+
}
296+
} while (in.isReadable());
297+
}
298+
gzipState = GzipState.PROCESS_FHCRC;
299+
case PROCESS_FHCRC:
300+
if ((flags & FHCRC) != 0) {
301+
if (!in.isReadable()) {
302+
return false;
303+
}
304+
int headerCrc = in.readShort();
305+
int readCrc = (int) crc.getValue() & 0xffff;
306+
if (headerCrc != readCrc) {
307+
throw new CompressionException(
308+
"Header CRC value missmatch. Expected: " + headerCrc + ", Got: " + readCrc);
309+
}
310+
}
311+
crc.reset();
312+
gzipState = GzipState.HEADER_END;
313+
case HEADER_END:
314+
return true;
315+
default:
316+
throw new IllegalStateException();
317+
}
318+
}
319+
320+
private boolean readGZIPFooter(ByteBuf buf) {
321+
if (buf.readableBytes() < 8) {
322+
return false;
323+
}
324+
int dataCrc = buf.readInt();
325+
int readCrc = (int) crc.getValue() & 0xffff;
326+
if (dataCrc != readCrc) {
327+
throw new CompressionException(
328+
"Data CRC value missmatch. Expected: " + dataCrc + ", Got: " + readCrc);
329+
}
330+
331+
int dataLength = buf.readInt();
332+
int readLength = inflater.getTotalOut();
333+
if (dataLength != readLength) {
334+
throw new CompressionException(
335+
"Number of bytes missmatch. Expected: " + dataLength + ", Got: " + readLength);
336+
}
337+
return true;
338+
}
339+
}

‎codec/src/main/java/io/netty/handler/codec/compression/ZlibCodecFactory.java

+20-3
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,32 @@ public static ZlibEncoder newZlibEncoder(int compressionLevel, int windowBits, i
7979
}
8080

8181
public static ZlibDecoder newZlibDecoder() {
82-
return new JZlibDecoder();
82+
if (PlatformDependent.javaVersion() < 7) {
83+
return new JZlibDecoder();
84+
} else {
85+
return new JdkZlibDecoder();
86+
}
8387
}
8488

8589
public static ZlibDecoder newZlibDecoder(ZlibWrapper wrapper) {
86-
return new JZlibDecoder(wrapper);
90+
switch (wrapper) {
91+
case ZLIB_OR_NONE:
92+
return new JZlibDecoder(wrapper);
93+
default:
94+
if (PlatformDependent.javaVersion() < 7) {
95+
return new JZlibDecoder(wrapper);
96+
} else {
97+
return new JdkZlibDecoder(wrapper);
98+
}
99+
}
87100
}
88101

89102
public static ZlibDecoder newZlibDecoder(byte[] dictionary) {
90-
return new JZlibDecoder(dictionary);
103+
if (PlatformDependent.javaVersion() < 7) {
104+
return new JZlibDecoder(dictionary);
105+
} else {
106+
return new JdkZlibDecoder(dictionary);
107+
}
91108
}
92109

93110
private ZlibCodecFactory() {

‎codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.netty.buffer.ByteBuf;
1919
import io.netty.buffer.Unpooled;
2020
import io.netty.channel.embedded.EmbeddedChannel;
21+
import io.netty.util.CharsetUtil;
2122
import org.junit.Test;
2223

2324
import static org.junit.Assert.*;
@@ -26,7 +27,7 @@ public class JZlibTest {
2627

2728
@Test
2829
public void testZLIB() throws Exception {
29-
ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
30+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
3031

3132
EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.ZLIB));
3233

@@ -52,7 +53,7 @@ public void testZLIB() throws Exception {
5253

5354
@Test
5455
public void testNONE() throws Exception {
55-
ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
56+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
5657

5758
EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.NONE));
5859

@@ -79,7 +80,7 @@ public void testNONE() throws Exception {
7980

8081
@Test
8182
public void testGZIP() throws Exception {
82-
ByteBuf data = Unpooled.wrappedBuffer("test".getBytes());
83+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
8384

8485
EmbeddedChannel chEncoder = new EmbeddedChannel(new JZlibEncoder(ZlibWrapper.GZIP));
8586

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2013 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+
* http://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.buffer.Unpooled;
20+
import io.netty.channel.embedded.EmbeddedChannel;
21+
import io.netty.util.CharsetUtil;
22+
import org.junit.Test;
23+
24+
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertTrue;
26+
27+
public class JdkZlibTest {
28+
29+
@Test
30+
public void testZLIB() throws Exception {
31+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
32+
33+
EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.ZLIB));
34+
35+
chEncoder.writeOutbound(data.copy());
36+
assertTrue(chEncoder.finish());
37+
38+
ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
39+
40+
EmbeddedChannel chDecoderZlib = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.ZLIB));
41+
42+
chDecoderZlib.writeInbound(deflatedData.copy());
43+
assertTrue(chDecoderZlib.finish());
44+
45+
assertEquals(data, chDecoderZlib.readInbound());
46+
}
47+
48+
@Test
49+
public void testNONE() throws Exception {
50+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
51+
52+
EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.NONE));
53+
54+
chEncoder.writeOutbound(data.copy());
55+
assertTrue(chEncoder.finish());
56+
57+
ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
58+
59+
EmbeddedChannel chDecoderZlibNone = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.NONE));
60+
61+
chDecoderZlibNone.writeInbound(deflatedData.copy());
62+
assertTrue(chDecoderZlibNone.finish());
63+
64+
assertEquals(data, chDecoderZlibNone.readInbound());
65+
}
66+
67+
@Test(expected = IllegalArgumentException.class)
68+
public void testZLIB_OR_NONE() throws Exception {
69+
new JdkZlibDecoder(ZlibWrapper.ZLIB_OR_NONE);
70+
}
71+
72+
@Test
73+
public void testGZIP() throws Exception {
74+
ByteBuf data = Unpooled.copiedBuffer("test", CharsetUtil.UTF_8);
75+
76+
EmbeddedChannel chEncoder = new EmbeddedChannel(new JdkZlibEncoder(ZlibWrapper.GZIP));
77+
78+
chEncoder.writeOutbound(data.copy());
79+
assertTrue(chEncoder.finish());
80+
81+
ByteBuf deflatedData = (ByteBuf) chEncoder.readOutbound();
82+
83+
EmbeddedChannel chDecoderGZip = new EmbeddedChannel(new JdkZlibDecoder(ZlibWrapper.GZIP));
84+
85+
chDecoderGZip.writeInbound(deflatedData.copy());
86+
assertTrue(chDecoderGZip.finish());
87+
88+
assertEquals(data, chDecoderGZip.readInbound());
89+
}
90+
}

0 commit comments

Comments
 (0)
Please sign in to comment.