/** * Copyright 2014 Ricardo Padilha * * Licensed 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 net.dsys.snio.impl.codec; import java.nio.ByteBuffer; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; import javax.annotation.Nonnegative; import net.dsys.commons.api.exception.Bug; import net.dsys.snio.api.codec.InvalidEncodingException; import net.dsys.snio.api.codec.InvalidLengthException; import net.dsys.snio.api.codec.MessageCodec; /** * Frame encoding that compresses messages using deflate. Messages cannot be * longer than 65499 bytes to make sure that they will fit in an UDP datagram. * Thread-safety is guaranteed only between encoding and decoding, i.e., two * different threads can encode and decode at the same time, but two threads * cannot encode at the same time. * * @author Ricardo Padilha */ final class DeflateCodec implements MessageCodec { private static final int UNSIGNED_INT_MASK = Integer.MAX_VALUE; private static final int ZLIB_BLOCK_LENGTH = 0x3FFF; // 16383 private static final int ZLIB_FIXED_OVERHEAD = 6; private static final int ZLIB_BLOCK_OVERHEAD = 5; private static final int INT_LENGTH = Integer.SIZE / Byte.SIZE; private static final int HEADER_LENGTH = INT_LENGTH; private static final int FOOTER_LENGTH = 0; private static final int MAX_BODY_LENGTH = maxUncompressedLength(UNSIGNED_INT_MASK) - HEADER_LENGTH; private final int headerLength; private final int bodyLength; private final int compressedLength; private final int footerLength; private final int frameLength; private final Deflater deflater; private final Inflater inflater; private final byte[] deflaterInput; private final byte[] deflaterOutput; private final byte[] inflaterInput; private final byte[] inflaterOutput; DeflateCodec(@Nonnegative final int bodyLength) { if (bodyLength < 1 || bodyLength > MAX_BODY_LENGTH) { throw new IllegalArgumentException("bodyLength < 1 || bodyLength > MAX_BODY_LENGTH: " + bodyLength); } this.deflater = new Deflater(Deflater.BEST_SPEED, false); this.inflater = new Inflater(false); this.headerLength = HEADER_LENGTH; this.bodyLength = bodyLength; this.compressedLength = maxCompressedLength(bodyLength); this.footerLength = FOOTER_LENGTH; this.frameLength = headerLength + compressedLength + footerLength; this.deflaterInput = new byte[bodyLength]; this.deflaterOutput = new byte[compressedLength]; this.inflaterInput = new byte[compressedLength]; this.inflaterOutput = new byte[bodyLength]; } /** * The overhead for ZLIB is 6 bytes fixed + 5 bytes / 16KB block. * * @see http://www.zlib.net/zlib_tech.html */ private static int maxCompressedLength(final int length) { final int n = (length / ZLIB_BLOCK_LENGTH) + 1; return length + ZLIB_FIXED_OVERHEAD + n * ZLIB_BLOCK_OVERHEAD; } /** * The overhead for ZLIB is 6 bytes fixed + 5 bytes / 16KB block. * * @see http://www.zlib.net/zlib_tech.html */ private static int maxUncompressedLength(final int length) { final int n = (length / ZLIB_BLOCK_LENGTH) + 1; return length - ZLIB_FIXED_OVERHEAD - n * ZLIB_BLOCK_OVERHEAD; } /** * {@inheritDoc} */ @Override public int getHeaderLength() { return headerLength; } /** * {@inheritDoc} */ @Override public int getFooterLength() { return footerLength; } /** * {@inheritDoc} */ @Override public int getBodyLength() { return bodyLength; } /** * {@inheritDoc} */ @Override public int getFrameLength() { return frameLength; } /** * {@inheritDoc} */ @Override public int getEncodedLength(final ByteBuffer in) { return headerLength + maxCompressedLength(in.remaining()); } /** * {@inheritDoc} * * @throws InvalidLengthException */ @Override public boolean isValid(final ByteBuffer out) { final int length = out.remaining(); return length > 0 && length <= bodyLength; } /** * {@inheritDoc} */ @Override public void put(final ByteBuffer in, final ByteBuffer out) { final int inflated = in.remaining(); deflater.reset(); if (in.hasArray()) { final int offset = in.arrayOffset() + in.position(); deflater.setInput(in.array(), offset, inflated); in.position(in.position() + inflated); } else { in.get(deflaterInput, 0, inflated); deflater.setInput(deflaterInput, 0, inflated); } deflater.finish(); if (out.hasArray()) { final int offset = out.arrayOffset() + out.position() + HEADER_LENGTH; final int remaining = out.remaining() - HEADER_LENGTH; final int deflated = deflater.deflate(out.array(), offset, remaining); if (deflated < 1 || deflated > compressedLength) { throw new Bug("Unexpected deflated size: " + deflated); } out.putInt(deflated); out.position(out.position() + deflated); } else { final int deflated = deflater.deflate(deflaterOutput); if (deflated < 1 || deflated > compressedLength) { throw new Bug("Unexpected deflated size: " + deflated); } out.putInt(deflated); out.put(deflaterOutput, 0, deflated); } } /** * {@inheritDoc} */ @Override public boolean hasNext(final ByteBuffer in) throws InvalidEncodingException { final int rem = in.remaining(); if (rem < headerLength) { return false; } final int length = getDecodedLength(in); if (length < 1 || length > compressedLength) { throw new InvalidLengthException(length); } return (rem >= headerLength + length); } /** * {@inheritDoc} */ @Override public int getDecodedLength(final ByteBuffer in) { return in.getInt(in.position()) & UNSIGNED_INT_MASK; } /** * {@inheritDoc} */ @Override public void get(final ByteBuffer in, final ByteBuffer out) throws InvalidEncodingException { final int deflated = in.getInt() & UNSIGNED_INT_MASK; inflater.reset(); if (in.hasArray()) { final int offset = in.arrayOffset() + in.position(); inflater.setInput(in.array(), offset, deflated); in.position(in.position() + deflated); } else { in.get(inflaterInput, 0, deflated); inflater.setInput(inflaterInput, 0, deflated); } try { if (out.hasArray()) { final int offset = out.arrayOffset() + out.position(); final int inflated = inflater.inflate(out.array(), offset, out.remaining()); if (inflated < 1 || inflated > bodyLength) { throw new InvalidLengthException(inflated); } out.position(out.position() + inflated); } else { final int inflated = inflater.inflate(inflaterOutput); if (inflated < 1 || inflated > bodyLength) { throw new InvalidLengthException(inflated); } out.put(inflaterOutput, 0, inflated); } } catch (final DataFormatException e) { throw new InvalidEncodingException(e); } } /** * {@inheritDoc} */ @Override public void close() { deflater.end(); inflater.end(); } /** * {@inheritDoc} */ @Override public String toString() { return "DeflateCodec(" + headerLength + ":" + bodyLength + ")"; } }