TapeInputStream.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
- package org.apache.commons.compress.archivers.dump;
- import java.io.FilterInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.Arrays;
- import java.util.zip.DataFormatException;
- import java.util.zip.Inflater;
- import org.apache.commons.compress.utils.ExactMath;
- import org.apache.commons.compress.utils.IOUtils;
- /**
- * Filter stream that mimics a physical tape drive capable of compressing the data stream.
- *
- * @NotThreadSafe
- */
- final class TapeInputStream extends FilterInputStream {
- private static final int RECORD_SIZE = DumpArchiveConstants.TP_SIZE;
- private byte[] blockBuffer = new byte[DumpArchiveConstants.TP_SIZE];
- private int currBlkIdx = -1;
- private int blockSize = DumpArchiveConstants.TP_SIZE;
- private int readOffset = DumpArchiveConstants.TP_SIZE;
- private boolean isCompressed;
- private long bytesRead;
- /**
- * Constructs a new instance.
- *
- * @param in the underlying input stream.
- */
- TapeInputStream(final InputStream in) {
- super(in);
- }
- /**
- * @see java.io.InputStream#available
- */
- @Override
- public int available() throws IOException {
- if (readOffset < blockSize) {
- return blockSize - readOffset;
- }
- return in.available();
- }
- /**
- * Close the input stream.
- *
- * @throws IOException on error
- */
- @Override
- public void close() throws IOException {
- if (in != null && in != System.in) {
- in.close();
- }
- }
- /**
- * Gets number of bytes read.
- *
- * @return number of bytes read.
- */
- public long getBytesRead() {
- return bytesRead;
- }
- /**
- * Peek at the next record from the input stream and return the data.
- *
- * @return The record data.
- * @throws IOException on error
- */
- public byte[] peek() throws IOException {
- // we need to read from the underlying stream. This
- // isn't a problem since it would be the first step in
- // any subsequent read() anyway.
- if (readOffset == blockSize) {
- try {
- readBlock(true);
- } catch (final ShortFileException sfe) { // NOSONAR
- return null;
- }
- }
- // copy data, increment counters.
- final byte[] b = new byte[RECORD_SIZE];
- System.arraycopy(blockBuffer, readOffset, b, 0, b.length);
- return b;
- }
- /**
- * @see java.io.InputStream#read()
- */
- @Override
- public int read() throws IOException {
- throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
- }
- /**
- * {@inheritDoc}
- *
- * <p>
- * reads the full given length unless EOF is reached.
- * </p>
- *
- * @param len length to read, must be a multiple of the stream's record size
- */
- @Override
- public int read(final byte[] b, int off, final int len) throws IOException {
- if (len == 0) {
- return 0;
- }
- if (len % RECORD_SIZE != 0) {
- throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
- }
- int bytes = 0;
- while (bytes < len) {
- // we need to read from the underlying stream.
- // this will reset readOffset value.
- // return -1 if there's a problem.
- if (readOffset == blockSize) {
- try {
- readBlock(true);
- } catch (final ShortFileException sfe) { // NOSONAR
- return -1;
- }
- }
- int n = 0;
- if (readOffset + len - bytes <= blockSize) {
- // we can read entirely from the buffer.
- n = len - bytes;
- } else {
- // copy what we can from the buffer.
- n = blockSize - readOffset;
- }
- // copy data, increment counters.
- System.arraycopy(blockBuffer, readOffset, b, off, n);
- readOffset += n;
- bytes += n;
- off += n;
- }
- return bytes;
- }
- /**
- * Read next block. All decompression is handled here.
- *
- * @param decompress if false the buffer will not be decompressed. This is an optimization for longer seeks.
- */
- private void readBlock(final boolean decompress) throws IOException {
- if (in == null) {
- throw new IOException("Input buffer is closed");
- }
- if (!isCompressed || currBlkIdx == -1) {
- // file is not compressed
- readFully(blockBuffer, 0, blockSize);
- bytesRead += blockSize;
- } else {
- readFully(blockBuffer, 0, 4);
- bytesRead += 4;
- final int h = DumpArchiveUtil.convert32(blockBuffer, 0);
- final boolean compressed = (h & 0x01) == 0x01;
- if (!compressed) {
- // file is compressed but this block is not.
- readFully(blockBuffer, 0, blockSize);
- bytesRead += blockSize;
- } else {
- // this block is compressed.
- final int flags = h >> 1 & 0x07;
- int length = h >> 4 & 0x0FFFFFFF;
- final byte[] compBuffer = readRange(length);
- bytesRead += length;
- if (!decompress) {
- // just in case someone reads the data.
- Arrays.fill(blockBuffer, (byte) 0);
- } else {
- switch (DumpArchiveConstants.COMPRESSION_TYPE.find(flags & 0x03)) {
- case ZLIB:
- final Inflater inflator = new Inflater();
- try {
- inflator.setInput(compBuffer, 0, compBuffer.length);
- length = inflator.inflate(blockBuffer);
- if (length != blockSize) {
- throw new ShortFileException();
- }
- } catch (final DataFormatException e) {
- throw new DumpArchiveException("Bad data", e);
- } finally {
- inflator.end();
- }
- break;
- case BZLIB:
- throw new UnsupportedCompressionAlgorithmException("BZLIB2");
- case LZO:
- throw new UnsupportedCompressionAlgorithmException("LZO");
- default:
- throw new UnsupportedCompressionAlgorithmException();
- }
- }
- }
- }
- currBlkIdx++;
- readOffset = 0;
- }
- /**
- * Read buffer
- */
- private void readFully(final byte[] b, final int off, final int len) throws IOException {
- final int count = IOUtils.readFully(in, b, off, len);
- if (count < len) {
- throw new ShortFileException();
- }
- }
- private byte[] readRange(final int len) throws IOException {
- final byte[] ret = IOUtils.readRange(in, len);
- if (ret.length < len) {
- throw new ShortFileException();
- }
- return ret;
- }
- /**
- * Read a record from the input stream and return the data.
- *
- * @return The record data.
- * @throws IOException on error
- */
- public byte[] readRecord() throws IOException {
- final byte[] result = new byte[RECORD_SIZE];
- // the read implementation will loop internally as long as
- // input is available
- if (-1 == read(result, 0, result.length)) {
- throw new ShortFileException();
- }
- return result;
- }
- /**
- * 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
- * block level.
- *
- * @param recsPerBlock records per block
- * @param isCompressed true if the archive is compressed
- * @throws IOException more than one block has been read
- * @throws IOException there was an error reading additional blocks.
- * @throws IOException recsPerBlock is smaller than 1
- */
- public void resetBlockSize(final int recsPerBlock, final boolean isCompressed) throws IOException {
- this.isCompressed = isCompressed;
- if (recsPerBlock < 1) {
- throw new IOException("Block with " + recsPerBlock + " records found, must be at least 1");
- }
- blockSize = RECORD_SIZE * recsPerBlock;
- if (blockSize < 1) {
- throw new IOException("Block size cannot be less than or equal to 0: " + blockSize);
- }
- // save first block in case we need it again
- final byte[] oldBuffer = blockBuffer;
- // read rest of new block
- blockBuffer = new byte[blockSize];
- System.arraycopy(oldBuffer, 0, blockBuffer, 0, RECORD_SIZE);
- readFully(blockBuffer, RECORD_SIZE, blockSize - RECORD_SIZE);
- this.currBlkIdx = 0;
- this.readOffset = RECORD_SIZE;
- }
- /**
- * Skip bytes. Same as read but without the arraycopy.
- *
- * <p>
- * skips the full given length unless EOF is reached.
- * </p>
- *
- * @param len length to read, must be a multiple of the stream's record size
- */
- @Override
- public long skip(final long len) throws IOException {
- if (len % RECORD_SIZE != 0) {
- throw new IllegalArgumentException("All reads must be multiple of record size (" + RECORD_SIZE + " bytes.");
- }
- long bytes = 0;
- while (bytes < len) {
- // we need to read from the underlying stream.
- // this will reset readOffset value. We do not perform
- // any decompression if we won't eventually read the data.
- // return -1 if there's a problem.
- if (readOffset == blockSize) {
- try {
- readBlock(len - bytes < blockSize);
- } catch (final ShortFileException sfe) { // NOSONAR
- return -1;
- }
- }
- long n = 0;
- if (readOffset + (len - bytes) <= blockSize) {
- // we can read entirely from the buffer.
- n = len - bytes;
- } else {
- // copy what we can from the buffer.
- n = (long) blockSize - readOffset;
- }
- // do not copy data but still increment counters.
- readOffset = ExactMath.add(readOffset, n);
- bytes += n;
- }
- return bytes;
- }
- }