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