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;
}
}