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