TarOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for TarOutputStream.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.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;

/**
 * The TarOutputStream writes a UNIX tar archive as an OutputStream.
 * Methods are provided to put entries, and then write their contents
 * by writing to this stream using write().
 *
 *
 * @version $Revision: 12504 $
 * @author Timothy Gerard Endres,
 *  <a href="mailto:time@gjt.org">time@trustice.com</a>.
 * @see TarBuffer
 * @see TarHeader
 * @see TarEntry
 */

public class TarOutputStream extends FilterOutputStream {
    protected boolean debug;
    protected int currSize;
    protected int currBytes;
    protected byte[] oneBuf;
    protected byte[] recordBuf;
    protected int assemLen;
    protected byte[] assemBuf;
    protected TarBuffer buffer;

    public TarOutputStream(OutputStream os) {
        this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);
    }

    public TarOutputStream(OutputStream os, int blockSize) {
        this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE);
    }

    public TarOutputStream(OutputStream os, int blockSize, int recordSize) {
        super(os);

        this.buffer = new TarBuffer(os, blockSize, recordSize);

        this.debug = false;
        this.assemLen = 0;
        this.assemBuf = new byte[recordSize];
        this.recordBuf = new byte[recordSize];
        this.oneBuf = new byte[1];
    }

    /**
     * Sets the debugging flag.
     *
     * @param debugF True to turn on debugging.
     */
    public void setDebug(boolean debugF) {
        this.debug = debugF;
    }

    /**
     * Sets the debugging flag in this stream's TarBuffer.
     *
     * @param debugF True to turn on debugging.
     */
    public void setBufferDebug(boolean debug) {
        this.buffer.setDebug(debug);
    }

    /**
     * Ends the TAR archive without closing the underlying OutputStream.
     * The result is that the EOF record of nulls is written.
     */

    public void finish() throws IOException {
        this.writeEOFRecord();
    }

    /**
     * Ends the TAR archive and closes the underlying OutputStream.
     * This means that finish() is called followed by calling the
     * TarBuffer's close().
     */

    public void close() throws IOException {
        this.finish();
        this.buffer.close();
    }

    /**
     * Get the record size being used by this stream's TarBuffer.
     *
     * @return The TarBuffer record size.
     */
    public int getRecordSize() {
        return this.buffer.getRecordSize();
    }

    /**
     * Put an entry on the output stream. This writes the entry's
     * header record and positions the output stream for writing
     * the contents of the entry. Once this method is called, the
     * stream is ready for calls to write() to write the entry's
     * contents. Once the contents are written, closeEntry()
     * <B>MUST</B> be called to ensure that all buffered data
     * is completely written to the output stream.
     *
     * @param entry The TarEntry to be written to the archive.
     */
    public void putNextEntry(TarEntry entry) throws IOException {
        if (entry.getHeader().name.length() > TarHeader.NAMELEN)
            throw new InvalidHeaderException(
                    "file name '" + entry.getHeader().name + "' is too long ( > " + TarHeader.NAMELEN + " bytes )");

        entry.writeEntryHeader(this.recordBuf);
        this.buffer.writeRecord(this.recordBuf);

        this.currBytes = 0;

        if (entry.isDirectory())
            this.currSize = 0;
        else
            this.currSize = (int) entry.getSize();
    }

    /**
     * Close an entry. This method MUST be called for all file
     * entries that contain data. The reason is that we must
     * buffer data written to the stream in order to satisfy
     * the buffer's record based writes. Thus, there may be
     * data fragments still being assembled that must be written
     * to the output stream before this entry is closed and the
     * next entry written.
     */
    public void closeEntry() throws IOException {
        if (this.assemLen > 0) {
            for (int i = this.assemLen; i < this.assemBuf.length; ++i)
                this.assemBuf[i] = 0;

            this.buffer.writeRecord(this.assemBuf);

            this.currBytes += this.assemLen;
            this.assemLen = 0;
        }

        if (this.currBytes < this.currSize)
            throw new IOException("entry closed at '" + this.currBytes + "' before the '" + this.currSize
                    + "' bytes specified in the header were written");
    }

    /**
     * Writes a byte to the current tar archive entry.
     *
     * This method simply calls read( byte[], int, int ).
     *
     * @param b The byte written.
     */
    public void write(int b) throws IOException {
        this.oneBuf[0] = (byte) b;
        this.write(this.oneBuf, 0, 1);
    }

    /**
     * Writes bytes to the current tar archive entry.
     *
     * This method simply calls read( byte[], int, int ).
     *
     * @param wBuf The buffer to write to the archive.
     * @return The number of bytes read, or -1 at EOF.
     */
    public void write(byte[] wBuf) throws IOException {
        this.write(wBuf, 0, wBuf.length);
    }

    /**
     * Writes bytes to the current tar archive entry. This method
     * is aware of the current entry and will throw an exception if
     * you attempt to write bytes past the length specified for the
     * current entry. The method is also (painfully) aware of the
     * record buffering required by TarBuffer, and manages buffers
     * that are not a multiple of recordsize in length, including
     * assembling records from small buffers.
     *
     * This method simply calls read( byte[], int, int ).
     *
     * @param wBuf The buffer to write to the archive.
     * @param wOffset The offset in the buffer from which to get bytes.
     * @param numToWrite The number of bytes to write.
     */
    public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException {
        if ((this.currBytes + numToWrite) > this.currSize)
            throw new IOException("request to write '" + numToWrite + "' bytes exceeds size in header of '"
                    + this.currSize + "' bytes");

        //
        // We have to deal with assembly!!!
        // The programmer can be writing little 32 byte chunks for all
        // we know, and we must assemble complete records for writing.
        // REVIEW Maybe this should be in TarBuffer? Could that help to
        //        eliminate some of the buffer copying.
        //
        if (this.assemLen > 0) {
            if ((this.assemLen + numToWrite) >= this.recordBuf.length) {
                int aLen = this.recordBuf.length - this.assemLen;

                System.arraycopy(this.assemBuf, 0, this.recordBuf, 0, this.assemLen);

                System.arraycopy(wBuf, wOffset, this.recordBuf, this.assemLen, aLen);

                this.buffer.writeRecord(this.recordBuf);

                this.currBytes += this.recordBuf.length;

                wOffset += aLen;
                numToWrite -= aLen;
                this.assemLen = 0;
            } else // ( (this.assemLen + numToWrite ) < this.recordBuf.length )
            {
                System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen, numToWrite);
                wOffset += numToWrite;
                this.assemLen += numToWrite;
                numToWrite -= numToWrite;
            }
        }

        //
        // When we get here we have EITHER:
        //   o An empty "assemble" buffer.
        //   o No bytes to write (numToWrite == 0)
        //

        for (; numToWrite > 0;) {
            if (numToWrite < this.recordBuf.length) {
                System.arraycopy(wBuf, wOffset, this.assemBuf, this.assemLen, numToWrite);
                this.assemLen += numToWrite;
                break;
            }

            this.buffer.writeRecord(wBuf, wOffset);

            int num = this.recordBuf.length;
            this.currBytes += num;
            numToWrite -= num;
            wOffset += num;
        }
    }

    /**
     * Write an EOF (end of archive) record to the tar archive.
     * An EOF record consists of a record of all zeros.
     */
    private void writeEOFRecord() throws IOException {
        for (int i = 0; i < this.recordBuf.length; ++i)
            this.recordBuf[i] = 0;
        this.buffer.writeRecord(this.recordBuf);
    }

}

/*
 * * Authored by Timothy Gerard Endres * <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);
    }

}