TapeInputStream.java

  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. import java.io.FilterInputStream;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.util.Arrays;
  24. import java.util.zip.DataFormatException;
  25. import java.util.zip.Inflater;

  26. import org.apache.commons.compress.utils.ExactMath;
  27. import org.apache.commons.compress.utils.IOUtils;

  28. /**
  29.  * Filter stream that mimics a physical tape drive capable of compressing the data stream.
  30.  *
  31.  * @NotThreadSafe
  32.  */
  33. final class TapeInputStream extends FilterInputStream {
  34.     private static final int RECORD_SIZE = DumpArchiveConstants.TP_SIZE;
  35.     private byte[] blockBuffer = new byte[DumpArchiveConstants.TP_SIZE];
  36.     private int currBlkIdx = -1;
  37.     private int blockSize = DumpArchiveConstants.TP_SIZE;
  38.     private int readOffset = DumpArchiveConstants.TP_SIZE;
  39.     private boolean isCompressed;
  40.     private long bytesRead;

  41.     /**
  42.      * Constructs a new instance.
  43.      *
  44.      * @param in the underlying input stream.
  45.      */
  46.     TapeInputStream(final InputStream in) {
  47.         super(in);
  48.     }

  49.     /**
  50.      * @see java.io.InputStream#available
  51.      */
  52.     @Override
  53.     public int available() throws IOException {
  54.         if (readOffset < blockSize) {
  55.             return blockSize - readOffset;
  56.         }

  57.         return in.available();
  58.     }

  59.     /**
  60.      * Close the input stream.
  61.      *
  62.      * @throws IOException on error
  63.      */
  64.     @Override
  65.     public void close() throws IOException {
  66.         if (in != null && in != System.in) {
  67.             in.close();
  68.         }
  69.     }

  70.     /**
  71.      * Gets number of bytes read.
  72.      *
  73.      * @return number of bytes read.
  74.      */
  75.     public long getBytesRead() {
  76.         return bytesRead;
  77.     }

  78.     /**
  79.      * Peek at the next record from the input stream and return the data.
  80.      *
  81.      * @return The record data.
  82.      * @throws IOException on error
  83.      */
  84.     public byte[] peek() throws IOException {
  85.         // we need to read from the underlying stream. This
  86.         // isn't a problem since it would be the first step in
  87.         // any subsequent read() anyway.
  88.         if (readOffset == blockSize) {
  89.             try {
  90.                 readBlock(true);
  91.             } catch (final ShortFileException sfe) { // NOSONAR
  92.                 return null;
  93.             }
  94.         }

  95.         // copy data, increment counters.
  96.         final byte[] b = new byte[RECORD_SIZE];
  97.         System.arraycopy(blockBuffer, readOffset, b, 0, b.length);

  98.         return b;
  99.     }

  100.     /**
  101.      * @see java.io.InputStream#read()
  102.      */
  103.     @Override
  104.     public int read() throws IOException {
  105.         throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
  106.     }

  107.     /**
  108.      * {@inheritDoc}
  109.      *
  110.      * <p>
  111.      * reads the full given length unless EOF is reached.
  112.      * </p>
  113.      *
  114.      * @param len length to read, must be a multiple of the stream's record size
  115.      */
  116.     @Override
  117.     public int read(final byte[] b, int off, final int len) throws IOException {
  118.         if (len == 0) {
  119.             return 0;
  120.         }
  121.         if (len % RECORD_SIZE != 0) {
  122.             throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
  123.         }

  124.         int bytes = 0;

  125.         while (bytes < len) {
  126.             // we need to read from the underlying stream.
  127.             // this will reset readOffset value.
  128.             // return -1 if there's a problem.
  129.             if (readOffset == blockSize) {
  130.                 try {
  131.                     readBlock(true);
  132.                 } catch (final ShortFileException sfe) { // NOSONAR
  133.                     return -1;
  134.                 }
  135.             }

  136.             int n = 0;

  137.             if (readOffset + len - bytes <= blockSize) {
  138.                 // we can read entirely from the buffer.
  139.                 n = len - bytes;
  140.             } else {
  141.                 // copy what we can from the buffer.
  142.                 n = blockSize - readOffset;
  143.             }

  144.             // copy data, increment counters.
  145.             System.arraycopy(blockBuffer, readOffset, b, off, n);
  146.             readOffset += n;
  147.             bytes += n;
  148.             off += n;
  149.         }

  150.         return bytes;
  151.     }

  152.     /**
  153.      * Read next block. All decompression is handled here.
  154.      *
  155.      * @param decompress if false the buffer will not be decompressed. This is an optimization for longer seeks.
  156.      */
  157.     private void readBlock(final boolean decompress) throws IOException {
  158.         if (in == null) {
  159.             throw new IOException("Input buffer is closed");
  160.         }

  161.         if (!isCompressed || currBlkIdx == -1) {
  162.             // file is not compressed
  163.             readFully(blockBuffer, 0, blockSize);
  164.             bytesRead += blockSize;
  165.         } else {
  166.             readFully(blockBuffer, 0, 4);
  167.             bytesRead += 4;

  168.             final int h = DumpArchiveUtil.convert32(blockBuffer, 0);
  169.             final boolean compressed = (h & 0x01) == 0x01;

  170.             if (!compressed) {
  171.                 // file is compressed but this block is not.
  172.                 readFully(blockBuffer, 0, blockSize);
  173.                 bytesRead += blockSize;
  174.             } else {
  175.                 // this block is compressed.
  176.                 final int flags = h >> 1 & 0x07;
  177.                 int length = h >> 4 & 0x0FFFFFFF;
  178.                 final byte[] compBuffer = readRange(length);
  179.                 bytesRead += length;

  180.                 if (!decompress) {
  181.                     // just in case someone reads the data.
  182.                     Arrays.fill(blockBuffer, (byte) 0);
  183.                 } else {
  184.                     switch (DumpArchiveConstants.COMPRESSION_TYPE.find(flags & 0x03)) {
  185.                     case ZLIB:

  186.                         final Inflater inflator = new Inflater();
  187.                         try {
  188.                             inflator.setInput(compBuffer, 0, compBuffer.length);
  189.                             length = inflator.inflate(blockBuffer);

  190.                             if (length != blockSize) {
  191.                                 throw new ShortFileException();
  192.                             }
  193.                         } catch (final DataFormatException e) {
  194.                             throw new DumpArchiveException("Bad data", e);
  195.                         } finally {
  196.                             inflator.end();
  197.                         }

  198.                         break;

  199.                     case BZLIB:
  200.                         throw new UnsupportedCompressionAlgorithmException("BZLIB2");

  201.                     case LZO:
  202.                         throw new UnsupportedCompressionAlgorithmException("LZO");

  203.                     default:
  204.                         throw new UnsupportedCompressionAlgorithmException();
  205.                     }
  206.                 }
  207.             }
  208.         }

  209.         currBlkIdx++;
  210.         readOffset = 0;
  211.     }

  212.     /**
  213.      * Read buffer
  214.      */
  215.     private void readFully(final byte[] b, final int off, final int len) throws IOException {
  216.         final int count = IOUtils.readFully(in, b, off, len);
  217.         if (count < len) {
  218.             throw new ShortFileException();
  219.         }
  220.     }

  221.     private byte[] readRange(final int len) throws IOException {
  222.         final byte[] ret = IOUtils.readRange(in, len);
  223.         if (ret.length < len) {
  224.             throw new ShortFileException();
  225.         }
  226.         return ret;
  227.     }

  228.     /**
  229.      * Read a record from the input stream and return the data.
  230.      *
  231.      * @return The record data.
  232.      * @throws IOException on error
  233.      */
  234.     public byte[] readRecord() throws IOException {
  235.         final byte[] result = new byte[RECORD_SIZE];

  236.         // the read implementation will loop internally as long as
  237.         // input is available
  238.         if (-1 == read(result, 0, result.length)) {
  239.             throw new ShortFileException();
  240.         }

  241.         return result;
  242.     }

  243.     /**
  244.      * 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
  245.      * block level.
  246.      *
  247.      * @param recsPerBlock records per block
  248.      * @param isCompressed true if the archive is compressed
  249.      * @throws IOException more than one block has been read
  250.      * @throws IOException there was an error reading additional blocks.
  251.      * @throws IOException recsPerBlock is smaller than 1
  252.      */
  253.     public void resetBlockSize(final int recsPerBlock, final boolean isCompressed) throws IOException {
  254.         this.isCompressed = isCompressed;

  255.         if (recsPerBlock < 1) {
  256.             throw new IOException("Block with " + recsPerBlock + " records found, must be at least 1");
  257.         }
  258.         blockSize = RECORD_SIZE * recsPerBlock;
  259.         if (blockSize < 1) {
  260.             throw new IOException("Block size cannot be less than or equal to 0: " + blockSize);
  261.         }

  262.         // save first block in case we need it again
  263.         final byte[] oldBuffer = blockBuffer;

  264.         // read rest of new block
  265.         blockBuffer = new byte[blockSize];
  266.         System.arraycopy(oldBuffer, 0, blockBuffer, 0, RECORD_SIZE);
  267.         readFully(blockBuffer, RECORD_SIZE, blockSize - RECORD_SIZE);

  268.         this.currBlkIdx = 0;
  269.         this.readOffset = RECORD_SIZE;
  270.     }

  271.     /**
  272.      * Skip bytes. Same as read but without the arraycopy.
  273.      *
  274.      * <p>
  275.      * skips the full given length unless EOF is reached.
  276.      * </p>
  277.      *
  278.      * @param len length to read, must be a multiple of the stream's record size
  279.      */
  280.     @Override
  281.     public long skip(final long len) throws IOException {
  282.         if (len % RECORD_SIZE != 0) {
  283.             throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
  284.         }

  285.         long bytes = 0;

  286.         while (bytes < len) {
  287.             // we need to read from the underlying stream.
  288.             // this will reset readOffset value. We do not perform
  289.             // any decompression if we won't eventually read the data.
  290.             // return -1 if there's a problem.
  291.             if (readOffset == blockSize) {
  292.                 try {
  293.                     readBlock(len - bytes < blockSize);
  294.                 } catch (final ShortFileException sfe) { // NOSONAR
  295.                     return -1;
  296.                 }
  297.             }

  298.             long n = 0;

  299.             if (readOffset + (len - bytes) <= blockSize) {
  300.                 // we can read entirely from the buffer.
  301.                 n = len - bytes;
  302.             } else {
  303.                 // copy what we can from the buffer.
  304.                 n = (long) blockSize - readOffset;
  305.             }

  306.             // do not copy data but still increment counters.
  307.             readOffset = ExactMath.add(readOffset, n);
  308.             bytes += n;
  309.         }

  310.         return bytes;
  311.     }
  312. }