com.google.archivepatcher.applier.PartiallyCompressingOutputStream Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of archive-patcher Show documentation
Show all versions of archive-patcher Show documentation
Google Archive Patcher (EIDU fork)
The newest version!
// Copyright 2016 Google Inc. All rights reserved.
//
// 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 com.google.archivepatcher.applier;
import com.google.archivepatcher.shared.IDeflater;
import com.google.archivepatcher.shared.IDeflaterOutputStream;
import com.google.archivepatcher.shared.JreDeflateParameters;
import com.google.archivepatcher.shared.TypedRange;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiFunction;
/**
* An {@link OutputStream} that is pre-configured to compress some of the bytes that are written to
* it according to the specified parameters.
*/
public class PartiallyCompressingOutputStream extends FilterOutputStream {
/**
* The underlying stream.
*/
private final OutputStream normalOut;
private final BiFunction deflaterFactory;
/**
* The deflater, non-null only during compression.
*/
private IDeflater deflater = null;
/**
* The deflater stream, non-null only during compression.
*/
private IDeflaterOutputStream deflaterOut = null;
/**
* Used when writing one byte at a time.
*/
private final byte[] internalCopyBuffer = new byte[1];
/**
* The size of the buffer to use when compressing bytes.
*/
private final int compressionBufferSize;
/**
* The number of bytes written so far.
*/
private long numBytesWritten;
/**
* The iterator that is used to iterate over the compression ranges.
*/
private final Iterator> rangeIterator;
/**
* The compress range that is either being worked on or that is coming up next.
*/
private TypedRange nextCompressedRange = null;
/**
* The last {@link JreDeflateParameters} that were used.
*/
private JreDeflateParameters lastDeflateParameters = null;
/**
* Creates a new stream that wraps the specified other stream, compressing the specified ranges
* with the specified parameters. All unspecified ranges are implicitly copied without
* modification.
* @param compressionRanges ranges to be compressed, with accompanying parameters
* @param out the stream to write to
* @param compressionBufferSize the size of the buffer to use when compressing data
*/
public PartiallyCompressingOutputStream(
List> compressionRanges,
OutputStream out,
int compressionBufferSize,
BiFunction deflaterFactory) {
super(out);
this.normalOut = out;
this.compressionBufferSize = compressionBufferSize;
this.deflaterFactory = deflaterFactory;
rangeIterator = compressionRanges.iterator();
if (rangeIterator.hasNext()) {
nextCompressedRange = rangeIterator.next();
} else {
// Degenerate case, no compression at all/
nextCompressedRange = null;
}
}
@Override
public void write(int b) throws IOException {
internalCopyBuffer[0] = (byte) b;
write(internalCopyBuffer, 0, 1);
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
int writtenSoFar = 0;
while (writtenSoFar < length) {
writtenSoFar += writeChunk(buffer, offset + writtenSoFar, length - writtenSoFar);
}
}
/**
* Write up to length bytes from the specified buffer to the underlying stream. For
* simplicity, this method stops at the edges of ranges; it is always either copying OR
* compressing bytes, but never both. All manipulation of the compression state machinery is done
* within this method. When the end of a compression range is reached it is completely flushed to
* the output stream, to keep things as straightforward as possible.
* @param buffer the buffer to copy/compress bytes from
* @param offset the offset at which to start copying/compressing
* @param length the maximum number of bytes to copy or compress
* @return the number of bytes of the buffer that have been consumed
*/
private int writeChunk(byte[] buffer, int offset, int length) throws IOException {
if (bytesTillCompressionStarts() == 0 && !currentlyCompressing()) {
// Compression will begin immediately.
JreDeflateParameters parameters = nextCompressedRange.getMetadata();
if (deflater == null) {
deflater = deflaterFactory.apply(parameters.level, parameters.nowrap);
} else if (lastDeflateParameters.nowrap != parameters.nowrap) {
// Last deflater must be destroyed because nowrap settings do not match.
deflater.end();
deflater = deflaterFactory.apply(parameters.level, parameters.nowrap);
}
// Deflater will already have been reset at the end of this method, no need to do it again.
// Just set up the right parameters.
deflater.setLevel(parameters.level);
deflater.setStrategy(parameters.strategy);
deflaterOut = new IDeflaterOutputStream(normalOut, deflater, compressionBufferSize);
}
int numBytesToWrite;
OutputStream writeTarget;
if (currentlyCompressing()) {
// Don't write past the end of the compressed range.
numBytesToWrite = (int) Math.min(length, bytesTillCompressionEnds());
writeTarget = deflaterOut;
} else {
writeTarget = normalOut;
if (nextCompressedRange == null) {
// All compression ranges have been consumed.
numBytesToWrite = length;
} else {
// Don't write past the point where the next compressed range begins.
numBytesToWrite = (int) Math.min(length, bytesTillCompressionStarts());
}
}
writeTarget.write(buffer, offset, numBytesToWrite);
numBytesWritten += numBytesToWrite;
if (currentlyCompressing() && bytesTillCompressionEnds() == 0) {
// Compression range complete. Finish the output and set up for the next run.
deflaterOut.finish();
deflaterOut.flush();
deflaterOut = null;
deflater.reset();
lastDeflateParameters = nextCompressedRange.getMetadata();
if (rangeIterator.hasNext()) {
// More compression ranges await in the future.
nextCompressedRange = rangeIterator.next();
} else {
// All compression ranges have been consumed.
nextCompressedRange = null;
deflater.end();
deflater = null;
}
}
return numBytesToWrite;
}
private boolean currentlyCompressing() {
return deflaterOut != null;
}
private long bytesTillCompressionStarts() {
if (nextCompressedRange == null) {
// All compression ranges have been consumed
return -1L;
}
return nextCompressedRange.getOffset() - numBytesWritten;
}
private long bytesTillCompressionEnds() {
if (nextCompressedRange == null) {
// All compression ranges have been consumed
return -1L;
}
return (nextCompressedRange.getOffset() + nextCompressedRange.getLength()) - numBytesWritten;
}
}