mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-09-27 18:19:33 +02:00
288 lines
11 KiB
Java
288 lines
11 KiB
Java
/**
|
|
*
|
|
* 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;
|
|
}
|
|
}
|
|
}
|