File Input Output Java

/*
** Authored by Timothy Gerard Endres
**   
** 
** 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. 
** 
*/
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
/**
 * The TarOutputStream writes a UNIX tar archive as an OutputStream.
 * Methods are provided to put entries, and then write their contents
 * by writing to this stream using write().
 *
 *
 * @version $Revision: 12504 $
 * @author Timothy Gerard Endres,
 *  time@trustice.com.
 * @see TarBuffer
 * @see TarHeader
 * @see TarEntry
 */
public
class   TarOutputStream
extends   FilterOutputStream
  {
  protected boolean     debug;
  protected int       currSize;
  protected int       currBytes;
  protected byte[]      oneBuf;
  protected byte[]      recordBuf;
  protected int       assemLen;
  protected byte[]      assemBuf;
  protected TarBuffer     buffer;
  public
  TarOutputStream( OutputStream os )
    {
    this( os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE );
    }
  public
  TarOutputStream( OutputStream os, int blockSize )
    {
    this( os, blockSize, TarBuffer.DEFAULT_RCDSIZE );
    }
  public
  TarOutputStream( OutputStream os, int blockSize, int recordSize )
    {
    super( os );
    this.buffer = new TarBuffer( os, blockSize, recordSize );
    
    this.debug = false;
    this.assemLen = 0;
    this.assemBuf = new byte[ recordSize ];
    this.recordBuf = new byte[ recordSize ];
    this.oneBuf = new byte[1];
    }
  /**
   * Sets the debugging flag.
   *
   * @param debugF True to turn on debugging.
   */
  public void
  setDebug( boolean debugF )
    {
    this.debug = debugF;
    }
  /**
   * 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 );
    }
  /**
   * Ends the TAR archive without closing the underlying OutputStream.
   * The result is that the EOF record of nulls is written.
   */
  public void
  finish()
    throws IOException
    {
    this.writeEOFRecord();
    }
  /**
   * Ends the TAR archive and closes the underlying OutputStream.
   * This means that finish() is called followed by calling the
   * TarBuffer's close().
   */
  public void
  close()
    throws IOException
    {
    this.finish();
    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();
    }
  /**
   * Put an entry on the output stream. This writes the entry's
   * header record and positions the output stream for writing
   * the contents of the entry. Once this method is called, the
   * stream is ready for calls to write() to write the entry's
   * contents. Once the contents are written, closeEntry()
   * MUST be called to ensure that all buffered data
   * is completely written to the output stream.
   *
   * @param entry The TarEntry to be written to the archive.
   */
  public void
  putNextEntry( TarEntry entry )
    throws IOException
    {
    if ( entry.getHeader().name.length() > TarHeader.NAMELEN )
      throw new InvalidHeaderException
        ( "file name '" + entry.getHeader().name
          + "' is too long ( > "
          + TarHeader.NAMELEN + " bytes )" );
    entry.writeEntryHeader( this.recordBuf );
    this.buffer.writeRecord( this.recordBuf );
    this.currBytes = 0;
    if ( entry.isDirectory() )
      this.currSize = 0;
    else
      this.currSize = (int)entry.getSize();
    }
  /**
   * Close an entry. This method MUST be called for all file
   * entries that contain data. The reason is that we must
   * buffer data written to the stream in order to satisfy
   * the buffer's record based writes. Thus, there may be
   * data fragments still being assembled that must be written
   * to the output stream before this entry is closed and the
   * next entry written.
   */
  public void
  closeEntry()
    throws IOException
    {
    if ( this.assemLen > 0 )
      {
      for ( int i = this.assemLen ; i < this.assemBuf.length ; ++i )
        this.assemBuf[i] = 0;
      this.buffer.writeRecord( this.assemBuf );
      this.currBytes += this.assemLen;
      this.assemLen = 0;
      }
    if ( this.currBytes < this.currSize )
      throw new IOException
        ( "entry closed at '" + this.currBytes
          + "' before the '" + this.currSize
          + "' bytes specified in the header were written" );
    }
  /**
   * Writes a byte to the current tar archive entry.
   *
   * This method simply calls read( byte[], int, int ).
   *
   * @param b The byte written.
   */
  public void
  write( int b )
    throws IOException
    {
    this.oneBuf[0] = (byte) b;
    this.write( this.oneBuf, 0, 1 );
    }
  /**
   * Writes bytes to the current tar archive entry.
   *
   * This method simply calls read( byte[], int, int ).
   *
   * @param wBuf The buffer to write to the archive.
   * @return The number of bytes read, or -1 at EOF.
   */
  public void
  write( byte[] wBuf )
    throws IOException
    {
    this.write( wBuf, 0, wBuf.length );
    }
  /**
   * Writes bytes to the current tar archive entry. This method
   * is aware of the current entry and will throw an exception if
   * you attempt to write bytes past the length specified for the
   * current entry. The method is also (painfully) aware of the
   * record buffering required by TarBuffer, and manages buffers
   * that are not a multiple of recordsize in length, including
   * assembling records from small buffers.
   *
   * This method simply calls read( byte[], int, int ).
   *
   * @param wBuf The buffer to write to the archive.
   * @param wOffset The offset in the buffer from which to get bytes.
   * @param numToWrite The number of bytes to write.
   */
  public void
  write( byte[] wBuf, int wOffset, int numToWrite )
    throws IOException
    {
    if ( (this.currBytes + numToWrite) > this.currSize )
      throw new IOException
        ( "request to write '" + numToWrite
          + "' bytes exceeds size in header of '"
          + this.currSize + "' bytes" );
    //
    // We have to deal with assembly!!!
    // The programmer can be writing little 32 byte chunks for all
    // we know, and we must assemble complete records for writing.
    // REVIEW Maybe this should be in TarBuffer? Could that help to
    //        eliminate some of the buffer copying.
    //
    if ( this.assemLen > 0 )
      {
      if ( (this.assemLen + numToWrite ) >= this.recordBuf.length )
        {
        int aLen = this.recordBuf.length - this.assemLen;
        System.arraycopy
          ( this.assemBuf, 0, this.recordBuf, 0, this.assemLen );
        System.arraycopy
          ( wBuf, wOffset, this.recordBuf, this.assemLen, aLen );
        this.buffer.writeRecord( this.recordBuf );
        this.currBytes += this.recordBuf.length;
        wOffset += aLen;
        numToWrite -= aLen;
        this.assemLen = 0;
        }
      else // ( (this.assemLen + numToWrite ) < this.recordBuf.length )
        {
        System.arraycopy
          ( wBuf, wOffset, this.assemBuf,
            this.assemLen, numToWrite );
        wOffset += numToWrite;
        this.assemLen += numToWrite; 
        numToWrite -= numToWrite;
        }
      }
    //
    // When we get here we have EITHER:
    //   o An empty "assemble" buffer.
    //   o No bytes to write (numToWrite == 0)
    //
    for ( ; numToWrite > 0 ; )
      {
      if ( numToWrite < this.recordBuf.length )
        {
        System.arraycopy
          ( wBuf, wOffset, this.assemBuf, this.assemLen, numToWrite );
        this.assemLen += numToWrite;
        break;
        }
      this.buffer.writeRecord( wBuf, wOffset );
      int num = this.recordBuf.length;
      this.currBytes += num;
      numToWrite -= num;
      wOffset += num;
      }
    }
  /**
   * Write an EOF (end of archive) record to the tar archive.
   * An EOF record consists of a record of all zeros.
   */
  private void
  writeEOFRecord()
    throws IOException
    {
    for ( int i = 0 ; i < this.recordBuf.length ; ++i )
      this.recordBuf[i] = 0;
    this.buffer.writeRecord( this.recordBuf );
    }
  }
/*
 * * Authored by Timothy Gerard Endres * 
 *  * * 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. *
 */
/**
 * This class encapsulates the Tar Entry Header used in Tar Archives. The class
 * also holds a number of tar constants, used mostly in headers.
 */
class TarHeader extends Object {
  /**
   * The length of the name field in a header buffer.
   */
  public static final int NAMELEN = 100;
  /**
   * The length of the mode field in a header buffer.
   */
  public static final int MODELEN = 8;
  /**
   * The length of the user id field in a header buffer.
   */
  public static final int UIDLEN = 8;
  /**
   * The length of the group id field in a header buffer.
   */
  public static final int GIDLEN = 8;
  /**
   * The length of the checksum field in a header buffer.
   */
  public static final int CHKSUMLEN = 8;
  /**
   * The length of the size field in a header buffer.
   */
  public static final int SIZELEN = 12;
  /**
   * The length of the magic field in a header buffer.
   */
  public static final int MAGICLEN = 8;
  /**
   * The length of the modification time field in a header buffer.
   */
  public static final int MODTIMELEN = 12;
  /**
   * The length of the user name field in a header buffer.
   */
  public static final int UNAMELEN = 32;
  /**
   * The length of the group name field in a header buffer.
   */
  public static final int GNAMELEN = 32;
  /**
   * The length of the devices field in a header buffer.
   */
  public static final int DEVLEN = 8;
  /**
   * LF_ constants represent the "link flag" of an entry, or more commonly, the
   * "entry type". This is the "old way" of indicating a normal file.
   */
  public static final byte LF_OLDNORM = 0;
  /**
   * Normal file type.
   */
  public static final byte LF_NORMAL = (byte) '0';
  /**
   * Link file type.
   */
  public static final byte LF_LINK = (byte) '1';
  /**
   * Symbolic link file type.
   */
  public static final byte LF_SYMLINK = (byte) '2';
  /**
   * Character device file type.
   */
  public static final byte LF_CHR = (byte) '3';
  /**
   * Block device file type.
   */
  public static final byte LF_BLK = (byte) '4';
  /**
   * Directory file type.
   */
  public static final byte LF_DIR = (byte) '5';
  /**
   * FIFO (pipe) file type.
   */
  public static final byte LF_FIFO = (byte) '6';
  /**
   * Contiguous file type.
   */
  public static final byte LF_CONTIG = (byte) '7';
  /**
   * The magic tag representing a POSIX tar archive.
   */
  public static final String TMAGIC = "ustar";
  /**
   * The magic tag representing a GNU tar archive.
   */
  public static final String GNU_TMAGIC = "ustar  ";
  /**
   * The entry's name.
   */
  public StringBuffer name;
  /**
   * The entry's permission mode.
   */
  public int mode;
  /**
   * The entry's user id.
   */
  public int userId;
  /**
   * The entry's group id.
   */
  public int groupId;
  /**
   * The entry's size.
   */
  public long size;
  /**
   * The entry's modification time.
   */
  public long modTime;
  /**
   * The entry's checksum.
   */
  public int checkSum;
  /**
   * The entry's link flag.
   */
  public byte linkFlag;
  /**
   * The entry's link name.
   */
  public StringBuffer linkName;
  /**
   * The entry's magic tag.
   */
  public StringBuffer magic;
  /**
   * The entry's user name.
   */
  public StringBuffer userName;
  /**
   * The entry's group name.
   */
  public StringBuffer groupName;
  /**
   * The entry's major device number.
   */
  public int devMajor;
  /**
   * The entry's minor device number.
   */
  public int devMinor;
  public TarHeader() {
    this.magic = new StringBuffer(TarHeader.TMAGIC);
    this.name = new StringBuffer();
    this.linkName = new StringBuffer();
    String user = System.getProperty("user.name", "");
    if (user.length() > 31)
      user = user.substring(0, 31);
    this.userId = 0;
    this.groupId = 0;
    this.userName = new StringBuffer(user);
    this.groupName = new StringBuffer("");
  }
  /**
   * TarHeaders can be cloned.
   */
  public Object clone() {
    TarHeader hdr = null;
    try {
      hdr = (TarHeader) super.clone();
      hdr.name = (this.name == null) ? null : new StringBuffer(this.name.toString());
      hdr.mode = this.mode;
      hdr.userId = this.userId;
      hdr.groupId = this.groupId;
      hdr.size = this.size;
      hdr.modTime = this.modTime;
      hdr.checkSum = this.checkSum;
      hdr.linkFlag = this.linkFlag;
      hdr.linkName = (this.linkName == null) ? null : new StringBuffer(this.linkName.toString());
      hdr.magic = (this.magic == null) ? null : new StringBuffer(this.magic.toString());
      hdr.userName = (this.userName == null) ? null : new StringBuffer(this.userName.toString());
      hdr.groupName = (this.groupName == null) ? null : new StringBuffer(this.groupName.toString());
      hdr.devMajor = this.devMajor;
      hdr.devMinor = this.devMinor;
    } catch (CloneNotSupportedException ex) {
      ex.printStackTrace();
    }
    return hdr;
  }
  /**
   * Get the name of this entry.
   * 
   * @return Teh entry's name.
   */
  public String getName() {
    return this.name.toString();
  }
  /**
   * Parse an octal string from a header buffer. This is used for the file
   * permission mode value.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The long value of the octal string.
   */
  public static long parseOctal(byte[] header, int offset, int length)
      throws InvalidHeaderException {
    long result = 0;
    boolean stillPadding = true;
    int end = offset + length;
    for (int i = offset; i < end; ++i) {
      if (header[i] == 0)
        break;
      if (header[i] == (byte) ' ' || header[i] == '0') {
        if (stillPadding)
          continue;
        if (header[i] == (byte) ' ')
          break;
      }
      stillPadding = false;
      result = (result << 3) + (header[i] - '0');
    }
    return result;
  }
  /**
   * Parse an entry name from a header buffer.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The header's entry name.
   */
  public static StringBuffer parseName(byte[] header, int offset, int length)
      throws InvalidHeaderException {
    StringBuffer result = new StringBuffer(length);
    int end = offset + length;
    for (int i = offset; i < end; ++i) {
      if (header[i] == 0)
        break;
      result.append((char) header[i]);
    }
    return result;
  }
  /**
   * Determine the number of bytes in an entry name.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The number of bytes in a header's entry name.
   */
  public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) {
    int i;
    for (i = 0; i < length && i < name.length(); ++i) {
      buf[offset + i] = (byte) name.charAt(i);
    }
    for (; i < length; ++i) {
      buf[offset + i] = 0;
    }
    return offset + length;
  }
  /**
   * Parse an octal integer from a header buffer.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The integer value of the octal bytes.
   */
  public static int getOctalBytes(long value, byte[] buf, int offset, int length) {
    byte[] result = new byte[length];
    int idx = length - 1;
    buf[offset + idx] = 0;
    --idx;
    buf[offset + idx] = (byte) ' ';
    --idx;
    if (value == 0) {
      buf[offset + idx] = (byte) '0';
      --idx;
    } else {
      for (long val = value; idx >= 0 && val > 0; --idx) {
        buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7));
        val = val >> 3;
      }
    }
    for (; idx >= 0; --idx) {
      buf[offset + idx] = (byte) ' ';
    }
    return offset + length;
  }
  /**
   * Parse an octal long integer from a header buffer.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The long value of the octal bytes.
   */
  public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) {
    byte[] temp = new byte[length + 1];
    TarHeader.getOctalBytes(value, temp, 0, length + 1);
    System.arraycopy(temp, 0, buf, offset, length);
    return offset + length;
  }
  /**
   * Parse the checksum octal integer from a header buffer.
   * 
   * @param header
   *          The header buffer from which to parse.
   * @param offset
   *          The offset into the buffer from which to parse.
   * @param length
   *          The number of header bytes to parse.
   * @return The integer value of the entry's checksum.
   */
  public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) {
    TarHeader.getOctalBytes(value, buf, offset, length);
    buf[offset + length - 1] = (byte) ' ';
    buf[offset + length - 2] = 0;
    return offset + length;
  }
}
/*
 * * Authored by Timothy Gerard Endres * 
 *  * * 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. *
 */
/**
 * 
 * This class represents an entry in a Tar archive. It consists of the entry's
 * header, as well as the entry's File. Entries can be instantiated in one of
 * three ways, depending on how they are to be used.
 * 


 * TarEntries that are created from the header bytes read from an archive are
 * instantiated with the TarEntry( byte[] ) constructor. These entries will be
 * used when extracting from or listing the contents of an archive. These
 * entries have their header filled in using the header bytes. They also set the
 * File to null, since they reference an archive entry not a file.
 * 


 * TarEntries that are created from Files that are to be written into an archive
 * are instantiated with the TarEntry( File ) constructor. These entries have
 * their header filled in using the File's information. They also keep a
 * reference to the File for convenience when writing entries.
 * 


 * Finally, TarEntries can be constructed from nothing but a name. This allows
 * the programmer to construct the entry by hand, for instance when only an
 * InputStream is available for writing to the archive, and the header
 * information is constructed from other information. In this case the header
 * fields are set to defaults and the File is set to null.
 * 
 * 


 * The C structure for a Tar Entry's header is:
 * 
 * 


 *  struct header {
 *    char  name[NAMSIZ];
 *    char  mode[8];
 *    char  uid[8];
 *    char  gid[8];
 *    char  size[12];
 *    char  mtime[12];
 *    char  chksum[8];
 *    char  linkflag;
 *    char  linkname[NAMSIZ];
 *    char  magic[8];
 *    char  uname[TUNMLEN];
 *    char  gname[TGNMLEN];
 *    char  devmajor[8];
 *    char  devminor[8];
 *  } header;
 * 

 * 
 * @see TarHeader
 * 
 */
class TarEntry extends Object {
  /**
   * If this entry represents a File, this references it.
   */
  protected File file;
  /**
   * This is the entry's header information.
   */
  protected TarHeader header;
  /**
   * Construct an entry with only a name. This allows the programmer to
   * construct the entry's header "by hand". File is set to null.
   */
  public TarEntry(String name) {
    this.initialize();
    this.nameTarHeader(this.header, name);
  }
  /**
   * Construct an entry for a file. File is set to file, and the header is
   * constructed from information from the file.
   * 
   * @param file
   *          The file that the entry represents.
   */
  public TarEntry(File file) throws InvalidHeaderException {
    this.initialize();
    this.getFileTarHeader(this.header, file);
  }
  /**
   * Construct an entry from an archive's header bytes. File is set to null.
   * 
   * @param headerBuf
   *          The header bytes from a tar archive entry.
   */
  public TarEntry(byte[] headerBuf) throws InvalidHeaderException {
    this.initialize();
    this.parseTarHeader(this.header, headerBuf);
  }
  /**
   * Initialization code common to all constructors.
   */
  private void initialize() {
    this.file = null;
    this.header = new TarHeader();
  }
  /**
   * Determine if the two entries are equal. Equality is determined by the
   * header names being equal.
   * 
   * @return it Entry to be checked for equality.
   * @return True if the entries are equal.
   */
  public boolean equals(TarEntry it) {
    return this.header.name.toString().equals(it.header.name.toString());
  }
  /**
   * Determine if the given entry is a descendant of this entry. Descendancy is
   * determined by the name of the descendant starting with this entry's name.
   * 
   * @param desc
   *          Entry to be checked as a descendent of this.
   * @return True if entry is a descendant of this.
   */
  public boolean isDescendent(TarEntry desc) {
    return desc.header.name.toString().startsWith(this.header.name.toString());
  }
  /**
   * Get this entry's header.
   * 
   * @return This entry's TarHeader.
   */
  public TarHeader getHeader() {
    return this.header;
  }
  /**
   * Get this entry's name.
   * 
   * @return This entry's name.
   */
  public String getName() {
    return this.header.name.toString();
  }
  /**
   * Set this entry's name.
   * 
   * @param name
   *          This entry's new name.
   */
  public void setName(String name) {
    this.header.name = new StringBuffer(name);
  }
  /**
   * Get this entry's user id.
   * 
   * @return This entry's user id.
   */
  public int getUserId() {
    return this.header.userId;
  }
  /**
   * Set this entry's user id.
   * 
   * @param userId
   *          This entry's new user id.
   */
  public void setUserId(int userId) {
    this.header.userId = userId;
  }
  /**
   * Get this entry's group id.
   * 
   * @return This entry's group id.
   */
  public int getGroupId() {
    return this.header.groupId;
  }
  /**
   * Set this entry's group id.
   * 
   * @param groupId
   *          This entry's new group id.
   */
  public void setGroupId(int groupId) {
    this.header.groupId = groupId;
  }
  /**
   * Get this entry's user name.
   * 
   * @return This entry's user name.
   */
  public String getUserName() {
    return this.header.userName.toString();
  }
  /**
   * Set this entry's user name.
   * 
   * @param userName
   *          This entry's new user name.
   */
  public void setUserName(String userName) {
    this.header.userName = new StringBuffer(userName);
  }
  /**
   * Get this entry's group name.
   * 
   * @return This entry's group name.
   */
  public String getGroupName() {
    return this.header.groupName.toString();
  }
  /**
   * Set this entry's group name.
   * 
   * @param groupName
   *          This entry's new group name.
   */
  public void setGroupName(String groupName) {
    this.header.groupName = new StringBuffer(groupName);
  }
  /**
   * Convenience method to set this entry's group and user ids.
   * 
   * @param userId
   *          This entry's new user id.
   * @param groupId
   *          This entry's new group id.
   */
  public void setIds(int userId, int groupId) {
    this.setUserId(userId);
    this.setGroupId(groupId);
  }
  /**
   * Convenience method to set this entry's group and user names.
   * 
   * @param userName
   *          This entry's new user name.
   * @param groupName
   *          This entry's new group name.
   */
  public void setNames(String userName, String groupName) {
    this.setUserName(userName);
    this.setGroupName(groupName);
  }
  /**
   * Set this entry's modification time. The parameter passed to this method is
   * in "Java time".
   * 
   * @param time
   *          This entry's new modification time.
   */
  public void setModTime(long time) {
    this.header.modTime = time / 1000;
  }
  /**
   * Set this entry's modification time.
   * 
   * @param time
   *          This entry's new modification time.
   */
  public void setModTime(Date time) {
    this.header.modTime = time.getTime() / 1000;
  }
  /**
   * Set this entry's modification time.
   * 
   * @param time
   *          This entry's new modification time.
   */
  public Date getModTime() {
    return new Date(this.header.modTime * 1000);
  }
  /**
   * Get this entry's file.
   * 
   * @return This entry's file.
   */
  public File getFile() {
    return this.file;
  }
  /**
   * Get this entry's file size.
   * 
   * @return This entry's file size.
   */
  public long getSize() {
    return this.header.size;
  }
  /**
   * Set this entry's file size.
   * 
   * @param size
   *          This entry's new file size.
   */
  public void setSize(long size) {
    this.header.size = size;
  }
  /**
   * Convenience method that will modify an entry's name directly in place in an
   * entry header buffer byte array.
   * 
   * @param outbuf
   *          The buffer containing the entry header to modify.
   * @param newName
   *          The new name to place into the header buffer.
   */
  public void adjustEntryName(byte[] outbuf, String newName) {
    int offset = 0;
    offset = TarHeader.getNameBytes(new StringBuffer(newName), outbuf, offset, TarHeader.NAMELEN);
  }
  /**
   * Return whether or not this entry represents a directory.
   * 
   * @return True if this entry is a directory.
   */
  public boolean isDirectory() {
    if (this.file != null)
      return this.file.isDirectory();
    if (this.header != null) {
      if (this.header.linkFlag == TarHeader.LF_DIR)
        return true;
      if (this.header.name.toString().endsWith("/"))
        return true;
    }
    return false;
  }
  /**
   * Fill in a TarHeader with information from a File.
   * 
   * @param hdr
   *          The TarHeader to fill in.
   * @param file
   *          The file from which to get the header information.
   */
  public void getFileTarHeader(TarHeader hdr, File file) throws InvalidHeaderException {
    this.file = file;
    String name = file.getPath();
    String osname = System.getProperty("os.name");
    if (osname != null) {
      // Strip off drive letters!
      // REVIEW Would a better check be "(File.separator == '\')"?
      // String Win32Prefix = "Windows";
      // String prefix = osname.substring( 0, Win32Prefix.length() );
      // if ( prefix.equalsIgnoreCase( Win32Prefix ) )
      // if ( File.separatorChar == '\\' )
      // Per Patrick Beard:
      String Win32Prefix = "windows";
      if (osname.toLowerCase().startsWith(Win32Prefix)) {
        if (name.length() > 2) {
          char ch1 = name.charAt(0);
          char ch2 = name.charAt(1);
          if (ch2 == ':' && ((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z'))) {
            name = name.substring(2);
          }
        }
      }
    }
    name = name.replace(File.separatorChar, '/');
    // No absolute pathnames
    // Windows (and Posix?) paths can start with "\\NetworkDrive\",
    // so we loop on starting /'s.
    for (; name.startsWith("/");)
      name = name.substring(1);
    hdr.linkName = new StringBuffer("");
    hdr.name = new StringBuffer(name);
    if (file.isDirectory()) {
      hdr.mode = 040755;
      hdr.linkFlag = TarHeader.LF_DIR;
      if (hdr.name.charAt(hdr.name.length() - 1) != '/')
        hdr.name.append("/");
    } else {
      hdr.mode = 0100644;
      hdr.linkFlag = TarHeader.LF_NORMAL;
    }
    // UNDONE When File lets us get the userName, use it!
    hdr.size = file.length();
    hdr.modTime = file.lastModified() / 1000;
    hdr.checkSum = 0;
    hdr.devMajor = 0;
    hdr.devMinor = 0;
  }
  /**
   * If this entry represents a file, and the file is a directory, return an
   * array of TarEntries for this entry's children.
   * 
   * @return An array of TarEntry's for this entry's children.
   */
  public TarEntry[] getDirectoryEntries() throws InvalidHeaderException {
    if (this.file == null || !this.file.isDirectory()) {
      return new TarEntry[0];
    }
    String[] list = this.file.list();
    TarEntry[] result = new TarEntry[list.length];
    for (int i = 0; i < list.length; ++i) {
      result[i] = new TarEntry(new File(this.file, list[i]));
    }
    return result;
  }
  /**
   * Compute the checksum of a tar entry header.
   * 
   * @param buf
   *          The tar entry's header buffer.
   * @return The computed checksum.
   */
  public long computeCheckSum(byte[] buf) {
    long sum = 0;
    for (int i = 0; i < buf.length; ++i) {
      sum += 255 & buf[i];
    }
    return sum;
  }
  /**
   * Write an entry's header information to a header buffer.
   * 
   * @param outbuf
   *          The tar entry header buffer to fill in.
   */
  public void writeEntryHeader(byte[] outbuf) {
    int offset = 0;
    offset = TarHeader.getNameBytes(this.header.name, outbuf, offset, TarHeader.NAMELEN);
    offset = TarHeader.getOctalBytes(this.header.mode, outbuf, offset, TarHeader.MODELEN);
    offset = TarHeader.getOctalBytes(this.header.userId, outbuf, offset, TarHeader.UIDLEN);
    offset = TarHeader.getOctalBytes(this.header.groupId, outbuf, offset, TarHeader.GIDLEN);
    long size = this.header.size;
    offset = TarHeader.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN);
    offset = TarHeader.getLongOctalBytes(this.header.modTime, outbuf, offset, TarHeader.MODTIMELEN);
    int csOffset = offset;
    for (int c = 0; c < TarHeader.CHKSUMLEN; ++c)
      outbuf[offset++] = (byte) ' ';
    outbuf[offset++] = this.header.linkFlag;
    offset = TarHeader.getNameBytes(this.header.linkName, outbuf, offset, TarHeader.NAMELEN);
    offset = TarHeader.getNameBytes(this.header.magic, outbuf, offset, TarHeader.MAGICLEN);
    offset = TarHeader.getNameBytes(this.header.userName, outbuf, offset, TarHeader.UNAMELEN);
    offset = TarHeader.getNameBytes(this.header.groupName, outbuf, offset, TarHeader.GNAMELEN);
    offset = TarHeader.getOctalBytes(this.header.devMajor, outbuf, offset, TarHeader.DEVLEN);
    offset = TarHeader.getOctalBytes(this.header.devMinor, outbuf, offset, TarHeader.DEVLEN);
    for (; offset < outbuf.length;)
      outbuf[offset++] = 0;
    long checkSum = this.computeCheckSum(outbuf);
    TarHeader.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN);
  }
  /**
   * Parse an entry's TarHeader information from a header buffer.
   * 
   * @param hdr
   *          The TarHeader to fill in from the buffer information.
   * @param header
   *          The tar entry header buffer to get information from.
   */
  public void parseTarHeader(TarHeader hdr, byte[] header) throws InvalidHeaderException {
    int offset = 0;
    hdr.name = TarHeader.parseName(header, offset, TarHeader.NAMELEN);
    offset += TarHeader.NAMELEN;
    hdr.mode = (int) TarHeader.parseOctal(header, offset, TarHeader.MODELEN);
    offset += TarHeader.MODELEN;
    hdr.userId = (int) TarHeader.parseOctal(header, offset, TarHeader.UIDLEN);
    offset += TarHeader.UIDLEN;
    hdr.groupId = (int) TarHeader.parseOctal(header, offset, TarHeader.GIDLEN);
    offset += TarHeader.GIDLEN;
    hdr.size = TarHeader.parseOctal(header, offset, TarHeader.SIZELEN);
    offset += TarHeader.SIZELEN;
    hdr.modTime = TarHeader.parseOctal(header, offset, TarHeader.MODTIMELEN);
    offset += TarHeader.MODTIMELEN;
    hdr.checkSum = (int) TarHeader.parseOctal(header, offset, TarHeader.CHKSUMLEN);
    offset += TarHeader.CHKSUMLEN;
    hdr.linkFlag = header[offset++];
    hdr.linkName = TarHeader.parseName(header, offset, TarHeader.NAMELEN);
    offset += TarHeader.NAMELEN;
    hdr.magic = TarHeader.parseName(header, offset, TarHeader.MAGICLEN);
    offset += TarHeader.MAGICLEN;
    hdr.userName = TarHeader.parseName(header, offset, TarHeader.UNAMELEN);
    offset += TarHeader.UNAMELEN;
    hdr.groupName = TarHeader.parseName(header, offset, TarHeader.GNAMELEN);
    offset += TarHeader.GNAMELEN;
    hdr.devMajor = (int) TarHeader.parseOctal(header, offset, TarHeader.DEVLEN);
    offset += TarHeader.DEVLEN;
    hdr.devMinor = (int) TarHeader.parseOctal(header, offset, TarHeader.DEVLEN);
  }
  /**
   * Fill in a TarHeader given only the entry's name.
   * 
   * @param hdr
   *          The TarHeader to fill in.
   * @param name
   *          The tar entry name.
   */
  public void nameTarHeader(TarHeader hdr, String name) {
    boolean isDir = name.endsWith("/");
    hdr.checkSum = 0;
    hdr.devMajor = 0;
    hdr.devMinor = 0;
    hdr.name = new StringBuffer(name);
    hdr.mode = isDir ? 040755 : 0100644;
    hdr.userId = 0;
    hdr.groupId = 0;
    hdr.size = 0;
    hdr.checkSum = 0;
    hdr.modTime = (new java.util.Date()).getTime() / 1000;
    hdr.linkFlag = isDir ? TarHeader.LF_DIR : TarHeader.LF_NORMAL;
    hdr.linkName = new StringBuffer("");
    hdr.userName = new StringBuffer("");
    hdr.groupName = new StringBuffer("");
    hdr.devMajor = 0;
    hdr.devMinor = 0;
  }
}
/**
 * The TarBuffer class implements the tar archive concept of a buffered input
 * stream. This concept goes back to the days of blocked tape drives and special
 * io devices. In the Java universe, the only real function that this class
 * performs is to ensure that files have the correct "block" size, or other tars
 * will complain.
 * 


 * You should never have a need to access this class directly. TarBuffers are
 * created by Tar IO Streams.
 * 
 * @version $Revision: 12504 $
 * @author Timothy Gerard Endres,  *         href="mailto:time@gjt.org">time@trustice.com.
 * @see TarArchive
 */
class TarBuffer extends Object {
  public static final int DEFAULT_RCDSIZE = (512);
  public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20);
  private InputStream inStream;
  private OutputStream outStream;
  private byte[] blockBuffer;
  private int currBlkIdx;
  private int currRecIdx;
  private int blockSize;
  private int recordSize;
  private int recsPerBlock;
  private boolean debug;
  public TarBuffer(InputStream inStream) {
    this(inStream, TarBuffer.DEFAULT_BLKSIZE);
  }
  public TarBuffer(InputStream inStream, int blockSize) {
    this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
  }
  public TarBuffer(InputStream inStream, int blockSize, int recordSize) {
    this.inStream = inStream;
    this.outStream = null;
    this.initialize(blockSize, recordSize);
  }
  public TarBuffer(OutputStream outStream) {
    this(outStream, TarBuffer.DEFAULT_BLKSIZE);
  }
  public TarBuffer(OutputStream outStream, int blockSize) {
    this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);
  }
  public TarBuffer(OutputStream outStream, int blockSize, int recordSize) {
    this.inStream = null;
    this.outStream = outStream;
    this.initialize(blockSize, recordSize);
  }
  /**
   * Initialization common to all constructors.
   */
  private void initialize(int blockSize, int recordSize) {
    this.debug = false;
    this.blockSize = blockSize;
    this.recordSize = recordSize;
    this.recsPerBlock = (this.blockSize / this.recordSize);
    this.blockBuffer = new byte[this.blockSize];
    if (this.inStream != null) {
      this.currBlkIdx = -1;
      this.currRecIdx = this.recsPerBlock;
    } else {
      this.currBlkIdx = 0;
      this.currRecIdx = 0;
    }
  }
  /**
   * Get the TAR Buffer's block size. Blocks consist of multiple records.
   */
  public int getBlockSize() {
    return this.blockSize;
  }
  /**
   * Get the TAR Buffer's record size.
   */
  public int getRecordSize() {
    return this.recordSize;
  }
  /**
   * Set the debugging flag for the buffer.
   * 
   * @param debug
   *          If true, print debugging output.
   */
  public void setDebug(boolean debug) {
    this.debug = debug;
  }
  /**
   * Determine if an archive record indicate End of Archive. End of archive is
   * indicated by a record that consists entirely of null bytes.
   * 
   * @param record
   *          The record data to check.
   */
  public boolean isEOFRecord(byte[] record) {
    for (int i = 0, sz = this.getRecordSize(); i < sz; ++i)
      if (record[i] != 0)
        return false;
    return true;
  }
  /**
   * Skip over a record on the input stream.
   */
  public void skipRecord() throws IOException {
    if (this.debug) {
      System.err
          .println("SkipRecord: recIdx = " + this.currRecIdx + " blkIdx = " + this.currBlkIdx);
    }
    if (this.inStream == null)
      throw new IOException("reading (via skip) from an output buffer");
    if (this.currRecIdx >= this.recsPerBlock) {
      if (!this.readBlock())
        return; // UNDONE
    }
    this.currRecIdx++;
  }
  /**
   * Read a record from the input stream and return the data.
   * 
   * @return The record data.
   */
  public byte[] readRecord() throws IOException {
    if (this.debug) {
      System.err
          .println("ReadRecord: recIdx = " + this.currRecIdx + " blkIdx = " + this.currBlkIdx);
    }
    if (this.inStream == null)
      throw new IOException("reading from an output buffer");
    if (this.currRecIdx >= this.recsPerBlock) {
      if (!this.readBlock())
        return null;
    }
    byte[] result = new byte[this.recordSize];
    System.arraycopy(this.blockBuffer, (this.currRecIdx * this.recordSize), result, 0,
        this.recordSize);
    this.currRecIdx++;
    return result;
  }
  /**
   * @return false if End-Of-File, else true
   */
  private boolean readBlock() throws IOException {
    if (this.debug) {
      System.err.println("ReadBlock: blkIdx = " + this.currBlkIdx);
    }
    if (this.inStream == null)
      throw new IOException("reading from an output buffer");
    this.currRecIdx = 0;
    int offset = 0;
    int bytesNeeded = this.blockSize;
    for (; bytesNeeded > 0;) {
      long numBytes = this.inStream.read(this.blockBuffer, offset, bytesNeeded);
      //
      // NOTE
      // We have fit EOF, and the block is not full!
      //
      // This is a broken archive. It does not follow the standard
      // blocking algorithm. However, because we are generous, and
      // it requires little effort, we will simply ignore the error
      // and continue as if the entire block were read. This does
      // not appear to break anything upstream. We used to return
      // false in this case.
      //
      // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.
      //
      if (numBytes == -1)
        break;
      offset += numBytes;
      bytesNeeded -= numBytes;
      if (numBytes != this.blockSize) {
        if (this.debug) {
          System.err.println("ReadBlock: INCOMPLETE READ " + numBytes + " of " + this.blockSize
              + " bytes read.");
        }
      }
    }
    this.currBlkIdx++;
    return true;
  }
  /**
   * Get the current block number, zero based.
   * 
   * @return The current zero based block number.
   */
  public int getCurrentBlockNum() {
    return this.currBlkIdx;
  }
  /**
   * Get the current record number, within the current block, zero based. Thus,
   * current offset = (currentBlockNum * recsPerBlk) + currentRecNum.
   * 
   * @return The current zero based record number.
   */
  public int getCurrentRecordNum() {
    return this.currRecIdx - 1;
  }
  /**
   * Write an archive record to the archive.
   * 
   * @param record
   *          The record data to write to the archive.
   */
  public void writeRecord(byte[] record) throws IOException {
    if (this.debug) {
      System.err.println("WriteRecord: recIdx = " + this.currRecIdx + " blkIdx = "
          + this.currBlkIdx);
    }
    if (this.outStream == null)
      throw new IOException("writing to an input buffer");
    if (record.length != this.recordSize)
      throw new IOException("record to write has length '" + record.length
          + "' which is not the record size of '" + this.recordSize + "'");
    if (this.currRecIdx >= this.recsPerBlock) {
      this.writeBlock();
    }
    System.arraycopy(record, 0, this.blockBuffer, (this.currRecIdx * this.recordSize),
        this.recordSize);
    this.currRecIdx++;
  }
  /**
   * Write an archive record to the archive, where the record may be inside of a
   * larger array buffer. The buffer must be "offset plus record size" long.
   * 
   * @param buf
   *          The buffer containing the record data to write.
   * @param offset
   *          The offset of the record data within buf.
   */
  public void writeRecord(byte[] buf, int offset) throws IOException {
    if (this.debug) {
      System.err.println("WriteRecord: recIdx = " + this.currRecIdx + " blkIdx = "
          + this.currBlkIdx);
    }
    if (this.outStream == null)
      throw new IOException("writing to an input buffer");
    if ((offset + this.recordSize) > buf.length)
      throw new IOException("record has length '" + buf.length + "' with offset '" + offset
          + "' which is less than the record size of '" + this.recordSize + "'");
    if (this.currRecIdx >= this.recsPerBlock) {
      this.writeBlock();
    }
    System.arraycopy(buf, offset, this.blockBuffer, (this.currRecIdx * this.recordSize),
        this.recordSize);
    this.currRecIdx++;
  }
  /**
   * Write a TarBuffer block to the archive.
   */
  private void writeBlock() throws IOException {
    if (this.debug) {
      System.err.println("WriteBlock: blkIdx = " + this.currBlkIdx);
    }
    if (this.outStream == null)
      throw new IOException("writing to an input buffer");
    this.outStream.write(this.blockBuffer, 0, this.blockSize);
    this.outStream.flush();
    this.currRecIdx = 0;
    this.currBlkIdx++;
  }
  /**
   * Flush the current data block if it has any data in it.
   */
  private void flushBlock() throws IOException {
    if (this.debug) {
      System.err.println("TarBuffer.flushBlock() called.");
    }
    if (this.outStream == null)
      throw new IOException("writing to an input buffer");
    if (this.currRecIdx > 0) {
      this.writeBlock();
    }
  }
  /**
   * Close the TarBuffer. If this is an output buffer, also flush the current
   * block before closing.
   */
  public void close() throws IOException {
    if (this.debug) {
      System.err.println("TarBuffer.closeBuffer().");
    }
    if (this.outStream != null) {
      this.flushBlock();
      if (this.outStream != System.out && this.outStream != System.err) {
        this.outStream.close();
        this.outStream = null;
      }
    } else if (this.inStream != null) {
      if (this.inStream != System.in) {
        this.inStream.close();
        this.inStream = null;
      }
    }
  }
}
class InvalidHeaderException extends IOException {
  public InvalidHeaderException() {
    super();
  }
  public InvalidHeaderException(String msg) {
    super(msg);
  }
}