/** * * Copyright 2018 Florian Schmaus * * 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 org.jivesoftware.smack.compression.zlib; import java.io.IOException; import java.nio.ByteBuffer; import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.Inflater; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.XmppInputOutputFilter; import org.jivesoftware.smack.compression.XMPPInputOutputStream; import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod; import org.jivesoftware.smack.compression.XmppCompressionFactory; public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { public static final ZlibXmppCompressionFactory INSTANCE = new ZlibXmppCompressionFactory(); private ZlibXmppCompressionFactory() { super("zlib", 100); } @Override public XmppInputOutputFilter fabricate(ConnectionConfiguration configuration) { return new ZlibXmppInputOutputFilter(); } private static final class ZlibXmppInputOutputFilter implements XmppInputOutputFilter { private static final int MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE = 4; private static final int MINIMUM_OUTPUT_BUFFER_INCREASE = 480; private final Deflater compressor; private final Inflater decompressor = new Inflater(); private long compressorInBytes; private long compressorOutBytes; private long decompressorInBytes; private long decompressorOutBytes; private int maxOutputOutput = -1; private int maxInputOutput = -1; private int maxBytesWrittenAfterFullFlush = -1; private ZlibXmppInputOutputFilter() { this(Deflater.DEFAULT_COMPRESSION); } private ZlibXmppInputOutputFilter(int compressionLevel) { compressor = new Deflater(compressionLevel); } private ByteBuffer outputBuffer; @Override public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, boolean moreDataAvailable) throws IOException { if (destinationAddressChanged && XMPPInputOutputStream.getFlushMethod() == FlushMethod.FULL_FLUSH) { outputBuffer = ByteBuffer.allocate(256); int bytesWritten = deflate(Deflater.FULL_FLUSH); maxBytesWrittenAfterFullFlush = Math.max(bytesWritten, maxBytesWrittenAfterFullFlush); compressorOutBytes += bytesWritten; } if (outputData == null && outputBuffer == null) { return OutputResult.NO_OUTPUT; } int bytesRemaining = outputData.remaining(); if (outputBuffer == null) { final int outputBufferSize = bytesRemaining < MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE ? MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE : bytesRemaining; // We assume that the compressed data will not take more space as the uncompressed. Even if this is not // always true, the automatic buffer resize mechanism of deflate() will take care. outputBuffer = ByteBuffer.allocate(outputBufferSize); } // There is an invariant of Deflater/Inflater that input should only be set if needsInput() return true. assert compressor.needsInput(); final byte[] compressorInputBuffer; final int compressorInputBufferOffset, compressorInputBufferLength; if (outputData.hasArray()) { compressorInputBuffer = outputData.array(); compressorInputBufferOffset = outputData.arrayOffset(); compressorInputBufferLength = outputData.remaining(); } else { compressorInputBuffer = new byte[outputData.remaining()]; compressorInputBufferOffset = 0; compressorInputBufferLength = compressorInputBuffer.length; outputData.get(compressorInputBuffer); } compressorInBytes += compressorInputBufferLength; compressor.setInput(compressorInputBuffer, compressorInputBufferOffset, compressorInputBufferLength); int flushMode; if (moreDataAvailable) { flushMode = Deflater.NO_FLUSH; } else { flushMode = Deflater.SYNC_FLUSH; } int bytesWritten = deflate(flushMode); maxOutputOutput = Math.max(outputBuffer.position(), maxOutputOutput); compressorOutBytes += bytesWritten; OutputResult outputResult = new OutputResult(outputBuffer); outputBuffer = null; return outputResult; } private int deflate(int flushMode) { // compressor.finish(); int totalBytesWritten = 0; while (true) { int initialOutputBufferPosition = outputBuffer.position(); byte[] buffer = outputBuffer.array(); int length = outputBuffer.limit() - initialOutputBufferPosition; int bytesWritten = compressor.deflate(buffer, initialOutputBufferPosition, length, flushMode); int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten; outputBuffer.position(newOutputBufferPosition); totalBytesWritten += bytesWritten; if (compressor.needsInput() && outputBuffer.hasRemaining()) { break; } int increasedBufferSize = outputBuffer.capacity() * 2; if (increasedBufferSize < MINIMUM_OUTPUT_BUFFER_INCREASE) { increasedBufferSize = MINIMUM_OUTPUT_BUFFER_INCREASE; } ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize); outputBuffer.flip(); newCurrentOutputBuffer.put(outputBuffer); outputBuffer = newCurrentOutputBuffer; } return totalBytesWritten; } @Override public ByteBuffer input(ByteBuffer inputData) throws IOException { int bytesRemaining = inputData.remaining(); final byte[] inputBytes; final int offset, length; if (inputData.hasArray()) { inputBytes = inputData.array(); offset = inputData.arrayOffset(); length = inputData.remaining(); } else { // Copy since we are dealing with a buffer whose array is not accessible (possibly a direct buffer). inputBytes = new byte[bytesRemaining]; inputData.get(inputBytes); offset = 0; length = inputBytes.length; } decompressorInBytes += length; decompressor.setInput(inputBytes, offset, length); int bytesInflated; // Assume that the inflated/decompressed result will be roughly at most twice the size of the compressed // variant. It appears to hold most of the times, if not, then the buffer resize mechanism will take care of // it. ByteBuffer outputBuffer = ByteBuffer.allocate(2 * length); while (true) { byte[] inflateOutputBuffer = outputBuffer.array(); int inflateOutputBufferOffset = outputBuffer.position(); int inflateOutputBufferLength = outputBuffer.limit() - inflateOutputBufferOffset; try { bytesInflated = decompressor.inflate(inflateOutputBuffer, inflateOutputBufferOffset, inflateOutputBufferLength); } catch (DataFormatException e) { throw new IOException(e); } outputBuffer.position(inflateOutputBufferOffset + bytesInflated); decompressorOutBytes += bytesInflated; if (decompressor.needsInput()) { break; } int increasedBufferSize = outputBuffer.capacity() * 2; ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize); outputBuffer.flip(); increasedOutputBuffer.put(outputBuffer); outputBuffer = increasedOutputBuffer; } if (bytesInflated == 0) { return null; } maxInputOutput = Math.max(outputBuffer.position(), maxInputOutput); return outputBuffer; } @Override public Stats getStats() { return new Stats(this); } } public static final class Stats { public final long compressorInBytes; public final long compressorOutBytes; public final double compressionRatio; public final long decompressorInBytes; public final long decompressorOutBytes; public final double decompressionRatio; public final int maxOutputOutput; public final int maxInputOutput; public final int maxBytesWrittenAfterFullFlush; private Stats(ZlibXmppInputOutputFilter filter) { // Note that we read the out bytes before the in bytes to not over approximate the compression ratio. compressorOutBytes = filter.compressorOutBytes; compressorInBytes = filter.compressorInBytes; compressionRatio = (double) compressorOutBytes / compressorInBytes; decompressorOutBytes = filter.decompressorOutBytes; decompressorInBytes = filter.decompressorInBytes; decompressionRatio = (double) decompressorInBytes / decompressorOutBytes; maxOutputOutput = filter.maxOutputOutput; maxInputOutput = filter.maxInputOutput; maxBytesWrittenAfterFullFlush = filter.maxBytesWrittenAfterFullFlush; } private transient String toStringCache; @Override public String toString() { if (toStringCache != null) { return toStringCache; } toStringCache = "compressor-in-bytes: " + compressorInBytes + '\n' + "compressor-out-bytes: " + compressorOutBytes + '\n' + "compression-ratio: " + compressionRatio + '\n' + "decompressor-in-bytes: " + decompressorInBytes + '\n' + "decompressor-out-bytes: " + decompressorOutBytes + '\n' + "decompression-ratio: " + decompressionRatio + '\n' + "max-output-output: " + maxOutputOutput + '\n' + "max-input-output: " + maxInputOutput + '\n' + "max-bytes-written-after-full-flush: " + maxBytesWrittenAfterFullFlush + '\n' ; return toStringCache; } } }