/* ** Authored by Timothy Gerard Endres ** <mailto:time@gjt.org> <http://www.trustice.com> ** ** This work has been placed into the public domain. ** You may use this work in any way and for any purpose you wish. ** ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR ** REDISTRIBUTION OF THIS SOFTWARE. ** */ package installer; import java.io.*; /** * 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(). * * @version $Revision: 12504 $ * @author Timothy Gerard Endres, * <a href="mailto:time@gjt.org">time@trustice.com</a>. * @see TarBuffer * @see TarHeader * @see TarEntry */ public class TarInputStream extends FilterInputStream { protected boolean debug; protected boolean hasHitEOF; protected int entrySize; protected int entryOffset; protected byte[] oneBuf; protected byte[] readBuf; protected TarBuffer buffer; protected TarEntry currEntry; protected EntryFactory eFactory; public TarInputStream( InputStream is ) { this( is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE ); } public TarInputStream( InputStream is, int blockSize ) { this( is, blockSize, TarBuffer.DEFAULT_RCDSIZE ); } public TarInputStream( InputStream is, int blockSize, int recordSize ) { super( is ); this.buffer = new TarBuffer( is, blockSize, recordSize ); this.readBuf = null; this.oneBuf = new byte[1]; this.debug = false; this.hasHitEOF = false; this.eFactory = null; } /** * Sets the debugging flag. * * @param debugF True to turn on debugging. */ public void setDebug( boolean debugF ) { this.debug = debugF; } /** * Sets the debugging flag. * * @param debugF True to turn on debugging. */ public void setEntryFactory( EntryFactory factory ) { this.eFactory = factory; } /** * Sets the debugging flag in this stream's TarBuffer. * * @param debugF True to turn on debugging. */ public void setBufferDebug( boolean debug ) { this.buffer.setDebug( debug ); } /** * Closes this stream. Calls the TarBuffer's close() method. */ public void close() throws IOException { this.buffer.close(); } /** * Get the record size being used by this stream's TarBuffer. * * @return The TarBuffer record size. */ public int getRecordSize() { return this.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. * * * @return The number of available bytes for the current entry. */ public int available() throws IOException { return this.entrySize - this.entryOffset; } /** * 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. */ public void skip( int numToSkip ) throws IOException { // REVIEW // This is horribly inefficient, but it ensures that we // properly skip over bytes via the TarBuffer... // byte[] skipBuf = new byte[ 8 * 1024 ]; for ( int num = numToSkip ; num > 0 ; ) { int numRead = this.read( skipBuf, 0, ( num > skipBuf.length ? skipBuf.length : num ) ); if ( numRead == -1 ) break; num -= numRead; } } /** * Since we do not support marking just yet, we return false. * * @return False. */ public boolean markSupported() { return false; } /** * Since we do not support marking just yet, we do nothing. * * @param markLimit The limit to mark. */ public void mark( int markLimit ) { } /** * Since we do not support marking just yet, we do nothing. */ public void reset() { } /** * 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. */ public TarEntry getNextEntry() throws IOException { if ( this.hasHitEOF ) return null; if ( this.currEntry != null ) { int numToSkip = this.entrySize - this.entryOffset; if ( this.debug ) System.err.println ( "TarInputStream: SKIP currENTRY '" + this.currEntry.getName() + "' SZ " + this.entrySize + " OFF " + this.entryOffset + " skipping " + numToSkip + " bytes" ); if ( numToSkip > 0 ) { this.skip( numToSkip ); } this.readBuf = null; } byte[] headerBuf = this.buffer.readRecord(); if ( headerBuf == null ) { if ( this.debug ) { System.err.println( "READ NULL RECORD" ); } this.hasHitEOF = true; } else if ( this.buffer.isEOFRecord( headerBuf ) ) { if ( this.debug ) { System.err.println( "READ EOF RECORD" ); } this.hasHitEOF = true; } if ( this.hasHitEOF ) { this.currEntry = null; } else { try { if ( this.eFactory == null ) { this.currEntry = new TarEntry( headerBuf ); } else { this.currEntry = this.eFactory.createEntry( headerBuf ); } if ( ! ( headerBuf[257] == 'u' && headerBuf[258] == 's' && headerBuf[259] == 't' && headerBuf[260] == 'a' && headerBuf[261] == 'r' ) ) { throw new InvalidHeaderException ( "header magic is not 'ustar', but '" + headerBuf[257] + headerBuf[258] + headerBuf[259] + headerBuf[260] + headerBuf[261] + "', or (dec) " + ((int)headerBuf[257]) + ", " + ((int)headerBuf[258]) + ", " + ((int)headerBuf[259]) + ", " + ((int)headerBuf[260]) + ", " + ((int)headerBuf[261]) ); } if ( this.debug ) System.err.println ( "TarInputStream: SET CURRENTRY '" + this.currEntry.getName() + "' size = " + this.currEntry.getSize() ); this.entryOffset = 0; // REVIEW How do we resolve this discrepancy?! this.entrySize = (int) this.currEntry.getSize(); } catch ( InvalidHeaderException ex ) { this.entrySize = 0; this.entryOffset = 0; this.currEntry = null; throw new InvalidHeaderException ( "bad header in block " + this.buffer.getCurrentBlockNum() + " record " + this.buffer.getCurrentRecordNum() + ", " + ex.getMessage() ); } } return this.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. */ public int read() throws IOException { int num = this.read( this.oneBuf, 0, 1 ); if ( num == -1 ) return num; else return this.oneBuf[0]; } /** * Reads bytes from the current tar archive entry. * * This method simply calls read( byte[], int, int ). * * @param buf The buffer into which to place bytes read. * @return The number of bytes read, or -1 at EOF. */ public int read( byte[] buf ) throws IOException { return this.read( buf, 0, buf.length ); } /** * 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. */ public int read( byte[] buf, int offset, int numToRead ) throws IOException { int totalRead = 0; if ( this.entryOffset >= this.entrySize ) return -1; if ( (numToRead + this.entryOffset) > this.entrySize ) { numToRead = (this.entrySize - this.entryOffset); } if ( this.readBuf != null ) { int sz = ( numToRead > this.readBuf.length ) ? this.readBuf.length : numToRead; System.arraycopy( this.readBuf, 0, buf, offset, sz ); if ( sz >= this.readBuf.length ) { this.readBuf = null; } else { int newLen = this.readBuf.length - sz; byte[] newBuf = new byte[ newLen ]; System.arraycopy( this.readBuf, sz, newBuf, 0, newLen ); this.readBuf = newBuf; } totalRead += sz; numToRead -= sz; offset += sz; } for ( ; numToRead > 0 ; ) { byte[] rec = this.buffer.readRecord(); if ( rec == null ) { // Unexpected EOF! throw new IOException ( "unexpected EOF with " + numToRead + " bytes unread" ); } int sz = numToRead; int recLen = rec.length; if ( recLen > sz ) { System.arraycopy( rec, 0, buf, offset, sz ); this.readBuf = new byte[ recLen - sz ]; System.arraycopy( rec, sz, this.readBuf, 0, recLen - sz ); } else { sz = recLen; System.arraycopy( rec, 0, buf, offset, recLen ); } totalRead += sz; numToRead -= sz; offset += sz; } this.entryOffset += totalRead; return totalRead; } /** * Copies the contents of the current tar archive entry directly into * an output stream. * * @param out The OutputStream into which to write the entry's data. */ public void copyEntryContents( OutputStream out ) throws IOException { byte[] buf = new byte[ 32 * 1024 ]; for ( ; ; ) { int numRead = this.read( buf, 0, buf.length ); if ( numRead == -1 ) break; out.write( buf, 0, numRead ); } } /** * This interface is provided, with the method setEntryFactory(), to allow * the programmer to have their own TarEntry subclass instantiated for the * entries return from getNextEntry(). */ public interface EntryFactory { public TarEntry createEntry( String name ); public TarEntry createEntry( File path ) throws InvalidHeaderException; public TarEntry createEntry( byte[] headerBuf ) throws InvalidHeaderException; } public class EntryAdapter implements EntryFactory { public TarEntry createEntry( String name ) { return new TarEntry( name ); } public TarEntry createEntry( File path ) throws InvalidHeaderException { return new TarEntry( path ); } public TarEntry createEntry( byte[] headerBuf ) throws InvalidHeaderException { return new TarEntry( headerBuf ); } } }