/* * SingleXZInputStream * * Author: Lasse Collin <lasse.collin@tukaani.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.InputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.EOFException; import org.tukaani.xz.common.DecoderUtil; import org.tukaani.xz.common.StreamFlags; import org.tukaani.xz.index.IndexHash; import org.tukaani.xz.check.Check; /** * Decompresses exactly one XZ Stream in streamed mode (no seeking). * The decompression stops after the first XZ Stream has been decompressed, * and the read position in the input stream is left at the first byte * after the end of the XZ Stream. This can be useful when XZ data has * been stored inside some other file format or protocol. * <p> * Unless you know what you are doing, don't use this class to decompress * standalone .xz files. For that purpose, use <code>XZInputStream</code>. * * <h4>When uncompressed size is known beforehand</h4> * <p> * If you are decompressing complete XZ streams and your application knows * exactly how much uncompressed data there should be, it is good to try * reading one more byte by calling <code>read()</code> and checking * that it returns <code>-1</code>. This way the decompressor will parse the * file footers and verify the integrity checks, giving the caller more * confidence that the uncompressed data is valid. * * @see XZInputStream */ public class SingleXZInputStream extends InputStream { private InputStream in; private int memoryLimit; private StreamFlags streamHeaderFlags; private Check check; private BlockInputStream blockDecoder = null; private final IndexHash indexHash = new IndexHash(); private boolean endReached = false; private IOException exception = null; private final byte[] tempBuf = new byte[1]; /** * Creates a new XZ decompressor that decompresses exactly one * XZ Stream from <code>in</code> without a memory usage limit. * <p> * This constructor reads and parses the XZ Stream Header (12 bytes) * from <code>in</code>. The header of the first Block is not read * until <code>read</code> is called. * * @param in input stream from which XZ-compressed * data is read * * @throws XZFormatException * input is not in the XZ format * * @throws CorruptedInputException * XZ header CRC32 doesn't match * * @throws UnsupportedOptionsException * XZ header is valid but specifies options * not supported by this implementation * * @throws EOFException * less than 12 bytes of input was available * from <code>in</code> * * @throws IOException may be thrown by <code>in</code> */ public SingleXZInputStream(InputStream in) throws IOException { initialize(in, -1); } /** * Creates a new XZ decompressor that decompresses exactly one * XZ Stream from <code>in</code> with an optional memory usage limit. * <p> * This is identical to <code>SingleXZInputStream(InputStream)</code> * except that this takes also the <code>memoryLimit</code> argument. * * @param in input stream from which XZ-compressed * data is read * * @param memoryLimit memory usage limit in kibibytes (KiB) * or <code>-1</code> to impose no * memory usage limit * * @throws XZFormatException * input is not in the XZ format * * @throws CorruptedInputException * XZ header CRC32 doesn't match * * @throws UnsupportedOptionsException * XZ header is valid but specifies options * not supported by this implementation * * @throws EOFException * less than 12 bytes of input was available * from <code>in</code> * * @throws IOException may be thrown by <code>in</code> */ public SingleXZInputStream(InputStream in, int memoryLimit) throws IOException { initialize(in, memoryLimit); } SingleXZInputStream(InputStream in, int memoryLimit, byte[] streamHeader) throws IOException { initialize(in, memoryLimit, streamHeader); } private void initialize(InputStream in, int memoryLimit) throws IOException { byte[] streamHeader = new byte[DecoderUtil.STREAM_HEADER_SIZE]; new DataInputStream(in).readFully(streamHeader); initialize(in, memoryLimit, streamHeader); } private void initialize(InputStream in, int memoryLimit, byte[] streamHeader) throws IOException { this.in = in; this.memoryLimit = memoryLimit; streamHeaderFlags = DecoderUtil.decodeStreamHeader(streamHeader); check = Check.getInstance(streamHeaderFlags.checkType); } /** * Gets the ID of the integrity check used in this XZ Stream. * * @return the Check ID specified in the XZ Stream Header */ public int getCheckType() { return streamHeaderFlags.checkType; } /** * Gets the name of the integrity check used in this XZ Stream. * * @return the name of the check specified in the XZ Stream Header */ public String getCheckName() { return check.getName(); } /** * Decompresses the next byte from this input stream. * <p> * Reading lots of data with <code>read()</code> from this input stream * may be inefficient. Wrap it in {@link java.io.BufferedInputStream} * if you need to read lots of data one byte at a time. * * @return the next decompressed byte, or <code>-1</code> * to indicate the end of the compressed stream * * @throws CorruptedInputException * @throws UnsupportedOptionsException * @throws MemoryLimitException * * @throws XZIOException if the stream has been closed * * @throws EOFException * compressed input is truncated or corrupt * * @throws IOException may be thrown by <code>in</code> */ public int read() throws IOException { return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF); } /** * Decompresses into an array of bytes. * <p> * If <code>len</code> is zero, no bytes are read and <code>0</code> * is returned. Otherwise this will try to decompress <code>len</code> * bytes of uncompressed data. Less than <code>len</code> bytes may * be read only in the following situations: * <ul> * <li>The end of the compressed data was reached successfully.</li> * <li>An error is detected after at least one but less <code>len</code> * bytes have already been successfully decompressed. * The next call with non-zero <code>len</code> will immediately * throw the pending exception.</li> * <li>An exception is thrown.</li> * </ul> * * @param buf target buffer for uncompressed data * @param off start offset in <code>buf</code> * @param len maximum number of uncompressed bytes to read * * @return number of bytes read, or <code>-1</code> to indicate * the end of the compressed stream * * @throws CorruptedInputException * @throws UnsupportedOptionsException * @throws MemoryLimitException * * @throws XZIOException if the stream has been closed * * @throws EOFException * compressed input is truncated or corrupt * * @throws IOException may be thrown by <code>in</code> */ public int read(byte[] buf, int off, int len) throws IOException { if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) throw new IndexOutOfBoundsException(); if (len == 0) return 0; if (in == null) throw new XZIOException("Stream closed"); if (exception != null) throw exception; if (endReached) return -1; int size = 0; try { while (len > 0) { if (blockDecoder == null) { try { blockDecoder = new BlockInputStream( in, check, memoryLimit, -1, -1); } catch (IndexIndicatorException e) { indexHash.validate(in); validateStreamFooter(); endReached = true; return size > 0 ? size : -1; } } int ret = blockDecoder.read(buf, off, len); if (ret > 0) { size += ret; off += ret; len -= ret; } else if (ret == -1) { indexHash.add(blockDecoder.getUnpaddedSize(), blockDecoder.getUncompressedSize()); blockDecoder = null; } } } catch (IOException e) { exception = e; if (size == 0) throw e; } return size; } private void validateStreamFooter() throws IOException { byte[] buf = new byte[DecoderUtil.STREAM_HEADER_SIZE]; new DataInputStream(in).readFully(buf); StreamFlags streamFooterFlags = DecoderUtil.decodeStreamFooter(buf); if (!DecoderUtil.areStreamFlagsEqual(streamHeaderFlags, streamFooterFlags) || indexHash.getIndexSize() != streamFooterFlags.backwardSize) throw new CorruptedInputException( "XZ Stream Footer does not match Stream Header"); } /** * Returns the number of uncompressed bytes that can be read * without blocking. The value is returned with an assumption * that the compressed input data will be valid. If the compressed * data is corrupt, <code>CorruptedInputException</code> may get * thrown before the number of bytes claimed to be available have * been read from this input stream. * * @return the number of uncompressed bytes that can be read * without blocking */ public int available() throws IOException { if (in == null) throw new XZIOException("Stream closed"); if (exception != null) throw exception; return blockDecoder == null ? 0 : blockDecoder.available(); } /** * Closes the stream and calls <code>in.close()</code>. * If the stream was already closed, this does nothing. * * @throws IOException if thrown by <code>in.close()</code> */ public void close() throws IOException { if (in != null) { try { in.close(); } finally { in = null; } } } }