diff --git a/NOTICE.txt b/NOTICE.txt index a9095efad4..6fbd3e3cca 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -130,3 +130,10 @@ obtained at: * HOMEPAGE: * http://www.osgi.org/ +This product optionally depends on 'Snappy', a compression library produced +by Google Inc, which can be obtained at: + + * LICENSE: + * license/LICENSE.snappy.txt (New BSD License) + * HOMEPAGE: + * http://code.google.com/p/snappy/ diff --git a/codec-snappy/pom.xml b/codec-snappy/pom.xml new file mode 100644 index 0000000000..ff0cfc0fdb --- /dev/null +++ b/codec-snappy/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + io.netty + netty-parent + 4.0.0.Beta1-SNAPSHOT + + + netty-codec-snappy + jar + + Netty/Codec/Snappy + + + + ${project.groupId} + netty-codec + ${project.version} + + + ${project.groupId} + netty-handler + ${project.version} + + + diff --git a/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/Snappy.java b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/Snappy.java new file mode 100644 index 0000000000..a87e490966 --- /dev/null +++ b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/Snappy.java @@ -0,0 +1,458 @@ +/* + * Copyright 2012 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.snappy; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.compression.CompressionException; + +/** + * Uncompresses an input {@link ByteBuf} encoded with Snappy compression into an + * output {@link ByteBuf}. + * + * See http://code.google.com/p/snappy/source/browse/trunk/format_description.txt + */ +public class Snappy { + private static final int MAX_HT_SIZE = 1 << 14; + private static final int MIN_COMPRESSIBLE_BYTES = 15; + + // constants for the tag types + private static final int LITERAL = 0; + private static final int COPY_1_BYTE_OFFSET = 1; + private static final int COPY_2_BYTE_OFFSET = 2; + private static final int COPY_4_BYTE_OFFSET = 3; + + private int inputLength; + + public void reset() { + inputLength = 0; + } + + public void encode(ByteBuf in, ByteBuf out, int length) { + // Write the preamble length to the output buffer + int bytesToEncode = 1 + bitsToEncode(length - 1) / 7; + for (int i = 0; i < bytesToEncode; i++) { + if (i == bytesToEncode - 1) { + out.writeByte(length >> i * 7); + } else { + out.writeByte(0x80 | length >> i * 7); + } + } + + int inIndex = in.readerIndex(); + final int baseIndex = in.readerIndex(); + final int maxIndex = length; + + final short[] table = getHashTable(maxIndex); + final int shift = 32 - (int) Math.floor(Math.log(table.length) / Math.log(2)); + + int nextEmit = inIndex; + + if (maxIndex - inIndex >= MIN_COMPRESSIBLE_BYTES) { + int nextHash = hash(in, ++inIndex, shift); + outer: while (true) { + int skip = 32; + + int candidate; + int nextIndex = inIndex; + do { + inIndex = nextIndex; + int hash = nextHash; + int bytesBetweenHashLookups = skip++ >> 5; + nextIndex = inIndex + bytesBetweenHashLookups; + + if (nextIndex > maxIndex) { + break outer; + } + + nextHash = hash(in, nextIndex, shift); + + candidate = baseIndex + table[hash]; + + table[hash] = (short) (inIndex - baseIndex); + } + while (in.getInt(inIndex) != in.getInt(candidate)); + + encodeLiteral(in, out, inIndex - nextEmit); + + int insertTail; + do { + int base = inIndex; + int matched = 4 + findMatchingLength(in, candidate + 4, inIndex + 4, maxIndex); + inIndex += matched; + int offset = base - candidate; + encodeCopy(out, offset, matched); + in.readerIndex(in.readerIndex() + matched); + insertTail = inIndex - 1; + nextEmit = inIndex; + if (inIndex >= maxIndex) { + break outer; + } + + int prevHash = hash(in, insertTail, shift); + table[prevHash] = (short) (inIndex - baseIndex - 1); + int currentHash = hash(in, insertTail + 1, shift); + candidate = baseIndex + table[currentHash]; + table[currentHash] = (short) (inIndex - baseIndex); + } + while (in.getInt(insertTail + 1) == in.getInt(candidate)); + + nextHash = hash(in, insertTail + 2, shift); + ++inIndex; + } + } + + // If there are any remaining characters, write them out as a literal + if (nextEmit < maxIndex) { + encodeLiteral(in, out, maxIndex - nextEmit); + } + } + + /** + * Hashes the 4 bytes located at index, shifting the resulting hash into + * the appropriate range for our hash table. + * + * @param in The input buffer to read 4 bytes from + * @param index The index to read at + * @param shift The shift value, for ensuring that the resulting value is + * withing the range of our hash table size + * @return A 32-bit hash of 4 bytes located at index + */ + private static int hash(ByteBuf in, int index, int shift) { + return in.getInt(index) + 0x1e35a7bd >>> shift; + } + + /** + * Creates an appropriately sized hashtable for the given input size + * + * @param inputSize The size of our input, ie. the number of bytes we need to encode + * @return An appropriately sized empty hashtable + */ + private static short[] getHashTable(int inputSize) { + int htSize = 256; + while (htSize < MAX_HT_SIZE && htSize < inputSize) { + htSize <<= 1; + } + + short[] table; + if (htSize <= 256) { + table = new short[256]; + } else { + table = new short[MAX_HT_SIZE]; + } + + return table; + } + + /** + * Iterates over the supplied input buffer between the supplied minIndex and + * maxIndex to find how long our matched copy overlaps with an already-written + * literal value. + * + * @param in The input buffer to scan over + * @param minIndex The index in the input buffer to start scanning from + * @param inIndex The index of the start of our copy + * @param maxIndex The length of our input buffer + * @return The number of bytes for which our candidate copy is a repeat of + */ + private static int findMatchingLength(ByteBuf in, int minIndex, int inIndex, int maxIndex) { + int matched = 0; + + while (inIndex <= maxIndex - 4 && + in.getInt(inIndex) == in.getInt(minIndex + matched)) { + inIndex += 4; + matched += 4; + } + + while (inIndex < maxIndex && in.getByte(minIndex + matched) == in.getByte(inIndex)) { + ++inIndex; + ++matched; + } + + return matched; + } + + /** + * Calculates the minimum number of bits required to encode a value. This can + * then in turn be used to calculate the number of septets or octets (as + * appropriate) to use to encode a length parameter. + * + * @param value The value to calculate the minimum number of bits required to encode + * @return The minimum number of bits required to encode the supplied value + */ + private static int bitsToEncode(int value) { + int highestOneBit = Integer.highestOneBit(value); + int bitLength = 0; + while ((highestOneBit >>= 1) != 0) { + bitLength++; + } + + return bitLength; + } + + /** + * Writes a literal to the supplied output buffer by directly copying from + * the input buffer. The literal is taken from the current readerIndex + * up to the supplied length. + * + * @param in The input buffer to copy from + * @param out The output buffer to copy to + * @param length The length of the literal to copy + */ + private static void encodeLiteral(ByteBuf in, ByteBuf out, int length) { + if (length < 61) { + out.writeByte(length - 1 << 2); + } else { + int bitLength = bitsToEncode(length - 1); + int bytesToEncode = 1 + bitLength / 8; + out.writeByte(59 + bytesToEncode << 2); + for (int i = 0; i < bytesToEncode; i++) { + out.writeByte(length - 1 >> i * 8 & 0x0ff); + } + } + + out.writeBytes(in, length); + } + + private static void encodeCopyWithOffset(ByteBuf out, int offset, int length) { + if (length < 12 && offset < 2048) { + out.writeByte(COPY_1_BYTE_OFFSET | length - 4 << 2 | offset >> 8 << 5); + out.writeByte(offset & 0x0ff); + } else { + out.writeByte(COPY_2_BYTE_OFFSET | length - 1 << 2); + out.writeByte(offset & 0x0ff); + out.writeByte(offset >> 8 & 0x0ff); + } + } + + /** + * Encodes a series of copies, each at most 64 bytes in length. + * + * @param out The output buffer to write the copy pointer to + * @param offset The offset at which the original instance lies + * @param length The length of the original instance + */ + private static void encodeCopy(ByteBuf out, int offset, int length) { + while (length >= 68) { + encodeCopyWithOffset(out, offset, 64); + length -= 64; + } + + if (length > 64) { + encodeCopyWithOffset(out, offset, 60); + length -= 60; + } + + encodeCopyWithOffset(out, offset, length); + } + + public void decode(ByteBuf in, ByteBuf out, int maxLength) { + int inIndex = in.readerIndex(); + if (inputLength == 0) { + inputLength = readPreamble(in); + } + + if (inputLength == 0 || in.readerIndex() - inIndex + in.readableBytes() < maxLength) { + // Wait until we've got the entire chunk before continuing + return; + } + + out.ensureWritableBytes(inputLength); + + while (in.readable() && in.readerIndex() - inIndex < maxLength) { + byte tag = in.readByte(); + switch (tag & 0x03) { + case LITERAL: + decodeLiteral(tag, in, out); + break; + case COPY_1_BYTE_OFFSET: + decodeCopyWith1ByteOffset(tag, in, out); + break; + case COPY_2_BYTE_OFFSET: + decodeCopyWith2ByteOffset(tag, in, out); + break; + case COPY_4_BYTE_OFFSET: + decodeCopyWith4ByteOffset(tag, in, out); + break; + } + } + } + + /** + * Reads the length varint (a series of bytes, where the lower 7 bits + * are data and the upper bit is a flag to indicate more bytes to be + * read). + * + * @param in The input buffer to read the preamble from + * @return The calculated length based on the input buffer, or 0 if + * no preamble is able to be calculated + */ + private static int readPreamble(ByteBuf in) { + int length = 0; + int byteIndex = 0; + while (in.readableBytes() > 0) { + int current = in.readByte() & 0x0ff; + length += current << byteIndex++ * 7; + if ((current & 0x80) != 0x80) { + return length; + } + + if (byteIndex >= 4) { + throw new CompressionException("Preamble is greater than 4 bytes"); + } + } + + return 0; + } + + /** + * Reads a literal from the input buffer directly to the output buffer. + * A "literal" is an uncompressed segment of data stored directly in the + * byte stream. + * + * @param tag The tag that identified this segment as a literal is also + * used to encode part of the length of the data + * @param in The input buffer to read the literal from + * @param out The output buffer to write the literal to + */ + private static void decodeLiteral(byte tag, ByteBuf in, ByteBuf out) { + int length; + switch(tag >> 2 & 0x3F) { + case 60: + length = in.readByte() & 0x0ff; + break; + case 61: + length = (in.readByte() & 0x0ff) + + ((in.readByte() & 0x0ff) << 8); + break; + case 62: + length = (in.readByte() & 0x0ff) + + ((in.readByte() & 0x0ff) << 8) + + ((in.readByte() & 0x0ff) << 16); + break; + case 64: + length = (in.readByte() & 0x0ff) + + ((in.readByte() & 0x0ff) << 8) + + ((in.readByte() & 0x0ff) << 16) + + ((in.readByte() & 0x0ff) << 24); + break; + default: + length = tag >> 2 & 0x3F; + } + length += 1; + + out.writeBytes(in, length); + } + + /** + * Reads a compressed reference offset and length from the supplied input + * buffer, seeks back to the appropriate place in the input buffer and + * writes the found data to the supplied output stream. + * + * @param tag The tag used to identify this as a copy is also used to encode + * the length and part of the offset + * @param in The input buffer to read from + * @param out The output buffer to write to + * @throws CompressionException If the read offset is invalid + */ + private static void decodeCopyWith1ByteOffset(byte tag, ByteBuf in, ByteBuf out) { + int initialIndex = in.readerIndex(); + int length = 4 + ((tag & 0x0c) >> 2); + int offset = 1 + ((tag & 0x0e0) << 8) + + (in.readByte() & 0x0ff); + + validateOffset(offset, initialIndex); + + in.markReaderIndex(); + in.readerIndex(initialIndex - offset); + in.readBytes(out, length); + in.resetReaderIndex(); + } + + /** + * Reads a compressed reference offset and length from the supplied input + * buffer, seeks back to the appropriate place in the input buffer and + * writes the found data to the supplied output stream. + * + * @param tag The tag used to identify this as a copy is also used to encode + * the length and part of the offset + * @param in The input buffer to read from + * @param out The output buffer to write to + * @throws CompressionException If the read offset is invalid + */ + private static void decodeCopyWith2ByteOffset(byte tag, ByteBuf in, ByteBuf out) { + int initialIndex = in.readerIndex(); + int length = 1 + (tag >> 2 & 0x03f); + int offset = 1 + (in.readByte() & 0x0ff) + + (in.readByte() & 0x0ff) << 8; + + validateOffset(offset, initialIndex); + + in.markReaderIndex(); + in.readerIndex(initialIndex - offset); + in.readBytes(out, length); + in.resetReaderIndex(); + } + + /** + * Reads a compressed reference offset and length from the supplied input + * buffer, seeks back to the appropriate place in the input buffer and + * writes the found data to the supplied output stream. + * + * @param tag The tag used to identify this as a copy is also used to encode + * the length and part of the offset + * @param in The input buffer to read from + * @param out The output buffer to write to + * @throws CompressionException If the read offset is invalid + */ + private static void decodeCopyWith4ByteOffset(byte tag, ByteBuf in, ByteBuf out) { + int initialIndex = in.readerIndex(); + int length = 1 + (tag >> 2 & 0x03F); + int offset = 1 + (in.readByte() & 0x0ff) + + ((in.readByte() & 0x0ff) << 8) + + ((in.readByte() & 0x0ff) << 16) + + ((in.readByte() & 0x0ff) << 24); + + validateOffset(offset, initialIndex); + + in.markReaderIndex(); + in.readerIndex(initialIndex - offset); + in.readBytes(out, length); + in.resetReaderIndex(); + } + + /** + * Validates that the offset extracted from a compressed reference is within + * the permissible bounds of an offset (4 <= offset <= 32768), and does not + * exceed the length of the chunk currently read so far. + * + * @param offset The offset extracted from the compressed reference + * @param chunkSizeSoFar The number of bytes read so far from this chunk + * @throws CompressionException if the offset is invalid + */ + private static void validateOffset(int offset, int chunkSizeSoFar) { + if (offset > Short.MAX_VALUE) { + throw new CompressionException("Offset exceeds maximum permissible value"); + } + + if (offset <= 4) { + throw new CompressionException("Offset is less than minimum permissible value"); + } + + if (offset > chunkSizeSoFar) { + throw new CompressionException("Offset exceeds size of chunk"); + } + } +} diff --git a/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtil.java b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtil.java new file mode 100644 index 0000000000..8050dd9fdd --- /dev/null +++ b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtil.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012 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.snappy; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.compression.CompressionException; + +import java.util.zip.CRC32; + +public final class SnappyChecksumUtil { + /** + * Computes the CRC32 checksum of the supplied data, performs the "mask" operation + * on the computed checksum, and then compares the resulting masked checksum to the + * supplied checksum. + * + * @param slice The input data to calculate the CRC32 checksum of + * @param checksum The checksum decoded from the stream to compare against + * @throws CompressionException If the calculated and supplied checksums do not match + */ + public static void validateChecksum(ByteBuf slice, int checksum) { + if (calculateChecksum(slice) != checksum) { + throw new CompressionException("Uncompressed data did not match checksum"); + } + } + + /** + * Computes the CRC32 checksum of the supplied data and performs the "mask" operation + * on the computed checksum + * + * @param slice The input data to calculate the CRC32 checksum of + */ + public static int calculateChecksum(ByteBuf slice) { + CRC32 crc32 = new CRC32(); + try { + if (slice.hasArray()) { + crc32.update(slice.array()); + } else { + byte[] array = new byte[slice.readableBytes()]; + slice.markReaderIndex(); + slice.readBytes(array); + slice.resetReaderIndex(); + crc32.update(array); + } + + return maskChecksum((int) crc32.getValue()); + } finally { + crc32.reset(); + } + } + + /** + * From the spec: + * + * "Checksums are not stored directly, but masked, as checksumming data and + * then its own checksum can be problematic. The masking is the same as used + * in Apache Hadoop: Rotate the checksum by 15 bits, then add the constant + * 0xa282ead8 (using wraparound as normal for unsigned integers)." + * + * @param checksum The actual checksum of the data + * @return The masked checksum + */ + static int maskChecksum(int checksum) { + return (checksum >> 15 | checksum << 17) + 0xa282ead8; + } + + // utility class + private SnappyChecksumUtil() { } +} diff --git a/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoder.java b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoder.java new file mode 100644 index 0000000000..226fd99f4d --- /dev/null +++ b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoder.java @@ -0,0 +1,189 @@ +/* + * Copyright 2012 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.snappy; + +import java.nio.charset.Charset; +import java.util.Arrays; + +import static io.netty.handler.codec.compression.snappy.SnappyChecksumUtil.validateChecksum; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToByteDecoder; +import io.netty.handler.codec.compression.CompressionException; + +/** + * Uncompresses a {@link ByteBuf} encoded with the Snappy framing format. + * + * See http://code.google.com/p/snappy/source/browse/trunk/framing_format.txt + */ +public class SnappyFramedDecoder extends ByteToByteDecoder { + enum ChunkType { + STREAM_IDENTIFIER, + COMPRESSED_DATA, + UNCOMPRESSED_DATA, + RESERVED_UNSKIPPABLE, + RESERVED_SKIPPABLE + } + + private static final byte[] SNAPPY = "sNaPpY".getBytes(Charset.forName("US-ASCII")); + + private final Snappy snappy = new Snappy(); + private final boolean validateChecksums; + + private int chunkLength; + private ChunkType chunkType; + private boolean started; + + /** + * Creates a new snappy-framed decoder with validation of checksums + * turned off + */ + public SnappyFramedDecoder() { + this(false); + } + + /** + * Creates a new snappy-framed decoder with validation of checksums + * as specified. + * + * @param validateChecksums + * If true, the checksum field will be validated against the actual + * uncompressed data, and if the checksums do not match, a suitable + * {@link CompressionException} will be thrown + */ + public SnappyFramedDecoder(boolean validateChecksums) { + this.validateChecksums = validateChecksums; + } + + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + if (!in.readable()) { + return; + } + + while (in.readable()) { + if (chunkLength == 0) { + if (in.readableBytes() < 3) { + // We need to be at least able to read the chunk type identifier (one byte), + // and the length of the chunk (2 bytes) in order to proceed + return; + } + + byte type = in.readByte(); + chunkType = mapChunkType(type); + chunkLength = (in.readByte() & 0x0ff) + + ((in.readByte() & 0x0ff) << 8); + + // The spec mandates that reserved unskippable chunks must immediately + // return an error, as we must assume that we cannot decode the stream + // correctly + if (chunkType == ChunkType.RESERVED_UNSKIPPABLE) { + throw new CompressionException("Found reserved unskippable chunk type: " + type); + } + } + + if (chunkLength == 0 || in.readableBytes() < chunkLength) { + // Wait until the entire chunk is available, as it will prevent us from + // having to buffer the data here instead + return; + } + + int checksum; + + switch(chunkType) { + case STREAM_IDENTIFIER: + if (chunkLength != SNAPPY.length) { + throw new CompressionException("Unexpected length of stream identifier: " + chunkLength); + } + + byte[] identifier = new byte[chunkLength]; + in.readBytes(identifier); + + if (!Arrays.equals(identifier, SNAPPY)) { + throw new CompressionException("Unexpected stream identifier contents. Mismatched snappy " + + "protocol version?"); + } + + started = true; + + break; + case RESERVED_SKIPPABLE: + if (!started) { + throw new CompressionException("Received RESERVED_SKIPPABLE tag before STREAM_IDENTIFIER"); + } + in.skipBytes(chunkLength); + break; + case UNCOMPRESSED_DATA: + if (!started) { + throw new CompressionException("Received UNCOMPRESSED_DATA tag before STREAM_IDENTIFIER"); + } + checksum = in.readByte() & 0x0ff + + (in.readByte() << 8 & 0x0ff) + + (in.readByte() << 16 & 0x0ff) + + (in.readByte() << 24 & 0x0ff); + if (validateChecksums) { + ByteBuf data = in.readBytes(chunkLength); + validateChecksum(data, checksum); + out.writeBytes(data); + } else { + in.readBytes(out, chunkLength); + } + break; + case COMPRESSED_DATA: + if (!started) { + throw new CompressionException("Received COMPRESSED_DATA tag before STREAM_IDENTIFIER"); + } + checksum = in.readByte() & 0x0ff + + (in.readByte() << 8 & 0x0ff) + + (in.readByte() << 16 & 0x0ff) + + (in.readByte() << 24 & 0x0ff); + if (validateChecksums) { + ByteBuf uncompressed = ctx.alloc().buffer(); + snappy.decode(in, uncompressed, chunkLength); + validateChecksum(uncompressed, checksum); + } else { + snappy.decode(in, out, chunkLength); + } + snappy.reset(); + break; + } + + chunkLength = 0; + } + } + + /** + * Decodes the chunk type from the type tag byte. + * + * @param type The tag byte extracted from the stream + * @return The appropriate {@link ChunkType}, defaulting to + * {@link ChunkType#RESERVED_UNSKIPPABLE} + */ + static ChunkType mapChunkType(byte type) { + if (type == 0) { + return ChunkType.COMPRESSED_DATA; + } else if (type == 1) { + return ChunkType.UNCOMPRESSED_DATA; + } else if (type == -0x80) { + return ChunkType.STREAM_IDENTIFIER; + } else if ((type & 0x80) == 0x80) { + return ChunkType.RESERVED_SKIPPABLE; + } else { + return ChunkType.RESERVED_UNSKIPPABLE; + } + } +} diff --git a/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoder.java b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoder.java new file mode 100644 index 0000000000..a467bd8d7f --- /dev/null +++ b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoder.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012 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.snappy; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToByteEncoder; + +import static io.netty.handler.codec.compression.snappy.SnappyChecksumUtil.*; + +/** + * Compresses a {@link ByteBuf} using the Snappy framing format. + * + * See http://code.google.com/p/snappy/source/browse/trunk/framing_format.txt + */ +public class SnappyFramedEncoder extends ByteToByteEncoder { + /** + * The minimum amount that we'll consider actually attempting to compress. + * This value is preamble + the minimum length our Snappy service will + * compress (instead of just emitting a literal). + */ + private static final int MIN_COMPRESSIBLE_LENGTH = 18; + + /** + * All streams should start with the "Stream identifier", containing chunk + * type 0xff, a length field of 0x6, and 'sNaPpY' in ASCII. + */ + private static final byte[] STREAM_START = { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59 + }; + + private final Snappy snappy = new Snappy(); + private boolean started; + + @Override + public void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + if (!in.readable()) { + return; + } + + if (!started) { + started = true; + out.writeBytes(STREAM_START); + } + + final int chunkLength = in.readableBytes(); + if (chunkLength > MIN_COMPRESSIBLE_LENGTH) { + // If we have lots of available data, break it up into smaller chunks + int numberOfChunks = 1 + chunkLength / Short.MAX_VALUE; + for (int i = 0; i < numberOfChunks; i++) { + int subChunkLength = Math.min(Short.MAX_VALUE, chunkLength); + out.writeByte(0); + writeChunkLength(out, subChunkLength); + ByteBuf slice = in.slice(); + calculateAndWriteChecksum(slice, out); + + snappy.encode(slice, out, subChunkLength); + } + } else { + out.writeByte(1); + writeChunkLength(out, chunkLength); + ByteBuf slice = in.slice(); + calculateAndWriteChecksum(slice, out); + out.writeBytes(slice); + } + + in.readerIndex(in.readerIndex() + chunkLength); + } + + /** + * Writes the 2-byte chunk length to the output buffer. + * + * @param out The buffer to write to + * @param chunkLength The length to write + */ + private static void writeChunkLength(ByteBuf out, int chunkLength) { + out.writeByte(chunkLength & 0x0ff); + out.writeByte(chunkLength >> 8 & 0x0ff); + } + + /** + * Calculates and writes the 4-byte checksum to the output buffer + * + * @param slice The data to calculate the checksum for + * @param out The output buffer to write the checksum to + */ + private static void calculateAndWriteChecksum(ByteBuf slice, ByteBuf out) { + int checksum = calculateChecksum(slice); + out.writeByte(checksum & 0x0ff); + out.writeByte(checksum >> 8 & 0x0ff); + out.writeByte(checksum >> 16 & 0x0ff); + out.writeByte(checksum >> 24 & 0x0ff); + } +} diff --git a/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/package-info.java b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/package-info.java new file mode 100644 index 0000000000..17b6069924 --- /dev/null +++ b/codec-snappy/src/main/java/io/netty/handler/codec/compression/snappy/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012 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. + */ + +/** + * Encoder and decoder for the Snappy compression protocol. + */ +package io.netty.handler.codec.compression.snappy; diff --git a/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtilTest.java b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtilTest.java new file mode 100644 index 0000000000..f10684ef37 --- /dev/null +++ b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyChecksumUtilTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012 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.snappy; + +import static org.junit.Assert.assertEquals; + +import static io.netty.handler.codec.compression.snappy.SnappyChecksumUtil.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.compression.CompressionException; + +import org.junit.Test; + +public class SnappyChecksumUtilTest { + @Test + public void testCalculateChecksum() { + ByteBuf input = Unpooled.wrappedBuffer(new byte[] { + 'n', 'e', 't', 't', 'y' + }); + assertEquals(maskChecksum(0xddaa8ce6), calculateChecksum(input)); + } + + @Test + public void testValidateChecksumMatches() { + ByteBuf input = Unpooled.wrappedBuffer(new byte[] { + 'y', 't', 't', 'e', 'n' + }); + + validateChecksum(input, maskChecksum(0x37c55159)); + } + + @Test(expected = CompressionException.class) + public void testValidateChecksumFails() { + ByteBuf input = Unpooled.wrappedBuffer(new byte[] { + 'y', 't', 't', 'e', 'n' + }); + + validateChecksum(input, maskChecksum(0xddaa8ce6)); + } +} diff --git a/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoderTest.java b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoderTest.java new file mode 100644 index 0000000000..1a1c08b32c --- /dev/null +++ b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedDecoderTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012 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.snappy; + +import static org.junit.Assert.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.compression.CompressionException; + +import org.junit.Test; + +public class SnappyFramedDecoderTest { + private final SnappyFramedDecoder decoder = new SnappyFramedDecoder(); + + @Test(expected = CompressionException.class) + public void testReservedUnskippableChunkTypeCausesError() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x03, 0x01, 0x00, 0x00 + }); + + decoder.decode(null, in, null); + } + + @Test(expected = CompressionException.class) + public void testInvalidStreamIdentifierLength() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, 0x05, 0x00, 'n', 'e', 't', 't', 'y' + }); + + decoder.decode(null, in, null); + } + + @Test(expected = CompressionException.class) + public void testInvalidStreamIdentifierValue() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, 0x06, 0x00, 's', 'n', 'e', 't', 't', 'y' + }); + + decoder.decode(null, in, null); + } + + @Test(expected = CompressionException.class) + public void testReservedSkippableBeforeStreamIdentifier() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x7f, 0x06, 0x00, 's', 'n', 'e', 't', 't', 'y' + }); + + decoder.decode(null, in, null); + } + + @Test(expected = CompressionException.class) + public void testUncompressedDataBeforeStreamIdentifier() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x01, 0x05, 0x00, 'n', 'e', 't', 't', 'y' + }); + + decoder.decode(null, in, null); + } + + @Test(expected = CompressionException.class) + public void testCompressedDataBeforeStreamIdentifier() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x00, 0x05, 0x00, 'n', 'e', 't', 't', 'y' + }); + + decoder.decode(null, in, null); + } + + @Test + public void testReservedSkippableSkipsInput() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + -0x7f, 0x05, 0x00, 'n', 'e', 't', 't', 'y' + }); + + ByteBuf out = Unpooled.unmodifiableBuffer(Unpooled.EMPTY_BUFFER); + + decoder.decode(null, in, out); + + assertEquals(17, in.readerIndex()); + } + + @Test + public void testUncompressedDataAppendsToOut() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y' + }); + + ByteBuf out = Unpooled.buffer(5); + + decoder.decode(null, in, out); + + byte[] expected = { + 'n', 'e', 't', 't', 'y' + }; + assertArrayEquals(expected, out.array()); + } + + @Test + public void testCompressedDataDecodesAndAppendsToOut() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79 // "netty" + }); + + ByteBuf out = Unpooled.buffer(5); + + decoder.decode(null, in, out); + + byte[] expected = { + 'n', 'e', 't', 't', 'y' + }; + assertArrayEquals(expected, out.array()); + } +} diff --git a/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoderTest.java b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoderTest.java new file mode 100644 index 0000000000..1009befdaf --- /dev/null +++ b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyFramedEncoderTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012 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.snappy; + +import static org.junit.Assert.assertArrayEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import org.junit.Test; + +public class SnappyFramedEncoderTest { + private final SnappyFramedEncoder encoder = new SnappyFramedEncoder(); + + @Test + public void testSmallAmountOfDataIsUncompressed() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 'n', 'e', 't', 't', 'y' + }); + + ByteBuf out = Unpooled.buffer(21); + + encoder.encode(null, in, out); + + byte[] expected = { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x01, 0x05, 0x00, 0x2d, -0x5a, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y' + }; + assertArrayEquals(expected, out.array()); + } + + @Test + public void testLargeAmountOfDataIsCompressed() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 'n', 'e', 't', 't', 'y', 'n', 'e', 't', 't', 'y', + 'n', 'e', 't', 't', 'y', 'n', 'e', 't', 't', 'y' + }); + + ByteBuf out = Unpooled.buffer(26); + + encoder.encode(null, in, out); + + byte[] expected = { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x00, 0x14, 0x00, 0x7b, 0x1f, 0x65, 0x64, + 0x14, 0x10, + 'n', 'e', 't', 't', 'y', + 0x3a, 0x05, 0x00 + }; + assertArrayEquals(expected, out.array()); + } + + @Test + public void testStreamStartIsOnlyWrittenOnce() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 'n', 'e', 't', 't', 'y' + }); + + ByteBuf out = Unpooled.buffer(33); + + encoder.encode(null, in, out); + in.readerIndex(0); // rewind the buffer to write the same data + encoder.encode(null, in, out); + + byte[] expected = { + -0x80, 0x06, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, + 0x01, 0x05, 0x00, 0x2d, -0x5a, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y', + 0x01, 0x05, 0x00, 0x2d, -0x5a, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y', + }; + assertArrayEquals(expected, out.array()); + } +} diff --git a/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyTest.java b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyTest.java new file mode 100644 index 0000000000..ee6f62f338 --- /dev/null +++ b/codec-snappy/src/test/java/io/netty/handler/codec/compression/snappy/SnappyTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012 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.snappy; + +import static org.junit.Assert.assertArrayEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.compression.CompressionException; + +import org.junit.After; +import org.junit.Test; + +public class SnappyTest { + private final Snappy snappy = new Snappy(); + + @After + public void resetSnappy() { + snappy.reset(); + } + + @Test + public void testDecodeLiteral() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x05, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79 // "netty" + }); + ByteBuf out = Unpooled.buffer(5); + snappy.decode(in, out, 7); + + // "netty" + byte[] expected = { + 0x6e, 0x65, 0x74, 0x74, 0x79 + }; + assertArrayEquals("Literal was not decoded correctly", expected, out.array()); + } + + @Test + public void testDecodeCopyWith1ByteOffset() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x01, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79, // "netty" + 0x05 << 2 | 0x01, // copy with 1-byte offset + length + 0x05 // offset + }); + ByteBuf out = Unpooled.buffer(10); + snappy.decode(in, out, 9); + + // "nettynetty" - we saved a whole byte :) + byte[] expected = { + 0x6e, 0x65, 0x74, 0x74, 0x79, 0x6e, 0x65, 0x74, 0x74, 0x79 + }; + assertArrayEquals("Copy was not decoded correctly", expected, out.array()); + } + + @Test(expected = CompressionException.class) + public void testDecodeCopyWithTinyOffset() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x01, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79, // "netty" + 0x05 << 2 | 0x01, // copy with 1-byte offset + length + 0x03 // INVALID offset (< 4) + }); + ByteBuf out = Unpooled.buffer(10); + snappy.decode(in, out, 9); + } + + @Test(expected = CompressionException.class) + public void testDecodeCopyWithOffsetBeforeChunk() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x01, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79, // "netty" + 0x05 << 2 | 0x01, // copy with 1-byte offset + length + 0x0b // INVALID offset (greater than chunk size) + }); + ByteBuf out = Unpooled.buffer(10); + snappy.decode(in, out, 9); + } + + @Test(expected = CompressionException.class) + public void testDecodeWithOverlyLongPreamble() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + -0x80, -0x80, -0x80, -0x80, 0x7f, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79, // "netty" + }); + ByteBuf out = Unpooled.buffer(10); + snappy.decode(in, out, 9); + } + + @Test + public void encodeShortTextIsLiteral() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer(new byte[] { + 0x6e, 0x65, 0x74, 0x74, 0x79 + }); + ByteBuf out = Unpooled.buffer(7); + snappy.encode(in, out, 5); + + byte[] expected = { + 0x05, // preamble length + 0x04 << 2, // literal tag + length + 0x6e, 0x65, 0x74, 0x74, 0x79 // "netty" + }; + assertArrayEquals("Encoded literal was invalid", expected, out.array()); + } + + @Test + public void encodeLongTextUsesCopy() throws Exception { + ByteBuf in = Unpooled.wrappedBuffer( + ("Netty has been designed carefully with the experiences " + + "earned from the implementation of a lot of protocols " + + "such as FTP, SMTP, HTTP, and various binary and " + + "text-based legacy protocols").getBytes("US-ASCII") + ); + ByteBuf out = Unpooled.buffer(180); + snappy.encode(in, out, in.readableBytes()); + + // The only compressibility in the above are the words "the ", + // and "protocols", so this is a literal, followed by a copy + // followed by another literal, followed by another copy + byte[] expected = { + -0x49, 0x01, // preamble length + -0x10, 0x42, // literal tag + length + + // Literal + 0x4e, 0x65, 0x74, 0x74, 0x79, 0x20, 0x68, 0x61, 0x73, 0x20, + 0x62, 0x65, 0x65, 0x6e, 0x20, 0x64, 0x65, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x20, 0x63, 0x61, 0x72, 0x65, 0x66, 0x75, + 0x6c, 0x6c, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x20, 0x65, 0x61, 0x72, 0x6e, 0x65, + 0x64, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, + + // First copy (the) + 0x01, 0x1C, -0x10, + + // Next literal + 0x66, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x61, + 0x20, 0x6c, 0x6f, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x73, 0x20, 0x73, 0x75, + 0x63, 0x68, 0x20, 0x61, 0x73, 0x20, 0x46, 0x54, 0x50, 0x2c, + 0x20, 0x53, 0x4d, 0x54, 0x50, 0x2c, 0x20, 0x48, 0x54, 0x54, + 0x50, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x76, 0x61, 0x72, + 0x69, 0x6f, 0x75, 0x73, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, + 0x79, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x74, 0x65, 0x78, 0x74, + 0x2d, 0x62, 0x61, 0x73, 0x65, 0x64, 0x20, 0x6c, 0x65, 0x67, + 0x61, 0x63, 0x79, 0x20, + + // Second copy (protocols) + 0x15, 0x4c + }; + + assertArrayEquals("Encoded result was incorrect", expected, out.array()); + } +} diff --git a/license/LICENSE.snappy.txt b/license/LICENSE.snappy.txt new file mode 100644 index 0000000000..ba6a3ae553 --- /dev/null +++ b/license/LICENSE.snappy.txt @@ -0,0 +1,28 @@ +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pom.xml b/pom.xml index 2baeca654a..b00c56967e 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ buffer codec codec-http + codec-snappy codec-socks transport handler