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