org.tukaani.xz.XZOutputStream Maven / Gradle / Ivy
/*
* XZOutputStream
*
* Author: Lasse Collin
*
* This file has been put into the public domain.
* You can do whatever you want with this file.
*/
package org.tukaani.xz;
import java.io.OutputStream;
import java.io.IOException;
import org.tukaani.xz.common.EncoderUtil;
import org.tukaani.xz.common.StreamFlags;
import org.tukaani.xz.check.Check;
import org.tukaani.xz.index.IndexEncoder;
/**
* Compresses into the .xz file format.
*
* Examples
*
* Getting an output stream to compress with LZMA2 using the default
* settings and the default integrity check type (CRC64):
*
* FileOutputStream outfile = new FileOutputStream("foo.xz");
* XZOutputStream outxz = new XZOutputStream(outfile, new LZMA2Options());
*
*
* Using the preset level 8
for LZMA2 (the default
* is 6
) and SHA-256 instead of CRC64 for integrity checking:
*
* XZOutputStream outxz = new XZOutputStream(outfile, new LZMA2Options(8),
* XZ.CHECK_SHA256);
*
*
* Using the x86 BCJ filter together with LZMA2 to compress x86 executables
* and printing the memory usage information before creating the
* XZOutputStream:
*
* X86Options x86 = new X86Options();
* LZMA2Options lzma2 = new LZMA2Options();
* FilterOptions[] options = { x86, lzma2 };
* System.out.println("Encoder memory usage: "
* + FilterOptions.getEncoderMemoryUsage(options)
* + " KiB");
* System.out.println("Decoder memory usage: "
* + FilterOptions.getDecoderMemoryUsage(options)
* + " KiB");
* XZOutputStream outxz = new XZOutputStream(outfile, options);
*
*/
public class XZOutputStream extends FinishableOutputStream {
private OutputStream out;
private final StreamFlags streamFlags = new StreamFlags();
private final Check check;
private final IndexEncoder index = new IndexEncoder();
private BlockOutputStream blockEncoder = null;
private FilterEncoder[] filters;
/**
* True if the current filter chain supports flushing.
* If it doesn't support flushing, flush()
* will use endBlock()
as a fallback.
*/
private boolean filtersSupportFlushing;
private IOException exception = null;
private boolean finished = false;
private final byte[] tempBuf = new byte[1];
/**
* Creates a new XZ compressor using one filter and CRC64 as
* the integrity check. This constructor is equivalent to passing
* a single-member FilterOptions array to
* XZOutputStream(OutputStream, FilterOptions[])
.
*
* @param out output stream to which the compressed data
* will be written
*
* @param filterOptions
* filter options to use
*
* @throws UnsupportedOptionsException
* invalid filter chain
*
* @throws IOException may be thrown from out
*/
public XZOutputStream(OutputStream out, FilterOptions filterOptions)
throws IOException {
this(out, filterOptions, XZ.CHECK_CRC64);
}
/**
* Creates a new XZ compressor using one filter and the specified
* integrity check type. This constructor is equivalent to
* passing a single-member FilterOptions array to
* XZOutputStream(OutputStream, FilterOptions[], int)
.
*
* @param out output stream to which the compressed data
* will be written
*
* @param filterOptions
* filter options to use
*
* @param checkType type of the integrity check,
* for example XZ.CHECK_CRC32
*
* @throws UnsupportedOptionsException
* invalid filter chain
*
* @throws IOException may be thrown from out
*/
public XZOutputStream(OutputStream out, FilterOptions filterOptions,
int checkType) throws IOException {
this(out, new FilterOptions[] { filterOptions }, checkType);
}
/**
* Creates a new XZ compressor using 1-4 filters and CRC64 as
* the integrity check. This constructor is equivalent
* XZOutputStream(out, filterOptions, XZ.CHECK_CRC64)
.
*
* @param out output stream to which the compressed data
* will be written
*
* @param filterOptions
* array of filter options to use
*
* @throws UnsupportedOptionsException
* invalid filter chain
*
* @throws IOException may be thrown from out
*/
public XZOutputStream(OutputStream out, FilterOptions[] filterOptions)
throws IOException {
this(out, filterOptions, XZ.CHECK_CRC64);
}
/**
* Creates a new XZ compressor using 1-4 filters and the specified
* integrity check type.
*
* @param out output stream to which the compressed data
* will be written
*
* @param filterOptions
* array of filter options to use
*
* @param checkType type of the integrity check,
* for example XZ.CHECK_CRC32
*
* @throws UnsupportedOptionsException
* invalid filter chain
*
* @throws IOException may be thrown from out
*/
public XZOutputStream(OutputStream out, FilterOptions[] filterOptions,
int checkType) throws IOException {
this.out = out;
updateFilters(filterOptions);
streamFlags.checkType = checkType;
check = Check.getInstance(checkType);
encodeStreamHeader();
}
/**
* Updates the filter chain with a single filter.
* This is equivalent to passing a single-member FilterOptions array
* to updateFilters(FilterOptions[])
.
*
* @param filterOptions
* new filter to use
*
* @throws UnsupportedOptionsException
* unsupported filter chain, or trying to change
* the filter chain in the middle of a Block
*/
public void updateFilters(FilterOptions filterOptions)
throws XZIOException {
FilterOptions[] opts = new FilterOptions[1];
opts[0] = filterOptions;
updateFilters(opts);
}
/**
* Updates the filter chain with 1-4 filters.
*
* Currently this cannot be used to update e.g. LZMA2 options in the
* middle of a XZ Block. Use endBlock()
to finish the
* current XZ Block before calling this function. The new filter chain
* will then be used for the next XZ Block.
*
* @param filterOptions
* new filter chain to use
*
* @throws UnsupportedOptionsException
* unsupported filter chain, or trying to change
* the filter chain in the middle of a Block
*/
public void updateFilters(FilterOptions[] filterOptions)
throws XZIOException {
if (blockEncoder != null)
throw new UnsupportedOptionsException("Changing filter options "
+ "in the middle of a XZ Block not implemented");
if (filterOptions.length < 1 || filterOptions.length > 4)
throw new UnsupportedOptionsException(
"XZ filter chain must be 1-4 filters");
filtersSupportFlushing = true;
FilterEncoder[] newFilters = new FilterEncoder[filterOptions.length];
for (int i = 0; i < filterOptions.length; ++i) {
newFilters[i] = filterOptions[i].getFilterEncoder();
filtersSupportFlushing &= newFilters[i].supportsFlushing();
}
RawCoder.validate(newFilters);
filters = newFilters;
}
/**
* Writes one byte to be compressed.
*
* @throws XZIOException
* XZ Stream has grown too big
*
* @throws XZIOException
* finish()
or close()
* was already called
*
* @throws IOException may be thrown by the underlying output stream
*/
public void write(int b) throws IOException {
tempBuf[0] = (byte)b;
write(tempBuf, 0, 1);
}
/**
* Writes an array of bytes to be compressed.
* The compressors tend to do internal buffering and thus the written
* data won't be readable from the compressed output immediately.
* Use flush()
to force everything written so far to
* be written to the underlaying output stream, but be aware that
* flushing reduces compression ratio.
*
* @param buf buffer of bytes to be written
* @param off start offset in buf
* @param len number of bytes to write
*
* @throws XZIOException
* XZ Stream has grown too big: total file size
* about 8 EiB or the Index field exceeds
* 16 GiB; you shouldn't reach these sizes
* in practice
*
* @throws XZIOException
* finish()
or close()
* was already called and len > 0
*
* @throws IOException may be thrown by the underlying output stream
*/
public void write(byte[] buf, int off, int len) throws IOException {
if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length)
throw new IndexOutOfBoundsException();
if (exception != null)
throw exception;
if (finished)
throw new XZIOException("Stream finished or closed");
try {
if (blockEncoder == null)
blockEncoder = new BlockOutputStream(out, filters, check);
blockEncoder.write(buf, off, len);
} catch (IOException e) {
exception = e;
throw e;
}
}
/**
* Finishes the current XZ Block (but not the whole XZ Stream).
* This doesn't flush the stream so it's possible that not all data will
* be decompressible from the output stream when this function returns.
* Call also flush()
if flushing is wanted in addition to
* finishing the current XZ Block.
*
* If there is no unfinished Block open, this function will do nothing.
* (No empty XZ Block will be created.)
*
* This function can be useful, for example, to create
* random-accessible .xz files.
*
* Starting a new XZ Block means that the encoder state is reset.
* Doing this very often will increase the size of the compressed
* file a lot (more than plain flush()
would do).
*
* @throws XZIOException
* XZ Stream has grown too big
*
* @throws XZIOException
* stream finished or closed
*
* @throws IOException may be thrown by the underlying output stream
*/
public void endBlock() throws IOException {
if (exception != null)
throw exception;
if (finished)
throw new XZIOException("Stream finished or closed");
// NOTE: Once there is threading with multiple Blocks, it's possible
// that this function will be more like a barrier that returns
// before the last Block has been finished.
if (blockEncoder != null) {
try {
blockEncoder.finish();
index.add(blockEncoder.getUnpaddedSize(),
blockEncoder.getUncompressedSize());
blockEncoder = null;
} catch (IOException e) {
exception = e;
throw e;
}
}
}
/**
* Flushes the encoder and calls out.flush()
.
* All buffered pending data will then be decompressible from
* the output stream.
*
* Calling this function very often may increase the compressed
* file size a lot. The filter chain options may affect the size
* increase too. For example, with LZMA2 the HC4 match finder has
* smaller penalty with flushing than BT4.
*
* Some filters don't support flushing. If the filter chain has
* such a filter, flush()
will call endBlock()
* before flushing.
*
* @throws XZIOException
* XZ Stream has grown too big
*
* @throws XZIOException
* stream finished or closed
*
* @throws IOException may be thrown by the underlying output stream
*/
public void flush() throws IOException {
if (exception != null)
throw exception;
if (finished)
throw new XZIOException("Stream finished or closed");
try {
if (blockEncoder != null) {
if (filtersSupportFlushing) {
// This will eventually call out.flush() so
// no need to do it here again.
blockEncoder.flush();
} else {
endBlock();
out.flush();
}
} else {
out.flush();
}
} catch (IOException e) {
exception = e;
throw e;
}
}
/**
* Finishes compression without closing the underlying stream.
* No more data can be written to this stream after finishing
* (calling write
with an empty buffer is OK).
*
* Repeated calls to finish()
do nothing unless
* an exception was thrown by this stream earlier. In that case
* the same exception is thrown again.
*
* After finishing, the stream may be closed normally with
* close()
. If the stream will be closed anyway, there
* usually is no need to call finish()
separately.
*
* @throws XZIOException
* XZ Stream has grown too big
*
* @throws IOException may be thrown by the underlying output stream
*/
public void finish() throws IOException {
if (!finished) {
// This checks for pending exceptions so we don't need to
// worry about it here.
endBlock();
try {
index.encode(out);
encodeStreamFooter();
} catch (IOException e) {
exception = e;
throw e;
}
// Set it to true only if everything goes fine. Setting it earlier
// would cause repeated calls to finish() do nothing instead of
// throwing an exception to indicate an earlier error.
finished = true;
}
}
/**
* Finishes compression and closes the underlying stream.
* The underlying stream out
is closed even if finishing
* fails. If both finishing and closing fail, the exception thrown
* by finish()
is thrown and the exception from the failed
* out.close()
is lost.
*
* @throws XZIOException
* XZ Stream has grown too big
*
* @throws IOException may be thrown by the underlying output stream
*/
public void close() throws IOException {
if (out != null) {
// If finish() throws an exception, it stores the exception to
// the variable "exception". So we can ignore the possible
// exception here.
try {
finish();
} catch (IOException e) {}
try {
out.close();
} catch (IOException e) {
// Remember the exception but only if there is no previous
// pending exception.
if (exception == null)
exception = e;
}
out = null;
}
if (exception != null)
throw exception;
}
private void encodeStreamFlags(byte[] buf, int off) {
buf[off] = 0x00;
buf[off + 1] = (byte)streamFlags.checkType;
}
private void encodeStreamHeader() throws IOException {
out.write(XZ.HEADER_MAGIC);
byte[] buf = new byte[2];
encodeStreamFlags(buf, 0);
out.write(buf);
EncoderUtil.writeCRC32(out, buf);
}
private void encodeStreamFooter() throws IOException {
byte[] buf = new byte[6];
long backwardSize = index.getIndexSize() / 4 - 1;
for (int i = 0; i < 4; ++i)
buf[i] = (byte)(backwardSize >>> (i * 8));
encodeStreamFlags(buf, 4);
EncoderUtil.writeCRC32(out, buf);
out.write(buf);
out.write(XZ.FOOTER_MAGIC);
}
}