TarInputStream reads a UNIX tar archive as an InputStream : Zip Tar File « File Input Output « Java






TarInputStream reads a UNIX tar archive as an InputStream

       

/*
 ** 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. 
 ** 
 */

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;

/**
 * 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);
    }
  }

}

/*
 * * 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. *
 */

/**
 * 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 * <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. *
 */

/**
 * 
 * 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.
 * <p>
 * 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.
 * <p>
 * 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.
 * <p>
 * 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.
 * 
 * <p>
 * The C structure for a Tar Entry's header is:
 * 
 * <pre>
 *  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;
 * </pre>
 * 
 * @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.
 * <p>
 * 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, <a
 *         href="mailto:time@gjt.org">time@trustice.com</a>.
 * @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);
  }

}

   
    
    
    
    
    
    
  








Related examples in the same category

1.Extract contents of a zip file
2.List the contents of a zip file
3.Read entries in a zip / compressed file
4.Decompress a zip file using ZipInputStream
5.Decompress a zip file using ZipFile
6.Create checksum for a zip file
7.Read a zip file checksum value
8.Create a zip file with java.util.zip package
9.Extract file/files from a zip file
10.Read files within a zip file
11.Retrieve a compressed file from a ZIP file
12.Retrieve the contents of a ZIP file
13.Making a zip file of directory including its subdirectories recursively
14.Displaying contents of a compressed zip file
15.Compress a Byte Array
16.Decompress a Byte Array
17.Read zip file
18.Write Zip file
19.The java.util.zip package can be used to create a checksum.
20.Read the content of a zip file ZipFile
21.List the entries of a zip file
22.Compressing Streams: Zipper, Java example
23.Compressing Streams: Parity Checksum
24.Compressing Streams: File Summer
25.Create a simple ZIP File: not retain any directory path information about the files.
26.Decompress a ZIP file.
27.Decompressing a Byte Array
28.Zip unzip byte array
29.Creating a ZIP File
30.Listing the Contents of a ZIP File
31.Retrieving a Compressed File from a ZIP File
32.Calculating the Checksum of a Byte Array (Compute Adler-32 checksum)
33.Compute CRC-32 checksum
34.Calculating the Checksum of a File
35.Compress string(byte array) by Deflater
36.Use Java code to zip a folder
37.Uses Zip compression to compress any number of files given on the command line
38.Load zip file and scan zip file
39.Reading the Contents of a ZIP File
40.UnZip -- print or unzip a JAR or PKZIP file using java.util.zip
41.Tape Archive Lister: Tar file
42.Compressing a Byte Array
43.Tar file stream
44.Tar file and untar file
45.bzip source code
46.Unpack an archive from a URL
47.Compare two zip files
48.Determine whether a file is a ZIP File.
49.Zip up a directory
50.Check sum for a path
51.Check sum for an InputStream
52.Extract zip file to destination folder
53.Return the first directory of this archive. This is needed to determine the plugin directory
54.Makes a zip file named xmlFileName from xmlURL at path
55.Unzipps a zip file placed at zipURL to path
56.A single checksum calculation for multiple files
57.Put file To Zip File
58.Provides both writing and reading from a file which is transparently compressed in Zip
59.TarOutputStream writes a UNIX tar archive as an OutputStream
60.Package files utility
61.Zip Compare
62.Unpack a segment from a zip
63.Unpack a zip file
64.Unzip file to a directory
65.Zip a list of file into one zip file.
66.Validate that an archive contains a named entry
67.A frame with a text area to show the contents of a file inside a ZIP archive
68.Compress object and decompress
69.Util for extracting *.jar, *.war and *.zip archives.