Home | History | Annotate | Download | only in xz
      1 /*
      2  * BlockInputStream
      3  *
      4  * Author: Lasse Collin <lasse.collin (at) tukaani.org>
      5  *
      6  * This file has been put into the public domain.
      7  * You can do whatever you want with this file.
      8  */
      9 
     10 package org.tukaani.xz;
     11 
     12 import java.io.InputStream;
     13 import java.io.DataInputStream;
     14 import java.io.ByteArrayInputStream;
     15 import java.io.IOException;
     16 import java.util.Arrays;
     17 import org.tukaani.xz.common.DecoderUtil;
     18 import org.tukaani.xz.check.Check;
     19 
     20 class BlockInputStream extends InputStream {
     21     private final DataInputStream inData;
     22     private final CountingInputStream inCounted;
     23     private InputStream filterChain;
     24     private final Check check;
     25     private final boolean verifyCheck;
     26 
     27     private long uncompressedSizeInHeader = -1;
     28     private long compressedSizeInHeader = -1;
     29     private long compressedSizeLimit;
     30     private final int headerSize;
     31     private long uncompressedSize = 0;
     32     private boolean endReached = false;
     33 
     34     private final byte[] tempBuf = new byte[1];
     35 
     36     public BlockInputStream(InputStream in,
     37                             Check check, boolean verifyCheck,
     38                             int memoryLimit,
     39                             long unpaddedSizeInIndex,
     40                             long uncompressedSizeInIndex)
     41             throws IOException, IndexIndicatorException {
     42         this.check = check;
     43         this.verifyCheck = verifyCheck;
     44         inData = new DataInputStream(in);
     45 
     46         byte[] buf = new byte[DecoderUtil.BLOCK_HEADER_SIZE_MAX];
     47 
     48         // Block Header Size or Index Indicator
     49         inData.readFully(buf, 0, 1);
     50 
     51         // See if this begins the Index field.
     52         if (buf[0] == 0x00)
     53             throw new IndexIndicatorException();
     54 
     55         // Read the rest of the Block Header.
     56         headerSize = 4 * ((buf[0] & 0xFF) + 1);
     57         inData.readFully(buf, 1, headerSize - 1);
     58 
     59         // Validate the CRC32.
     60         if (!DecoderUtil.isCRC32Valid(buf, 0, headerSize - 4, headerSize - 4))
     61             throw new CorruptedInputException("XZ Block Header is corrupt");
     62 
     63         // Check for reserved bits in Block Flags.
     64         if ((buf[1] & 0x3C) != 0)
     65             throw new UnsupportedOptionsException(
     66                     "Unsupported options in XZ Block Header");
     67 
     68         // Memory for the Filter Flags field
     69         int filterCount = (buf[1] & 0x03) + 1;
     70         long[] filterIDs = new long[filterCount];
     71         byte[][] filterProps = new byte[filterCount][];
     72 
     73         // Use a stream to parse the fields after the Block Flags field.
     74         // Exclude the CRC32 field at the end.
     75         ByteArrayInputStream bufStream = new ByteArrayInputStream(
     76                 buf, 2, headerSize - 6);
     77 
     78         try {
     79             // Set the maximum valid compressed size. This is overriden
     80             // by the value from the Compressed Size field if it is present.
     81             compressedSizeLimit = (DecoderUtil.VLI_MAX & ~3)
     82                                   - headerSize - check.getSize();
     83 
     84             // Decode and validate Compressed Size if the relevant flag
     85             // is set in Block Flags.
     86             if ((buf[1] & 0x40) != 0x00) {
     87                 compressedSizeInHeader = DecoderUtil.decodeVLI(bufStream);
     88 
     89                 if (compressedSizeInHeader == 0
     90                         || compressedSizeInHeader > compressedSizeLimit)
     91                     throw new CorruptedInputException();
     92 
     93                 compressedSizeLimit = compressedSizeInHeader;
     94             }
     95 
     96             // Decode Uncompressed Size if the relevant flag is set
     97             // in Block Flags.
     98             if ((buf[1] & 0x80) != 0x00)
     99                 uncompressedSizeInHeader = DecoderUtil.decodeVLI(bufStream);
    100 
    101             // Decode Filter Flags.
    102             for (int i = 0; i < filterCount; ++i) {
    103                 filterIDs[i] = DecoderUtil.decodeVLI(bufStream);
    104 
    105                 long filterPropsSize = DecoderUtil.decodeVLI(bufStream);
    106                 if (filterPropsSize > bufStream.available())
    107                     throw new CorruptedInputException();
    108 
    109                 filterProps[i] = new byte[(int)filterPropsSize];
    110                 bufStream.read(filterProps[i]);
    111             }
    112 
    113         } catch (IOException e) {
    114             throw new CorruptedInputException("XZ Block Header is corrupt");
    115         }
    116 
    117         // Check that the remaining bytes are zero.
    118         for (int i = bufStream.available(); i > 0; --i)
    119             if (bufStream.read() != 0x00)
    120                 throw new UnsupportedOptionsException(
    121                         "Unsupported options in XZ Block Header");
    122 
    123         // Validate the Blcok Header against the Index when doing
    124         // random access reading.
    125         if (unpaddedSizeInIndex != -1) {
    126             // Compressed Data must be at least one byte, so if Block Header
    127             // and Check alone take as much or more space than the size
    128             // stored in the Index, the file is corrupt.
    129             int headerAndCheckSize = headerSize + check.getSize();
    130             if (headerAndCheckSize >= unpaddedSizeInIndex)
    131                 throw new CorruptedInputException(
    132                         "XZ Index does not match a Block Header");
    133 
    134             // The compressed size calculated from Unpadded Size must
    135             // match the value stored in the Compressed Size field in
    136             // the Block Header.
    137             long compressedSizeFromIndex
    138                     = unpaddedSizeInIndex - headerAndCheckSize;
    139             if (compressedSizeFromIndex > compressedSizeLimit
    140                     || (compressedSizeInHeader != -1
    141                         && compressedSizeInHeader != compressedSizeFromIndex))
    142                 throw new CorruptedInputException(
    143                         "XZ Index does not match a Block Header");
    144 
    145             // The uncompressed size stored in the Index must match
    146             // the value stored in the Uncompressed Size field in
    147             // the Block Header.
    148             if (uncompressedSizeInHeader != -1
    149                     && uncompressedSizeInHeader != uncompressedSizeInIndex)
    150                 throw new CorruptedInputException(
    151                         "XZ Index does not match a Block Header");
    152 
    153             // For further validation, pretend that the values from the Index
    154             // were stored in the Block Header.
    155             compressedSizeLimit = compressedSizeFromIndex;
    156             compressedSizeInHeader = compressedSizeFromIndex;
    157             uncompressedSizeInHeader = uncompressedSizeInIndex;
    158         }
    159 
    160         // Check if the Filter IDs are supported, decode
    161         // the Filter Properties, and check that they are
    162         // supported by this decoder implementation.
    163         FilterDecoder[] filters = new FilterDecoder[filterIDs.length];
    164 
    165         for (int i = 0; i < filters.length; ++i) {
    166             if (filterIDs[i] == LZMA2Coder.FILTER_ID)
    167                 filters[i] = new LZMA2Decoder(filterProps[i]);
    168 
    169             else if (filterIDs[i] == DeltaCoder.FILTER_ID)
    170                 filters[i] = new DeltaDecoder(filterProps[i]);
    171 
    172             else if (BCJDecoder.isBCJFilterID(filterIDs[i]))
    173                 filters[i] = new BCJDecoder(filterIDs[i], filterProps[i]);
    174 
    175             else
    176                 throw new UnsupportedOptionsException(
    177                         "Unknown Filter ID " + filterIDs[i]);
    178         }
    179 
    180         RawCoder.validate(filters);
    181 
    182         // Check the memory usage limit.
    183         if (memoryLimit >= 0) {
    184             int memoryNeeded = 0;
    185             for (int i = 0; i < filters.length; ++i)
    186                 memoryNeeded += filters[i].getMemoryUsage();
    187 
    188             if (memoryNeeded > memoryLimit)
    189                 throw new MemoryLimitException(memoryNeeded, memoryLimit);
    190         }
    191 
    192         // Use an input size counter to calculate
    193         // the size of the Compressed Data field.
    194         inCounted = new CountingInputStream(in);
    195 
    196         // Initialize the filter chain.
    197         filterChain = inCounted;
    198         for (int i = filters.length - 1; i >= 0; --i)
    199             filterChain = filters[i].getInputStream(filterChain);
    200     }
    201 
    202     public int read() throws IOException {
    203         return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF);
    204     }
    205 
    206     public int read(byte[] buf, int off, int len) throws IOException {
    207         if (endReached)
    208             return -1;
    209 
    210         int ret = filterChain.read(buf, off, len);
    211 
    212         if (ret > 0) {
    213             if (verifyCheck)
    214                 check.update(buf, off, ret);
    215 
    216             uncompressedSize += ret;
    217 
    218             // Catch invalid values.
    219             long compressedSize = inCounted.getSize();
    220             if (compressedSize < 0
    221                     || compressedSize > compressedSizeLimit
    222                     || uncompressedSize < 0
    223                     || (uncompressedSizeInHeader != -1
    224                         && uncompressedSize > uncompressedSizeInHeader))
    225                 throw new CorruptedInputException();
    226 
    227             // Check the Block integrity as soon as possible:
    228             //   - The filter chain shouldn't return less than requested
    229             //     unless it hit the end of the input.
    230             //   - If the uncompressed size is known, we know when there
    231             //     shouldn't be more data coming. We still need to read
    232             //     one byte to let the filter chain catch errors and to
    233             //     let it read end of payload marker(s).
    234             if (ret < len || uncompressedSize == uncompressedSizeInHeader) {
    235                 if (filterChain.read() != -1)
    236                     throw new CorruptedInputException();
    237 
    238                 validate();
    239                 endReached = true;
    240             }
    241         } else if (ret == -1) {
    242             validate();
    243             endReached = true;
    244         }
    245 
    246         return ret;
    247     }
    248 
    249     private void validate() throws IOException {
    250         long compressedSize = inCounted.getSize();
    251 
    252         // Validate Compressed Size and Uncompressed Size if they were
    253         // present in Block Header.
    254         if ((compressedSizeInHeader != -1
    255                     && compressedSizeInHeader != compressedSize)
    256                 || (uncompressedSizeInHeader != -1
    257                     && uncompressedSizeInHeader != uncompressedSize))
    258             throw new CorruptedInputException();
    259 
    260         // Block Padding bytes must be zeros.
    261         while ((compressedSize++ & 3) != 0)
    262             if (inData.readUnsignedByte() != 0x00)
    263                 throw new CorruptedInputException();
    264 
    265         // Validate the integrity check if verifyCheck is true.
    266         byte[] storedCheck = new byte[check.getSize()];
    267         inData.readFully(storedCheck);
    268         if (verifyCheck && !Arrays.equals(check.finish(), storedCheck))
    269             throw new CorruptedInputException("Integrity check ("
    270                     + check.getName() + ") does not match");
    271     }
    272 
    273     public int available() throws IOException {
    274         return filterChain.available();
    275     }
    276 
    277     public long getUnpaddedSize() {
    278         return headerSize + inCounted.getSize() + check.getSize();
    279     }
    280 
    281     public long getUncompressedSize() {
    282         return uncompressedSize;
    283     }
    284 }
    285