View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.dump;
20  
21  import java.io.FilterInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.Arrays;
25  import java.util.zip.DataFormatException;
26  import java.util.zip.Inflater;
27  
28  import org.apache.commons.compress.utils.ExactMath;
29  import org.apache.commons.compress.utils.IOUtils;
30  
31  /**
32   * Filter stream that mimics a physical tape drive capable of compressing the data stream.
33   *
34   * @NotThreadSafe
35   */
36  final class TapeInputStream extends FilterInputStream {
37      private static final int RECORD_SIZE = DumpArchiveConstants.TP_SIZE;
38      private byte[] blockBuffer = new byte[DumpArchiveConstants.TP_SIZE];
39      private int currBlkIdx = -1;
40      private int blockSize = DumpArchiveConstants.TP_SIZE;
41      private int readOffset = DumpArchiveConstants.TP_SIZE;
42      private boolean isCompressed;
43      private long bytesRead;
44  
45      /**
46       * Constructs a new instance.
47       *
48       * @param in the underlying input stream.
49       */
50      TapeInputStream(final InputStream in) {
51          super(in);
52      }
53  
54      /**
55       * @see java.io.InputStream#available
56       */
57      @Override
58      public int available() throws IOException {
59          if (readOffset < blockSize) {
60              return blockSize - readOffset;
61          }
62  
63          return in.available();
64      }
65  
66      /**
67       * Close the input stream.
68       *
69       * @throws IOException on error
70       */
71      @Override
72      public void close() throws IOException {
73          if (in != null && in != System.in) {
74              in.close();
75          }
76      }
77  
78      /**
79       * Gets number of bytes read.
80       *
81       * @return number of bytes read.
82       */
83      public long getBytesRead() {
84          return bytesRead;
85      }
86  
87      /**
88       * Peek at the next record from the input stream and return the data.
89       *
90       * @return The record data.
91       * @throws IOException on error
92       */
93      public byte[] peek() throws IOException {
94          // we need to read from the underlying stream. This
95          // isn't a problem since it would be the first step in
96          // any subsequent read() anyway.
97          if (readOffset == blockSize) {
98              try {
99                  readBlock(true);
100             } catch (final ShortFileException sfe) { // NOSONAR
101                 return null;
102             }
103         }
104 
105         // copy data, increment counters.
106         final byte[] b = new byte[RECORD_SIZE];
107         System.arraycopy(blockBuffer, readOffset, b, 0, b.length);
108 
109         return b;
110     }
111 
112     /**
113      * @see java.io.InputStream#read()
114      */
115     @Override
116     public int read() throws IOException {
117         throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
118     }
119 
120     /**
121      * {@inheritDoc}
122      *
123      * <p>
124      * reads the full given length unless EOF is reached.
125      * </p>
126      *
127      * @param len length to read, must be a multiple of the stream's record size
128      */
129     @Override
130     public int read(final byte[] b, int off, final int len) throws IOException {
131         if (len == 0) {
132             return 0;
133         }
134         if (len % RECORD_SIZE != 0) {
135             throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
136         }
137 
138         int bytes = 0;
139 
140         while (bytes < len) {
141             // we need to read from the underlying stream.
142             // this will reset readOffset value.
143             // return -1 if there's a problem.
144             if (readOffset == blockSize) {
145                 try {
146                     readBlock(true);
147                 } catch (final ShortFileException sfe) { // NOSONAR
148                     return -1;
149                 }
150             }
151 
152             int n = 0;
153 
154             if (readOffset + len - bytes <= blockSize) {
155                 // we can read entirely from the buffer.
156                 n = len - bytes;
157             } else {
158                 // copy what we can from the buffer.
159                 n = blockSize - readOffset;
160             }
161 
162             // copy data, increment counters.
163             System.arraycopy(blockBuffer, readOffset, b, off, n);
164             readOffset += n;
165             bytes += n;
166             off += n;
167         }
168 
169         return bytes;
170     }
171 
172     /**
173      * Read next block. All decompression is handled here.
174      *
175      * @param decompress if false the buffer will not be decompressed. This is an optimization for longer seeks.
176      */
177     private void readBlock(final boolean decompress) throws IOException {
178         if (in == null) {
179             throw new IOException("Input buffer is closed");
180         }
181 
182         if (!isCompressed || currBlkIdx == -1) {
183             // file is not compressed
184             readFully(blockBuffer, 0, blockSize);
185             bytesRead += blockSize;
186         } else {
187             readFully(blockBuffer, 0, 4);
188             bytesRead += 4;
189 
190             final int h = DumpArchiveUtil.convert32(blockBuffer, 0);
191             final boolean compressed = (h & 0x01) == 0x01;
192 
193             if (!compressed) {
194                 // file is compressed but this block is not.
195                 readFully(blockBuffer, 0, blockSize);
196                 bytesRead += blockSize;
197             } else {
198                 // this block is compressed.
199                 final int flags = h >> 1 & 0x07;
200                 int length = h >> 4 & 0x0FFFFFFF;
201                 final byte[] compBuffer = readRange(length);
202                 bytesRead += length;
203 
204                 if (!decompress) {
205                     // just in case someone reads the data.
206                     Arrays.fill(blockBuffer, (byte) 0);
207                 } else {
208                     switch (DumpArchiveConstants.COMPRESSION_TYPE.find(flags & 0x03)) {
209                     case ZLIB:
210 
211                         final Inflater inflator = new Inflater();
212                         try {
213                             inflator.setInput(compBuffer, 0, compBuffer.length);
214                             length = inflator.inflate(blockBuffer);
215 
216                             if (length != blockSize) {
217                                 throw new ShortFileException();
218                             }
219                         } catch (final DataFormatException e) {
220                             throw new DumpArchiveException("Bad data", e);
221                         } finally {
222                             inflator.end();
223                         }
224 
225                         break;
226 
227                     case BZLIB:
228                         throw new UnsupportedCompressionAlgorithmException("BZLIB2");
229 
230                     case LZO:
231                         throw new UnsupportedCompressionAlgorithmException("LZO");
232 
233                     default:
234                         throw new UnsupportedCompressionAlgorithmException();
235                     }
236                 }
237             }
238         }
239 
240         currBlkIdx++;
241         readOffset = 0;
242     }
243 
244     /**
245      * Read buffer
246      */
247     private void readFully(final byte[] b, final int off, final int len) throws IOException {
248         final int count = IOUtils.readFully(in, b, off, len);
249         if (count < len) {
250             throw new ShortFileException();
251         }
252     }
253 
254     private byte[] readRange(final int len) throws IOException {
255         final byte[] ret = IOUtils.readRange(in, len);
256         if (ret.length < len) {
257             throw new ShortFileException();
258         }
259         return ret;
260     }
261 
262     /**
263      * Read a record from the input stream and return the data.
264      *
265      * @return The record data.
266      * @throws IOException on error
267      */
268     public byte[] readRecord() throws IOException {
269         final byte[] result = new byte[RECORD_SIZE];
270 
271         // the read implementation will loop internally as long as
272         // input is available
273         if (-1 == read(result, 0, result.length)) {
274             throw new ShortFileException();
275         }
276 
277         return result;
278     }
279 
280     /**
281      * Sets the DumpArchive Buffer's block size. We need to sync the block size with the dump archive's actual block size since compression is handled at the
282      * block level.
283      *
284      * @param recsPerBlock records per block
285      * @param isCompressed true if the archive is compressed
286      * @throws IOException more than one block has been read
287      * @throws IOException there was an error reading additional blocks.
288      * @throws IOException recsPerBlock is smaller than 1
289      */
290     public void resetBlockSize(final int recsPerBlock, final boolean isCompressed) throws IOException {
291         this.isCompressed = isCompressed;
292 
293         if (recsPerBlock < 1) {
294             throw new IOException("Block with " + recsPerBlock + " records found, must be at least 1");
295         }
296         blockSize = RECORD_SIZE * recsPerBlock;
297         if (blockSize < 1) {
298             throw new IOException("Block size cannot be less than or equal to 0: " + blockSize);
299         }
300 
301         // save first block in case we need it again
302         final byte[] oldBuffer = blockBuffer;
303 
304         // read rest of new block
305         blockBuffer = new byte[blockSize];
306         System.arraycopy(oldBuffer, 0, blockBuffer, 0, RECORD_SIZE);
307         readFully(blockBuffer, RECORD_SIZE, blockSize - RECORD_SIZE);
308 
309         this.currBlkIdx = 0;
310         this.readOffset = RECORD_SIZE;
311     }
312 
313     /**
314      * Skip bytes. Same as read but without the arraycopy.
315      *
316      * <p>
317      * skips the full given length unless EOF is reached.
318      * </p>
319      *
320      * @param len length to read, must be a multiple of the stream's record size
321      */
322     @Override
323     public long skip(final long len) throws IOException {
324         if (len % RECORD_SIZE != 0) {
325             throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
326         }
327 
328         long bytes = 0;
329 
330         while (bytes < len) {
331             // we need to read from the underlying stream.
332             // this will reset readOffset value. We do not perform
333             // any decompression if we won't eventually read the data.
334             // return -1 if there's a problem.
335             if (readOffset == blockSize) {
336                 try {
337                     readBlock(len - bytes < blockSize);
338                 } catch (final ShortFileException sfe) { // NOSONAR
339                     return -1;
340                 }
341             }
342 
343             long n = 0;
344 
345             if (readOffset + (len - bytes) <= blockSize) {
346                 // we can read entirely from the buffer.
347                 n = len - bytes;
348             } else {
349                 // copy what we can from the buffer.
350                 n = (long) blockSize - readOffset;
351             }
352 
353             // do not copy data but still increment counters.
354             readOffset = ExactMath.add(readOffset, n);
355             bytes += n;
356         }
357 
358         return bytes;
359     }
360 }