TarInputStream.java Source code

Java tutorial

Introduction

Here is the source code for TarInputStream.java

Source

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

}