279 lines
10 KiB
Java
279 lines
10 KiB
Java
|
|
/*
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|