/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.archive.tar.provider;
import com.mucommander.commons.io.BufferPool;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* The TarInputStream reads a UNIX tar archive as an InputStream.
* methods are provided to position at each successive entry in
* the archive, and the read each entry as a normal input stream
* using read().
*
* <p>-----------------------------------</p>
* <p>This class is based off the <code>org.apache.tools.tar</code> package of the <i>Apache Ant</i> project. The Ant
* code has been modified under the terms of the Apache License which you can find in the bundled muCommander license
* file. It was forked at version 1.7.1 of Ant.</p>
*
* @author Apache Ant, Maxence Bernard
*/
public class TarInputStream extends InputStream {
private static final int NAME_BUFFER_SIZE = 256;
private static final int BYTE_MASK = 0xFF;
protected boolean debug;
protected boolean hasHitEOF;
protected long entrySize;
protected long entryOffset;
protected byte[] recordBuf;
protected byte[] nameBuf;
protected int recordBufPos;
protected int recordBufLeft;
protected TarBuffer buffer;
protected TarEntry currEntry;
protected boolean closed;
/**
* This contents of this array is not used at all in this class,
* it is only here to avoid repreated object creation during calls
* to the no-arg read method.
*/
protected byte[] oneBuf;
/**
* Creates a new <code>TarInputStream</code> over the specified input stream using the default block size and
* record size and starting at the first entry.
*
* @param is the input stream providing the actual TAR data
* @throws IOException if an error ocurred while initializing the stream
*/
public TarInputStream(InputStream is) throws IOException {
this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, 0);
}
/**
* Creates a new <code>TarInputStream</code> over the specified input stream, starting at the specified
* entry offset.
*
* @param is the input stream providing the actual TAR data
* @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or
* <code>0</code> to start at the first entry.
* @throws IOException if an error ocurred while initializing the stream
*/
public TarInputStream(InputStream is, long entryOffset) throws IOException {
this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, entryOffset);
}
/**
* Creates a new <code>TarInputStream</code> over the specified input stream, using the specified
* block size, record size and start offset.
*
* @param is the input stream to use
* @param blockSize the block size to use
* @param recordSize the record size to use
* @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or
* <code>0</code> to start at the first entry.
* @throws IOException if an error ocurred while initializing the stream
*/
public TarInputStream(InputStream is, int blockSize, int recordSize, long entryOffset) throws IOException {
this.buffer = new TarBuffer(is, blockSize, recordSize);
this.recordBuf = BufferPool.getByteArray(buffer.getRecordSize());
this.nameBuf = BufferPool.getByteArray(NAME_BUFFER_SIZE);
this.oneBuf = BufferPool.getByteArray(1);
this.debug = false;
this.hasHitEOF = false;
if(entryOffset>0) {
if((entryOffset%recordSize)!=0)
throw new IllegalArgumentException("entryOffset ("+entryOffset+") is not a multiple of recordSize ("+recordSize+")");
skipBytes(entryOffset);
}
}
/**
* Sets the debugging flag.
*
* @param debug True to turn on debugging.
*/
public void setDebug(boolean debug) {
this.debug = debug;
buffer.setDebug(debug);
}
/**
* Closes this stream. Calls the TarBuffer's close() method.
* @throws IOException on error
*/
@Override
public void close() throws IOException {
if (!closed) {
try {
buffer.close();
}
finally {
BufferPool.releaseByteArray(recordBuf);
BufferPool.releaseByteArray(nameBuf);
BufferPool.releaseByteArray(oneBuf);
closed = true;
}
}
}
/**
* Get the record size being used by this stream's TarBuffer.
*
* @return The TarBuffer record size.
*/
public int getRecordSize() {
return buffer.getRecordSize();
}
/**
* Get the available data that can be read from the current
* entry in the archive. This does not indicate how much data
* is left in the entire archive, only in the current entry.
* This value is determined from the entry's size header field
* and the amount of data already read from the current entry.
* Integer.MAX_VALUE is returen in case more than Integer.MAX_VALUE
* bytes are left in the current entry in the archive.
*
* @return The number of available bytes for the current entry.
* @throws IOException for signature
*/
@Override
public int available() throws IOException {
if (entrySize - entryOffset > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) (entrySize - entryOffset);
}
/**
* Since we do not support marking just yet, we return false.
*
* @return False.
*/
@Override
public boolean markSupported() {
return false;
}
/**
* Since we do not support marking just yet, we do nothing.
*
* @param markLimit The limit to mark.
*/
@Override
public void mark(int markLimit) {
}
/**
* Since we do not support marking just yet, we do nothing.
*/
@Override
public void reset() {
}
/**
* Reads a whole new record from the {@link TarBuffer} into the {@link #recordBuf record buffer} and resets
* {@link #recordBufPos} and {@link #recordBufLeft} fields accordingly.
*
* @return <code>true</code> if the record has been read, <code>false</code> if EOF has been reached
* @throws IOException on error
*/
public boolean readRecord() throws IOException {
boolean ret = buffer.readRecord(recordBuf);
recordBufPos = 0;
recordBufLeft = ret?recordBuf.length:0;
return ret;
}
/**
* Returns the current entry where this <code>TarInputStream</code> is currently positionned.
*
* @return the current entry where this <code>TarInputStream</code> is currently positionned
*/
public TarEntry getCurrentEntry() {
return currEntry;
}
/**
* Get the next entry in this tar archive. This will skip
* over any remaining data in the current entry, if there
* is one, and place the input stream at the header of the
* next entry, and read the header and instantiate a new
* TarEntry from the header bytes and return that entry.
* If there are no more entries in the archive, null will
* be returned to indicate that the end of the archive has
* been reached.
*
* @return The next TarEntry in the archive, or null.
* @throws IOException on error
*/
public TarEntry getNextEntry() throws IOException {
if (hasHitEOF) {
return null;
}
if (currEntry != null) {
long numToSkip = entrySize - entryOffset;
if (debug) {
System.err.println("TarInputStream: SKIP currENTRY '"
+ currEntry.getName() + "' SZ "
+ entrySize + " OFF "
+ entryOffset + " skipping "
+ numToSkip + " bytes");
}
if (numToSkip > 0) {
skipBytes(numToSkip);
}
}
// Read the header record
if (!readRecord()) {
if (debug) {
System.err.println("READ NULL RECORD");
}
hasHitEOF = true;
} else if (buffer.isEOFRecord(recordBuf)) {
if (debug) {
System.err.println("READ EOF RECORD");
}
hasHitEOF = true;
}
if (hasHitEOF) {
currEntry = null;
} else {
currEntry = new TarEntry(recordBuf);
// Offset of the current entry from the start of the archive,
// allows to reposition the stream at the start of the entry
currEntry.setOffset(buffer.getCurrentBlockNum()*buffer.getBlockSize()
+ buffer.getCurrentRecordNum()*buffer.getRecordSize());
if (debug) {
System.err.println("TarInputStream: SET CURRENTRY '"
+ currEntry.getName()
+ "' size = "
+ currEntry.getSize());
}
// Update the current entry offset and size
entryOffset = 0;
entrySize = currEntry.getSize();
// Consume the rest of the record
recordBufPos = 0;
recordBufLeft = 0;
}
if (currEntry != null && currEntry.isGNULongNameEntry()) {
// read in the name
StringBuffer longName = new StringBuffer();
int length;
while ((length = read(nameBuf)) >= 0) {
longName.append(new String(nameBuf, 0, length));
}
getNextEntry();
if (currEntry == null) {
// Bugzilla: 40334
// Malformed tar file - long entry name not followed by entry
return null;
}
// remove trailing null terminator
if (longName.length() > 0
&& longName.charAt(longName.length() - 1) == 0) {
longName.deleteCharAt(longName.length() - 1);
}
currEntry.setName(longName.toString());
}
return currEntry;
}
/**
* Reads a byte from the current tar archive entry.
*
* This method simply calls read( byte[], int, int ).
*
* @return The byte read, or -1 at EOF.
* @throws IOException on error
*/
@Override
public int read() throws IOException {
int num = read(oneBuf, 0, 1);
return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK;
}
/**
* Reads bytes from the current tar archive entry.
*
* This method is aware of the boundaries of the current
* entry in the archive and will deal with them as if they
* were this stream's start and EOF.
*
* @param buf The buffer into which to place bytes read.
* @param offset The offset at which to place bytes read.
* @param numToRead The number of bytes to read.
* @return The number of bytes read, or -1 at EOF.
* @throws IOException on error
*/
@Override
public int read(byte[] buf, int offset, int numToRead) throws IOException {
int totalRead = 0;
// Have we already reached the end of file/entry ?
if (entryOffset >= entrySize) {
return -1;
}
// Can't read more than the entry's size
if ((numToRead + entryOffset) > entrySize) {
numToRead = (int) (entrySize - entryOffset);
}
// Read data one record (at most) at a time. The record buffer is first emptied before reading a new record.
while (numToRead > 0) {
// If there is no more data left to read from the current record buffer,
// read a new record
if(recordBufLeft<=0) {
if (!readRecord()) {
// Unexpected EOF!
throw new EOFException("unexpected EOF with " + numToRead + " bytes unread");
}
}
int sz = (numToRead > recordBufLeft)
? recordBufLeft
: numToRead;
System.arraycopy(recordBuf, recordBufPos, buf, offset, sz);
recordBufPos += sz;
recordBufLeft -= sz;
totalRead += sz;
numToRead -= sz;
offset += sz;
entryOffset += sz;
}
return totalRead;
}
/**
* Skip bytes in the input buffer. This skips bytes in the
* current entry's data, not the entire archive, and will
* stop at the end of the current entry's data if the number
* to skip extends beyond that point.
*
* @param numToSkip the number of bytes to skip.
* @return the number actually skipped
* @throws IOException on error
*/
@Override
public long skip(long numToSkip) throws IOException {
// Have we already reached the end of file/entry ?
if (entryOffset >= entrySize) {
return -1;
}
// Can't read more than the entry's size
if ((numToSkip + entryOffset) > entrySize) {
numToSkip = (int) (entrySize - entryOffset);
}
return skipBytes(numToSkip);
}
/**
* Skips the specified number of bytes, without checking for the current entry's boundaries.
*
* @param numToSkip the number of bytes to skip.
* @return the number actually skipped
* @throws IOException on error
*/
private long skipBytes(long numToSkip) throws IOException {
int totalSkipped = 0;
int recordSize = buffer.getRecordSize();
int blockSize = buffer.getBlockSize();
while (numToSkip > 0) {
// If the record buffer has some data left, empty it
if(recordBufLeft>0) {
int sz = (numToSkip > recordBufLeft)
? recordBufLeft
: (int)numToSkip;
recordBufPos += sz;
recordBufLeft -= sz;
totalSkipped += sz;
numToSkip -= sz;
entryOffset += sz;
}
// Skip a whole block if there are enough bytes left to skip, and if we are at the end of the current block
else if(numToSkip>=blockSize && buffer.getCurrentRecordNum()==buffer.getRecordsPerBlock()-1) {
if (!buffer.skipBlock()) {
// Unexpected EOF!
throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped");
}
totalSkipped += blockSize;
numToSkip -= blockSize;
entryOffset += blockSize;
}
// Skip a whole record if there are enough bytes left to skip
else if(numToSkip>=recordSize) {
if (!buffer.skipRecord()) {
// Unexpected EOF!
throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped");
}
totalSkipped += recordSize;
numToSkip -= recordSize;
entryOffset += recordSize;
}
// There is less than a record to skip -> read the record and skip
else {
if (!readRecord()) {
// Unexpected EOF!
throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped");
}
// if(recordBufLeft>0) will be matched on the next loop
}
}
return totalSkipped;
}
}