1 /* 2 * BlockInputStream 3 * 4 * Author: Lasse Collin <lasse.collin@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 BlockInputStream(InputStream in, Check check, boolean verifyCheck, int memoryLimit, long unpaddedSizeInIndex, long uncompressedSizeInIndex)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 read()202 public int read() throws IOException { 203 return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF); 204 } 205 read(byte[] buf, int off, int len)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 validate()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 available()273 public int available() throws IOException { 274 return filterChain.available(); 275 } 276 getUnpaddedSize()277 public long getUnpaddedSize() { 278 return headerSize + inCounted.getSize() + check.getSize(); 279 } 280 getUncompressedSize()281 public long getUncompressedSize() { 282 return uncompressedSize; 283 } 284 } 285