/* * BlockInputStream * * 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.ByteArrayInputStream; import java.io.IOException; import java.util.Arrays; import org.tukaani.xz.common.DecoderUtil; import org.tukaani.xz.check.Check; class BlockInputStream extends InputStream { private final DataInputStream inData; private final CountingInputStream inCounted; private InputStream filterChain; private final Check check; private long uncompressedSizeInHeader = -1; private long compressedSizeInHeader = -1; private long compressedSizeLimit; private final int headerSize; private long uncompressedSize = 0; private boolean endReached = false; private final byte[] tempBuf = new byte[1]; public BlockInputStream(InputStream in, Check check, int memoryLimit, long unpaddedSizeInIndex, long uncompressedSizeInIndex) throws IOException, IndexIndicatorException { this.check = check; inData = new DataInputStream(in); byte[] buf = new byte[DecoderUtil.BLOCK_HEADER_SIZE_MAX]; // Block Header Size or Index Indicator inData.readFully(buf, 0, 1); // See if this begins the Index field. if (buf[0] == 0x00) throw new IndexIndicatorException(); // Read the rest of the Block Header. headerSize = 4 * ((buf[0] & 0xFF) + 1); inData.readFully(buf, 1, headerSize - 1); // Validate the CRC32. if (!DecoderUtil.isCRC32Valid(buf, 0, headerSize - 4, headerSize - 4)) throw new CorruptedInputException("XZ Block Header is corrupt"); // Check for reserved bits in Block Flags. if ((buf[1] & 0x3C) != 0) throw new UnsupportedOptionsException( "Unsupported options in XZ Block Header"); // Memory for the Filter Flags field int filterCount = (buf[1] & 0x03) + 1; long[] filterIDs = new long[filterCount]; byte[][] filterProps = new byte[filterCount][]; // Use a stream to parse the fields after the Block Flags field. // Exclude the CRC32 field at the end. ByteArrayInputStream bufStream = new ByteArrayInputStream( buf, 2, headerSize - 6); try { // Set the maximum valid compressed size. This is overriden // by the value from the Compressed Size field if it is present. compressedSizeLimit = (DecoderUtil.VLI_MAX & ~3) - headerSize - check.getSize(); // Decode and validate Compressed Size if the relevant flag // is set in Block Flags. if ((buf[1] & 0x40) != 0x00) { compressedSizeInHeader = DecoderUtil.decodeVLI(bufStream); if (compressedSizeInHeader == 0 || compressedSizeInHeader > compressedSizeLimit) throw new CorruptedInputException(); compressedSizeLimit = compressedSizeInHeader; } // Decode Uncompressed Size if the relevant flag is set // in Block Flags. if ((buf[1] & 0x80) != 0x00) uncompressedSizeInHeader = DecoderUtil.decodeVLI(bufStream); // Decode Filter Flags. for (int i = 0; i < filterCount; ++i) { filterIDs[i] = DecoderUtil.decodeVLI(bufStream); long filterPropsSize = DecoderUtil.decodeVLI(bufStream); if (filterPropsSize > bufStream.available()) throw new CorruptedInputException(); filterProps[i] = new byte[(int)filterPropsSize]; bufStream.read(filterProps[i]); } } catch (IOException e) { throw new CorruptedInputException("XZ Block Header is corrupt"); } // Check that the remaining bytes are zero. for (int i = bufStream.available(); i > 0; --i) if (bufStream.read() != 0x00) throw new UnsupportedOptionsException( "Unsupported options in XZ Block Header"); // Validate the Blcok Header against the Index when doing // random access reading. if (unpaddedSizeInIndex != -1) { // Compressed Data must be at least one byte, so if Block Header // and Check alone take as much or more space than the size // stored in the Index, the file is corrupt. int headerAndCheckSize = headerSize + check.getSize(); if (headerAndCheckSize >= unpaddedSizeInIndex) throw new CorruptedInputException( "XZ Index does not match a Block Header"); // The compressed size calculated from Unpadded Size must // match the value stored in the Compressed Size field in // the Block Header. long compressedSizeFromIndex = unpaddedSizeInIndex - headerAndCheckSize; if (compressedSizeFromIndex > compressedSizeLimit || (compressedSizeInHeader != -1 && compressedSizeInHeader != compressedSizeFromIndex)) throw new CorruptedInputException( "XZ Index does not match a Block Header"); // The uncompressed size stored in the Index must match // the value stored in the Uncompressed Size field in // the Block Header. if (uncompressedSizeInHeader != -1 && uncompressedSizeInHeader != uncompressedSizeInIndex) throw new CorruptedInputException( "XZ Index does not match a Block Header"); // For further validation, pretend that the values from the Index // were stored in the Block Header. compressedSizeLimit = compressedSizeFromIndex; compressedSizeInHeader = compressedSizeFromIndex; uncompressedSizeInHeader = uncompressedSizeInIndex; } // Check if the Filter IDs are supported, decode // the Filter Properties, and check that they are // supported by this decoder implementation. FilterDecoder[] filters = new FilterDecoder[filterIDs.length]; for (int i = 0; i < filters.length; ++i) { if (filterIDs[i] == LZMA2Coder.FILTER_ID) filters[i] = new LZMA2Decoder(filterProps[i]); else if (filterIDs[i] == DeltaCoder.FILTER_ID) filters[i] = new DeltaDecoder(filterProps[i]); else if (BCJDecoder.isBCJFilterID(filterIDs[i])) filters[i] = new BCJDecoder(filterIDs[i], filterProps[i]); else throw new UnsupportedOptionsException( "Unknown Filter ID " + filterIDs[i]); } RawCoder.validate(filters); // Check the memory usage limit. if (memoryLimit >= 0) { int memoryNeeded = 0; for (int i = 0; i < filters.length; ++i) memoryNeeded += filters[i].getMemoryUsage(); if (memoryNeeded > memoryLimit) throw new MemoryLimitException(memoryNeeded, memoryLimit); } // Use an input size counter to calculate // the size of the Compressed Data field. inCounted = new CountingInputStream(in); // Initialize the filter chain. filterChain = inCounted; for (int i = filters.length - 1; i >= 0; --i) filterChain = filters[i].getInputStream(filterChain); } public int read() throws IOException { return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF); } public int read(byte[] buf, int off, int len) throws IOException { if (endReached) return -1; int ret = filterChain.read(buf, off, len); if (ret > 0) { check.update(buf, off, ret); uncompressedSize += ret; // Catch invalid values. long compressedSize = inCounted.getSize(); if (compressedSize < 0 || compressedSize > compressedSizeLimit || uncompressedSize < 0 || (uncompressedSizeInHeader != -1 && uncompressedSize > uncompressedSizeInHeader)) throw new CorruptedInputException(); // Check the Block integrity as soon as possible: // - The filter chain shouldn't return less than requested // unless it hit the end of the input. // - If the uncompressed size is known, we know when there // shouldn't be more data coming. We still need to read // one byte to let the filter chain catch errors and to // let it read end of payload marker(s). if (ret < len || uncompressedSize == uncompressedSizeInHeader) { if (filterChain.read() != -1) throw new CorruptedInputException(); validate(); endReached = true; } } else if (ret == -1) { validate(); endReached = true; } return ret; } private void validate() throws IOException { long compressedSize = inCounted.getSize(); // Validate Compressed Size and Uncompressed Size if they were // present in Block Header. if ((compressedSizeInHeader != -1 && compressedSizeInHeader != compressedSize) || (uncompressedSizeInHeader != -1 && uncompressedSizeInHeader != uncompressedSize)) throw new CorruptedInputException(); // Block Padding bytes must be zeros. while ((compressedSize++ & 3) != 0) if (inData.readUnsignedByte() != 0x00) throw new CorruptedInputException(); // Validate the integrity check. byte[] storedCheck = new byte[check.getSize()]; inData.readFully(storedCheck); if (!Arrays.equals(check.finish(), storedCheck)) throw new CorruptedInputException("Integrity check (" + check.getName() + ") does not match"); } public int available() throws IOException { return filterChain.available(); } public long getUnpaddedSize() { return headerSize + inCounted.getSize() + check.getSize(); } public long getUncompressedSize() { return uncompressedSize; } }