blob: 3a1b7b134c4a6a9b5fe03036eec5444cb60b6323 [file] [log] [blame]
/*
* LZMAOutputStream
*
* Authors: Lasse Collin <lasse.collin@tukaani.org>
* Igor Pavlov <http://7-zip.org/>
*
* 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.lz.LZEncoder;
import org.tukaani.xz.rangecoder.RangeEncoderToStream;
import org.tukaani.xz.lzma.LZMAEncoder;
/**
* Compresses into the legacy .lzma file format or into a raw LZMA stream.
*
* @since 1.6
*/
public class LZMAOutputStream extends FinishableOutputStream {
private OutputStream out;
private final ArrayCache arrayCache;
private LZEncoder lz;
private final RangeEncoderToStream rc;
private LZMAEncoder lzma;
private final int props;
private final boolean useEndMarker;
private final long expectedUncompressedSize;
private long currentUncompressedSize = 0;
private boolean finished = false;
private IOException exception = null;
private final byte[] tempBuf = new byte[1];
private LZMAOutputStream(OutputStream out, LZMA2Options options,
boolean useHeader, boolean useEndMarker,
long expectedUncompressedSize,
ArrayCache arrayCache)
throws IOException {
if (out == null)
throw new NullPointerException();
// -1 indicates unknown and >= 0 are for known sizes.
if (expectedUncompressedSize < -1)
throw new IllegalArgumentException(
"Invalid expected input size (less than -1)");
this.useEndMarker = useEndMarker;
this.expectedUncompressedSize = expectedUncompressedSize;
this.arrayCache = arrayCache;
this.out = out;
rc = new RangeEncoderToStream(out);
int dictSize = options.getDictSize();
lzma = LZMAEncoder.getInstance(rc,
options.getLc(), options.getLp(), options.getPb(),
options.getMode(),
dictSize, 0, options.getNiceLen(),
options.getMatchFinder(), options.getDepthLimit(),
arrayCache);
lz = lzma.getLZEncoder();
byte[] presetDict = options.getPresetDict();
if (presetDict != null && presetDict.length > 0) {
if (useHeader)
throw new UnsupportedOptionsException(
"Preset dictionary cannot be used in .lzma files "
+ "(try a raw LZMA stream instead)");
lz.setPresetDict(dictSize, presetDict);
}
props = (options.getPb() * 5 + options.getLp()) * 9 + options.getLc();
if (useHeader) {
// Props byte stores lc, lp, and pb.
out.write(props);
// Dictionary size is stored as a 32-bit unsigned little endian
// integer.
for (int i = 0; i < 4; ++i) {
out.write(dictSize & 0xFF);
dictSize >>>= 8;
}
// Uncompressed size is stored as a 64-bit unsigned little endian
// integer. The max value (-1 in two's complement) indicates
// unknown size.
for (int i = 0; i < 8; ++i)
out.write((int)(expectedUncompressedSize >>> (8 * i)) & 0xFF);
}
}
/**
* Creates a new compressor for the legacy .lzma file format.
* <p>
* If the uncompressed size of the input data is known, it will be stored
* in the .lzma header and no end of stream marker will be used. Otherwise
* the header will indicate unknown uncompressed size and the end of stream
* marker will be used.
* <p>
* Note that a preset dictionary cannot be used in .lzma files but
* it can be used for raw LZMA streams.
*
* @param out output stream to which the compressed data
* will be written
*
* @param options LZMA compression options; the same class
* is used here as is for LZMA2
*
* @param inputSize uncompressed size of the data to be compressed;
* use <code>-1</code> when unknown
*
* @throws IOException may be thrown from <code>out</code>
*/
public LZMAOutputStream(OutputStream out, LZMA2Options options,
long inputSize)
throws IOException {
this(out, options, inputSize, ArrayCache.getDefaultCache());
}
/**
* Creates a new compressor for the legacy .lzma file format.
* <p>
* This is identical to
* <code>LZMAOutputStream(OutputStream, LZMA2Options, long)</code>
* except that this also takes the <code>arrayCache</code> argument.
*
* @param out output stream to which the compressed data
* will be written
*
* @param options LZMA compression options; the same class
* is used here as is for LZMA2
*
* @param inputSize uncompressed size of the data to be compressed;
* use <code>-1</code> when unknown
*
* @param arrayCache cache to be used for allocating large arrays
*
* @throws IOException may be thrown from <code>out</code>
*
* @since 1.7
*/
public LZMAOutputStream(OutputStream out, LZMA2Options options,
long inputSize, ArrayCache arrayCache)
throws IOException {
this(out, options, true, inputSize == -1, inputSize, arrayCache);
}
/**
* Creates a new compressor for raw LZMA (also known as LZMA1) stream.
* <p>
* Raw LZMA streams can be encoded with or without end of stream marker.
* When decompressing the stream, one must know if the end marker was used
* and tell it to the decompressor. If the end marker wasn't used, the
* decompressor will also need to know the uncompressed size.
*
* @param out output stream to which the compressed data
* will be written
*
* @param options LZMA compression options; the same class
* is used here as is for LZMA2
*
* @param useEndMarker
* if end of stream marker should be written
*
* @throws IOException may be thrown from <code>out</code>
*/
public LZMAOutputStream(OutputStream out, LZMA2Options options,
boolean useEndMarker) throws IOException {
this(out, options, useEndMarker, ArrayCache.getDefaultCache());
}
/**
* Creates a new compressor for raw LZMA (also known as LZMA1) stream.
* <p>
* This is identical to
* <code>LZMAOutputStream(OutputStream, LZMA2Options, boolean)</code>
* except that this also takes the <code>arrayCache</code> argument.
*
* @param out output stream to which the compressed data
* will be written
*
* @param options LZMA compression options; the same class
* is used here as is for LZMA2
*
* @param useEndMarker
* if end of stream marker should be written
*
* @param arrayCache cache to be used for allocating large arrays
*
* @throws IOException may be thrown from <code>out</code>
*
* @since 1.7
*/
public LZMAOutputStream(OutputStream out, LZMA2Options options,
boolean useEndMarker, ArrayCache arrayCache)
throws IOException {
this(out, options, false, useEndMarker, -1, arrayCache);
}
/**
* Returns the LZMA lc/lp/pb properties encoded into a single byte.
* This might be useful when handling file formats other than .lzma
* that use the same encoding for the LZMA properties as .lzma does.
*/
public int getProps() {
return props;
}
/**
* Gets the amount of uncompressed data written to the stream.
* This is useful when creating raw LZMA streams without
* the end of stream marker.
*/
public long getUncompressedSize() {
return currentUncompressedSize;
}
public void write(int b) throws IOException {
tempBuf[0] = (byte)b;
write(tempBuf, 0, 1);
}
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");
if (expectedUncompressedSize != -1
&& expectedUncompressedSize - currentUncompressedSize < len)
throw new XZIOException("Expected uncompressed input size ("
+ expectedUncompressedSize + " bytes) was exceeded");
currentUncompressedSize += len;
try {
while (len > 0) {
int used = lz.fillWindow(buf, off, len);
off += used;
len -= used;
lzma.encodeForLZMA1();
}
} catch (IOException e) {
exception = e;
throw e;
}
}
/**
* Flushing isn't supported and will throw XZIOException.
*/
public void flush() throws IOException {
throw new XZIOException("LZMAOutputStream does not support flushing");
}
/**
* Finishes the stream without closing the underlying OutputStream.
*/
public void finish() throws IOException {
if (!finished) {
if (exception != null)
throw exception;
try {
if (expectedUncompressedSize != -1
&& expectedUncompressedSize != currentUncompressedSize)
throw new XZIOException("Expected uncompressed size ("
+ expectedUncompressedSize + ") doesn't equal "
+ "the number of bytes written to the stream ("
+ currentUncompressedSize + ")");
lz.setFinishing();
lzma.encodeForLZMA1();
if (useEndMarker)
lzma.encodeLZMA1EndMarker();
rc.finish();
} catch (IOException e) {
exception = e;
throw e;
}
finished = true;
lzma.putArraysToCache(arrayCache);
lzma = null;
lz = null;
}
}
/**
* Finishes the stream and closes the underlying OutputStream.
*/
public void close() throws IOException {
if (out != null) {
try {
finish();
} catch (IOException e) {}
try {
out.close();
} catch (IOException e) {
if (exception == null)
exception = e;
}
out = null;
}
if (exception != null)
throw exception;
}
}