Mega Code Archive

 
Categories / Java / File Input Output
 

TarInputStream reads a UNIX tar archive as an InputStream

/*  ** Authored by Timothy Gerard Endres  ** <mailto:time@gjt.org>  <http://www.trustice.com>  **   ** This work has been placed into the public domain.  ** You may use this work in any way and for any purpose you wish.  **  ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,  ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR  ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY  ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR  ** REDISTRIBUTION OF THIS SOFTWARE.   **   */ import java.io.File; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; /**  * The TarInputStream reads a UNIX tar archive as an InputStream. methods are  * provided to position at each successive entry in the archive, and the read  * each entry as a normal input stream using read().  *   * @version $Revision: 12504 $  * @author Timothy Gerard Endres, <a  *         href="mailto:time@gjt.org">time@trustice.com</a>.  * @see TarBuffer  * @see TarHeader  * @see TarEntry  */ public class TarInputStream extends FilterInputStream {   protected boolean debug;   protected boolean hasHitEOF;   protected int entrySize;   protected int entryOffset;   protected byte[] oneBuf;   protected byte[] readBuf;   protected TarBuffer buffer;   protected TarEntry currEntry;   protected EntryFactory eFactory;   public TarInputStream(InputStream is) {     this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE);   }   public TarInputStream(InputStream is, int blockSize) {     this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE);   }   public TarInputStream(InputStream is, int blockSize, int recordSize) {     super(is);     this.buffer = new TarBuffer(is, blockSize, recordSize);     this.readBuf = null;     this.oneBuf = new byte[1];     this.debug = false;     this.hasHitEOF = false;     this.eFactory = null;   }   /**    * Sets the debugging flag.    *     * @param debugF    *          True to turn on debugging.    */   public void setDebug(boolean debugF) {     this.debug = debugF;   }   /**    * Sets the debugging flag.    *     * @param debugF    *          True to turn on debugging.    */   public void setEntryFactory(EntryFactory factory) {     this.eFactory = factory;   }   /**    * Sets the debugging flag in this stream's TarBuffer.    *     * @param debugF    *          True to turn on debugging.    */   public void setBufferDebug(boolean debug) {     this.buffer.setDebug(debug);   }   /**    * Closes this stream. Calls the TarBuffer's close() method.    */   public void close() throws IOException {     this.buffer.close();   }   /**    * Get the record size being used by this stream's TarBuffer.    *     * @return The TarBuffer record size.    */   public int getRecordSize() {     return this.buffer.getRecordSize();   }   /**    * Get the available data that can be read from the current entry in the    * archive. This does not indicate how much data is left in the entire    * archive, only in the current entry. This value is determined from the    * entry's size header field and the amount of data already read from the    * current entry.    *     *     * @return The number of available bytes for the current entry.    */   public int available() throws IOException {     return this.entrySize - this.entryOffset;   }   /**    * Skip bytes in the input buffer. This skips bytes in the current entry's    * data, not the entire archive, and will stop at the end of the current    * entry's data if the number to skip extends beyond that point.    *     * @param numToSkip    *          The number of bytes to skip.    */   public void skip(int numToSkip) throws IOException {     // REVIEW     // This is horribly inefficient, but it ensures that we     // properly skip over bytes via the TarBuffer...     //     byte[] skipBuf = new byte[8 * 1024];     for (int num = numToSkip; num > 0;) {       int numRead = this.read(skipBuf, 0, (num > skipBuf.length ? skipBuf.length : num));       if (numRead == -1)         break;       num -= numRead;     }   }   /**    * Since we do not support marking just yet, we return false.    *     * @return False.    */   public boolean markSupported() {     return false;   }   /**    * Since we do not support marking just yet, we do nothing.    *     * @param markLimit    *          The limit to mark.    */   public void mark(int markLimit) {   }   /**    * Since we do not support marking just yet, we do nothing.    */   public void reset() {   }   /**    * Get the next entry in this tar archive. This will skip over any remaining    * data in the current entry, if there is one, and place the input stream at    * the header of the next entry, and read the header and instantiate a new    * TarEntry from the header bytes and return that entry. If there are no more    * entries in the archive, null will be returned to indicate that the end of    * the archive has been reached.    *     * @return The next TarEntry in the archive, or null.    */   public TarEntry getNextEntry() throws IOException {     if (this.hasHitEOF)       return null;     if (this.currEntry != null) {       int numToSkip = this.entrySize - this.entryOffset;       if (this.debug)         System.err.println("TarInputStream: SKIP currENTRY '" + this.currEntry.getName() + "' SZ "             + this.entrySize + " OFF " + this.entryOffset + "  skipping " + numToSkip + " bytes");       if (numToSkip > 0) {         this.skip(numToSkip);       }       this.readBuf = null;     }     byte[] headerBuf = this.buffer.readRecord();     if (headerBuf == null) {       if (this.debug) {         System.err.println("READ NULL RECORD");       }       this.hasHitEOF = true;     } else if (this.buffer.isEOFRecord(headerBuf)) {       if (this.debug) {         System.err.println("READ EOF RECORD");       }       this.hasHitEOF = true;     }     if (this.hasHitEOF) {       this.currEntry = null;     } else {       try {         if (this.eFactory == null) {           this.currEntry = new TarEntry(headerBuf);         } else {           this.currEntry = this.eFactory.createEntry(headerBuf);         }         if (!(headerBuf[257] == 'u' && headerBuf[258] == 's' && headerBuf[259] == 't'             && headerBuf[260] == 'a' && headerBuf[261] == 'r')) {           throw new InvalidHeaderException("header magic is not 'ustar', but '" + headerBuf[257]               + headerBuf[258] + headerBuf[259] + headerBuf[260] + headerBuf[261] + "', or (dec) "               + ((int) headerBuf[257]) + ", " + ((int) headerBuf[258]) + ", "               + ((int) headerBuf[259]) + ", " + ((int) headerBuf[260]) + ", "               + ((int) headerBuf[261]));         }         if (this.debug)           System.err.println("TarInputStream: SET CURRENTRY '" + this.currEntry.getName()               + "' size = " + this.currEntry.getSize());         this.entryOffset = 0;         // REVIEW How do we resolve this discrepancy?!         this.entrySize = (int) this.currEntry.getSize();       } catch (InvalidHeaderException ex) {         this.entrySize = 0;         this.entryOffset = 0;         this.currEntry = null;         throw new InvalidHeaderException("bad header in block " + this.buffer.getCurrentBlockNum()             + " record " + this.buffer.getCurrentRecordNum() + ", " + ex.getMessage());       }     }     return this.currEntry;   }   /**    * Reads a byte from the current tar archive entry.    *     * This method simply calls read( byte[], int, int ).    *     * @return The byte read, or -1 at EOF.    */   public int read() throws IOException {     int num = this.read(this.oneBuf, 0, 1);     if (num == -1)       return num;     else       return this.oneBuf[0];   }   /**    * Reads bytes from the current tar archive entry.    *     * This method simply calls read( byte[], int, int ).    *     * @param buf    *          The buffer into which to place bytes read.    * @return The number of bytes read, or -1 at EOF.    */   public int read(byte[] buf) throws IOException {     return this.read(buf, 0, buf.length);   }   /**    * Reads bytes from the current tar archive entry.    *     * This method is aware of the boundaries of the current entry in the archive    * and will deal with them as if they were this stream's start and EOF.    *     * @param buf    *          The buffer into which to place bytes read.    * @param offset    *          The offset at which to place bytes read.    * @param numToRead    *          The number of bytes to read.    * @return The number of bytes read, or -1 at EOF.    */   public int read(byte[] buf, int offset, int numToRead) throws IOException {     int totalRead = 0;     if (this.entryOffset >= this.entrySize)       return -1;     if ((numToRead + this.entryOffset) > this.entrySize) {       numToRead = (this.entrySize - this.entryOffset);     }     if (this.readBuf != null) {       int sz = (numToRead > this.readBuf.length) ? this.readBuf.length : numToRead;       System.arraycopy(this.readBuf, 0, buf, offset, sz);       if (sz >= this.readBuf.length) {         this.readBuf = null;       } else {         int newLen = this.readBuf.length - sz;         byte[] newBuf = new byte[newLen];         System.arraycopy(this.readBuf, sz, newBuf, 0, newLen);         this.readBuf = newBuf;       }       totalRead += sz;       numToRead -= sz;       offset += sz;     }     for (; numToRead > 0;) {       byte[] rec = this.buffer.readRecord();       if (rec == null) {         // Unexpected EOF!         throw new IOException("unexpected EOF with " + numToRead + " bytes unread");       }       int sz = numToRead;       int recLen = rec.length;       if (recLen > sz) {         System.arraycopy(rec, 0, buf, offset, sz);         this.readBuf = new byte[recLen - sz];         System.arraycopy(rec, sz, this.readBuf, 0, recLen - sz);       } else {         sz = recLen;         System.arraycopy(rec, 0, buf, offset, recLen);       }       totalRead += sz;       numToRead -= sz;       offset += sz;     }     this.entryOffset += totalRead;     return totalRead;   }   /**    * Copies the contents of the current tar archive entry directly into an    * output stream.    *     * @param out    *          The OutputStream into which to write the entry's data.    */   public void copyEntryContents(OutputStream out) throws IOException {     byte[] buf = new byte[32 * 1024];     for (;;) {       int numRead = this.read(buf, 0, buf.length);       if (numRead == -1)         break;       out.write(buf, 0, numRead);     }   }   /**    * This interface is provided, with the method setEntryFactory(), to allow the    * programmer to have their own TarEntry subclass instantiated for the entries    * return from getNextEntry().    */   public interface EntryFactory {     public TarEntry createEntry(String name);     public TarEntry createEntry(File path) throws InvalidHeaderException;     public TarEntry createEntry(byte[] headerBuf) throws InvalidHeaderException;   }   public class EntryAdapter implements EntryFactory {     public TarEntry createEntry(String name) {       return new TarEntry(name);     }     public TarEntry createEntry(File path) throws InvalidHeaderException {       return new TarEntry(path);     }     public TarEntry createEntry(byte[] headerBuf) throws InvalidHeaderException {       return new TarEntry(headerBuf);     }   } } /*  * * Authored by Timothy Gerard Endres * <mailto:time@gjt.org>  * <http://www.trustice.com> * * This work has been placed into the public  * domain. * You may use this work in any way and for any purpose you wish. * *  * THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, * NOT EVEN THE  * IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR * OF THIS SOFTWARE, ASSUMES  * _NO_ RESPONSIBILITY FOR ANY * CONSEQUENCE RESULTING FROM THE USE,  * MODIFICATION, OR * REDISTRIBUTION OF THIS SOFTWARE. *  */ /**  * This class encapsulates the Tar Entry Header used in Tar Archives. The class  * also holds a number of tar constants, used mostly in headers.  */ class TarHeader extends Object {   /**    * The length of the name field in a header buffer.    */   public static final int NAMELEN = 100;   /**    * The length of the mode field in a header buffer.    */   public static final int MODELEN = 8;   /**    * The length of the user id field in a header buffer.    */   public static final int UIDLEN = 8;   /**    * The length of the group id field in a header buffer.    */   public static final int GIDLEN = 8;   /**    * The length of the checksum field in a header buffer.    */   public static final int CHKSUMLEN = 8;   /**    * The length of the size field in a header buffer.    */   public static final int SIZELEN = 12;   /**    * The length of the magic field in a header buffer.    */   public static final int MAGICLEN = 8;   /**    * The length of the modification time field in a header buffer.    */   public static final int MODTIMELEN = 12;   /**    * The length of the user name field in a header buffer.    */   public static final int UNAMELEN = 32;   /**    * The length of the group name field in a header buffer.    */   public static final int GNAMELEN = 32;   /**    * The length of the devices field in a header buffer.    */   public static final int DEVLEN = 8;   /**    * LF_ constants represent the "link flag" of an entry, or more commonly, the    * "entry type". This is the "old way" of indicating a normal file.    */   public static final byte LF_OLDNORM = 0;   /**    * Normal file type.    */   public static final byte LF_NORMAL = (byte) '0';   /**    * Link file type.    */   public static final byte LF_LINK = (byte) '1';   /**    * Symbolic link file type.    */   public static final byte LF_SYMLINK = (byte) '2';   /**    * Character device file type.    */   public static final byte LF_CHR = (byte) '3';   /**    * Block device file type.    */   public static final byte LF_BLK = (byte) '4';   /**    * Directory file type.    */   public static final byte LF_DIR = (byte) '5';   /**    * FIFO (pipe) file type.    */   public static final byte LF_FIFO = (byte) '6';   /**    * Contiguous file type.    */   public static final byte LF_CONTIG = (byte) '7';   /**    * The magic tag representing a POSIX tar archive.    */   public static final String TMAGIC = "ustar";   /**    * The magic tag representing a GNU tar archive.    */   public static final String GNU_TMAGIC = "ustar  ";   /**    * The entry's name.    */   public StringBuffer name;   /**    * The entry's permission mode.    */   public int mode;   /**    * The entry's user id.    */   public int userId;   /**    * The entry's group id.    */   public int groupId;   /**    * The entry's size.    */   public long size;   /**    * The entry's modification time.    */   public long modTime;   /**    * The entry's checksum.    */   public int checkSum;   /**    * The entry's link flag.    */   public byte linkFlag;   /**    * The entry's link name.    */   public StringBuffer linkName;   /**    * The entry's magic tag.    */   public StringBuffer magic;   /**    * The entry's user name.    */   public StringBuffer userName;   /**    * The entry's group name.    */   public StringBuffer groupName;   /**    * The entry's major device number.    */   public int devMajor;   /**    * The entry's minor device number.    */   public int devMinor;   public TarHeader() {     this.magic = new StringBuffer(TarHeader.TMAGIC);     this.name = new StringBuffer();     this.linkName = new StringBuffer();     String user = System.getProperty("user.name", "");     if (user.length() > 31)       user = user.substring(0, 31);     this.userId = 0;     this.groupId = 0;     this.userName = new StringBuffer(user);     this.groupName = new StringBuffer("");   }   /**    * TarHeaders can be cloned.    */   public Object clone() {     TarHeader hdr = null;     try {       hdr = (TarHeader) super.clone();       hdr.name = (this.name == null) ? null : new StringBuffer(this.name.toString());       hdr.mode = this.mode;       hdr.userId = this.userId;       hdr.groupId = this.groupId;       hdr.size = this.size;       hdr.modTime = this.modTime;       hdr.checkSum = this.checkSum;       hdr.linkFlag = this.linkFlag;       hdr.linkName = (this.linkName == null) ? null : new StringBuffer(this.linkName.toString());       hdr.magic = (this.magic == null) ? null : new StringBuffer(this.magic.toString());       hdr.userName = (this.userName == null) ? null : new StringBuffer(this.userName.toString());       hdr.groupName = (this.groupName == null) ? null : new StringBuffer(this.groupName.toString());       hdr.devMajor = this.devMajor;       hdr.devMinor = this.devMinor;     } catch (CloneNotSupportedException ex) {       ex.printStackTrace();     }     return hdr;   }   /**    * Get the name of this entry.    *     * @return Teh entry's name.    */   public String getName() {     return this.name.toString();   }   /**    * Parse an octal string from a header buffer. This is used for the file    * permission mode value.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The long value of the octal string.    */   public static long parseOctal(byte[] header, int offset, int length)       throws InvalidHeaderException {     long result = 0;     boolean stillPadding = true;     int end = offset + length;     for (int i = offset; i < end; ++i) {       if (header[i] == 0)         break;       if (header[i] == (byte) ' ' || header[i] == '0') {         if (stillPadding)           continue;         if (header[i] == (byte) ' ')           break;       }       stillPadding = false;       result = (result << 3) + (header[i] - '0');     }     return result;   }   /**    * Parse an entry name from a header buffer.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The header's entry name.    */   public static StringBuffer parseName(byte[] header, int offset, int length)       throws InvalidHeaderException {     StringBuffer result = new StringBuffer(length);     int end = offset + length;     for (int i = offset; i < end; ++i) {       if (header[i] == 0)         break;       result.append((char) header[i]);     }     return result;   }   /**    * Determine the number of bytes in an entry name.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The number of bytes in a header's entry name.    */   public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) {     int i;     for (i = 0; i < length && i < name.length(); ++i) {       buf[offset + i] = (byte) name.charAt(i);     }     for (; i < length; ++i) {       buf[offset + i] = 0;     }     return offset + length;   }   /**    * Parse an octal integer from a header buffer.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The integer value of the octal bytes.    */   public static int getOctalBytes(long value, byte[] buf, int offset, int length) {     byte[] result = new byte[length];     int idx = length - 1;     buf[offset + idx] = 0;     --idx;     buf[offset + idx] = (byte) ' ';     --idx;     if (value == 0) {       buf[offset + idx] = (byte) '0';       --idx;     } else {       for (long val = value; idx >= 0 && val > 0; --idx) {         buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7));         val = val >> 3;       }     }     for (; idx >= 0; --idx) {       buf[offset + idx] = (byte) ' ';     }     return offset + length;   }   /**    * Parse an octal long integer from a header buffer.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The long value of the octal bytes.    */   public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) {     byte[] temp = new byte[length + 1];     TarHeader.getOctalBytes(value, temp, 0, length + 1);     System.arraycopy(temp, 0, buf, offset, length);     return offset + length;   }   /**    * Parse the checksum octal integer from a header buffer.    *     * @param header    *          The header buffer from which to parse.    * @param offset    *          The offset into the buffer from which to parse.    * @param length    *          The number of header bytes to parse.    * @return The integer value of the entry's checksum.    */   public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) {     TarHeader.getOctalBytes(value, buf, offset, length);     buf[offset + length - 1] = (byte) ' ';     buf[offset + length - 2] = 0;     return offset + length;   } } /*  * * Authored by Timothy Gerard Endres * <mailto:time@gjt.org>  * <http://www.trustice.com> * * This work has been placed into the public  * domain. * You may use this work in any way and for any purpose you wish. * *  * THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, * NOT EVEN THE  * IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR * OF THIS SOFTWARE, ASSUMES  * _NO_ RESPONSIBILITY FOR ANY * CONSEQUENCE RESULTING FROM THE USE,  * MODIFICATION, OR * REDISTRIBUTION OF THIS SOFTWARE. *  */ /**  *   * This class represents an entry in a Tar archive. It consists of the entry's  * header, as well as the entry's File. Entries can be instantiated in one of  * three ways, depending on how they are to be used.  * <p>  * TarEntries that are created from the header bytes read from an archive are  * instantiated with the TarEntry( byte[] ) constructor. These entries will be  * used when extracting from or listing the contents of an archive. These  * entries have their header filled in using the header bytes. They also set the  * File to null, since they reference an archive entry not a file.  * <p>  * TarEntries that are created from Files that are to be written into an archive  * are instantiated with the TarEntry( File ) constructor. These entries have  * their header filled in using the File's information. They also keep a  * reference to the File for convenience when writing entries.  * <p>  * Finally, TarEntries can be constructed from nothing but a name. This allows  * the programmer to construct the entry by hand, for instance when only an  * InputStream is available for writing to the archive, and the header  * information is constructed from other information. In this case the header  * fields are set to defaults and the File is set to null.  *   * <p>  * The C structure for a Tar Entry's header is:  *   * <pre>  *  struct header {  *    char  name[NAMSIZ];  *    char  mode[8];  *    char  uid[8];  *    char  gid[8];  *    char  size[12];  *    char  mtime[12];  *    char  chksum[8];  *    char  linkflag;  *    char  linkname[NAMSIZ];  *    char  magic[8];  *    char  uname[TUNMLEN];  *    char  gname[TGNMLEN];  *    char  devmajor[8];  *    char  devminor[8];  *  } header;  * </pre>  *   * @see TarHeader  *   */ class TarEntry extends Object {   /**    * If this entry represents a File, this references it.    */   protected File file;   /**    * This is the entry's header information.    */   protected TarHeader header;   /**    * Construct an entry with only a name. This allows the programmer to    * construct the entry's header "by hand". File is set to null.    */   public TarEntry(String name) {     this.initialize();     this.nameTarHeader(this.header, name);   }   /**    * Construct an entry for a file. File is set to file, and the header is    * constructed from information from the file.    *     * @param file    *          The file that the entry represents.    */   public TarEntry(File file) throws InvalidHeaderException {     this.initialize();     this.getFileTarHeader(this.header, file);   }   /**    * Construct an entry from an archive's header bytes. File is set to null.    *     * @param headerBuf    *          The header bytes from a tar archive entry.    */   public TarEntry(byte[] headerBuf) throws InvalidHeaderException {     this.initialize();     this.parseTarHeader(this.header, headerBuf);   }   /**    * Initialization code common to all constructors.    */   private void initialize() {     this.file = null;     this.header = new TarHeader();   }   /**    * Determine if the two entries are equal. Equality is determined by the    * header names being equal.    *     * @return it Entry to be checked for equality.    * @return True if the entries are equal.    */   public boolean equals(TarEntry it) {     return this.header.name.toString().equals(it.header.name.toString());   }   /**    * Determine if the given entry is a descendant of this entry. Descendancy is    * determined by the name of the descendant starting with this entry's name.    *     * @param desc    *          Entry to be checked as a descendent of this.    * @return True if entry is a descendant of this.    */   public boolean isDescendent(TarEntry desc) {     return desc.header.name.toString().startsWith(this.header.name.toString());   }   /**    * Get this entry's header.    *     * @return This entry's TarHeader.    */   public TarHeader getHeader() {     return this.header;   }   /**    * Get this entry's name.    *     * @return This entry's name.    */   public String getName() {     return this.header.name.toString();   }   /**    * Set this entry's name.    *     * @param name    *          This entry's new name.    */   public void setName(String name) {     this.header.name = new StringBuffer(name);   }   /**    * Get this entry's user id.    *     * @return This entry's user id.    */   public int getUserId() {     return this.header.userId;   }   /**    * Set this entry's user id.    *     * @param userId    *          This entry's new user id.    */   public void setUserId(int userId) {     this.header.userId = userId;   }   /**    * Get this entry's group id.    *     * @return This entry's group id.    */   public int getGroupId() {     return this.header.groupId;   }   /**    * Set this entry's group id.    *     * @param groupId    *          This entry's new group id.    */   public void setGroupId(int groupId) {     this.header.groupId = groupId;   }   /**    * Get this entry's user name.    *     * @return This entry's user name.    */   public String getUserName() {     return this.header.userName.toString();   }   /**    * Set this entry's user name.    *     * @param userName    *          This entry's new user name.    */   public void setUserName(String userName) {     this.header.userName = new StringBuffer(userName);   }   /**    * Get this entry's group name.    *     * @return This entry's group name.    */   public String getGroupName() {     return this.header.groupName.toString();   }   /**    * Set this entry's group name.    *     * @param groupName    *          This entry's new group name.    */   public void setGroupName(String groupName) {     this.header.groupName = new StringBuffer(groupName);   }   /**    * Convenience method to set this entry's group and user ids.    *     * @param userId    *          This entry's new user id.    * @param groupId    *          This entry's new group id.    */   public void setIds(int userId, int groupId) {     this.setUserId(userId);     this.setGroupId(groupId);   }   /**    * Convenience method to set this entry's group and user names.    *     * @param userName    *          This entry's new user name.    * @param groupName    *          This entry's new group name.    */   public void setNames(String userName, String groupName) {     this.setUserName(userName);     this.setGroupName(groupName);   }   /**    * Set this entry's modification time. The parameter passed to this method is    * in "Java time".    *     * @param time    *          This entry's new modification time.    */   public void setModTime(long time) {     this.header.modTime = time / 1000;   }   /**    * Set this entry's modification time.    *     * @param time    *          This entry's new modification time.    */   public void setModTime(Date time) {     this.header.modTime = time.getTime() / 1000;   }   /**    * Set this entry's modification time.    *     * @param time    *          This entry's new modification time.    */   public Date getModTime() {     return new Date(this.header.modTime * 1000);   }   /**    * Get this entry's file.    *     * @return This entry's file.    */   public File getFile() {     return this.file;   }   /**    * Get this entry's file size.    *     * @return This entry's file size.    */   public long getSize() {     return this.header.size;   }   /**    * Set this entry's file size.    *     * @param size    *          This entry's new file size.    */   public void setSize(long size) {     this.header.size = size;   }   /**    * Convenience method that will modify an entry's name directly in place in an    * entry header buffer byte array.    *     * @param outbuf    *          The buffer containing the entry header to modify.    * @param newName    *          The new name to place into the header buffer.    */   public void adjustEntryName(byte[] outbuf, String newName) {     int offset = 0;     offset = TarHeader.getNameBytes(new StringBuffer(newName), outbuf, offset, TarHeader.NAMELEN);   }   /**    * Return whether or not this entry represents a directory.    *     * @return True if this entry is a directory.    */   public boolean isDirectory() {     if (this.file != null)       return this.file.isDirectory();     if (this.header != null) {       if (this.header.linkFlag == TarHeader.LF_DIR)         return true;       if (this.header.name.toString().endsWith("/"))         return true;     }     return false;   }   /**    * Fill in a TarHeader with information from a File.    *     * @param hdr    *          The TarHeader to fill in.    * @param file    *          The file from which to get the header information.    */   public void getFileTarHeader(TarHeader hdr, File file) throws InvalidHeaderException {     this.file = file;     String name = file.getPath();     String osname = System.getProperty("os.name");     if (osname != null) {       // Strip off drive letters!       // REVIEW Would a better check be "(File.separator == '\')"?       // String Win32Prefix = "Windows";       // String prefix = osname.substring( 0, Win32Prefix.length() );       // if ( prefix.equalsIgnoreCase( Win32Prefix ) )       // if ( File.separatorChar == '\\' )       // Per Patrick Beard:       String Win32Prefix = "windows";       if (osname.toLowerCase().startsWith(Win32Prefix)) {         if (name.length() > 2) {           char ch1 = name.charAt(0);           char ch2 = name.charAt(1);           if (ch2 == ':' && ((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z'))) {             name = name.substring(2);           }         }       }     }     name = name.replace(File.separatorChar, '/');     // No absolute pathnames     // Windows (and Posix?) paths can start with "\\NetworkDrive\",     // so we loop on starting /'s.     for (; name.startsWith("/");)       name = name.substring(1);     hdr.linkName = new StringBuffer("");     hdr.name = new StringBuffer(name);     if (file.isDirectory()) {       hdr.mode = 040755;       hdr.linkFlag = TarHeader.LF_DIR;       if (hdr.name.charAt(hdr.name.length() - 1) != '/')         hdr.name.append("/");     } else {       hdr.mode = 0100644;       hdr.linkFlag = TarHeader.LF_NORMAL;     }     // UNDONE When File lets us get the userName, use it!     hdr.size = file.length();     hdr.modTime = file.lastModified() / 1000;     hdr.checkSum = 0;     hdr.devMajor = 0;     hdr.devMinor = 0;   }   /**    * If this entry represents a file, and the file is a directory, return an    * array of TarEntries for this entry's children.    *     * @return An array of TarEntry's for this entry's children.    */   public TarEntry[] getDirectoryEntries() throws InvalidHeaderException {     if (this.file == null || !this.file.isDirectory()) {       return new TarEntry[0];     }     String[] list = this.file.list();     TarEntry[] result = new TarEntry[list.length];     for (int i = 0; i < list.length; ++i) {       result[i] = new TarEntry(new File(this.file, list[i]));     }     return result;   }   /**    * Compute the checksum of a tar entry header.    *     * @param buf    *          The tar entry's header buffer.    * @return The computed checksum.    */   public long computeCheckSum(byte[] buf) {     long sum = 0;     for (int i = 0; i < buf.length; ++i) {       sum += 255 & buf[i];     }     return sum;   }   /**    * Write an entry's header information to a header buffer.    *     * @param outbuf    *          The tar entry header buffer to fill in.    */   public void writeEntryHeader(byte[] outbuf) {     int offset = 0;     offset = TarHeader.getNameBytes(this.header.name, outbuf, offset, TarHeader.NAMELEN);     offset = TarHeader.getOctalBytes(this.header.mode, outbuf, offset, TarHeader.MODELEN);     offset = TarHeader.getOctalBytes(this.header.userId, outbuf, offset, TarHeader.UIDLEN);     offset = TarHeader.getOctalBytes(this.header.groupId, outbuf, offset, TarHeader.GIDLEN);     long size = this.header.size;     offset = TarHeader.getLongOctalBytes(size, outbuf, offset, TarHeader.SIZELEN);     offset = TarHeader.getLongOctalBytes(this.header.modTime, outbuf, offset, TarHeader.MODTIMELEN);     int csOffset = offset;     for (int c = 0; c < TarHeader.CHKSUMLEN; ++c)       outbuf[offset++] = (byte) ' ';     outbuf[offset++] = this.header.linkFlag;     offset = TarHeader.getNameBytes(this.header.linkName, outbuf, offset, TarHeader.NAMELEN);     offset = TarHeader.getNameBytes(this.header.magic, outbuf, offset, TarHeader.MAGICLEN);     offset = TarHeader.getNameBytes(this.header.userName, outbuf, offset, TarHeader.UNAMELEN);     offset = TarHeader.getNameBytes(this.header.groupName, outbuf, offset, TarHeader.GNAMELEN);     offset = TarHeader.getOctalBytes(this.header.devMajor, outbuf, offset, TarHeader.DEVLEN);     offset = TarHeader.getOctalBytes(this.header.devMinor, outbuf, offset, TarHeader.DEVLEN);     for (; offset < outbuf.length;)       outbuf[offset++] = 0;     long checkSum = this.computeCheckSum(outbuf);     TarHeader.getCheckSumOctalBytes(checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN);   }   /**    * Parse an entry's TarHeader information from a header buffer.    *     * @param hdr    *          The TarHeader to fill in from the buffer information.    * @param header    *          The tar entry header buffer to get information from.    */   public void parseTarHeader(TarHeader hdr, byte[] header) throws InvalidHeaderException {     int offset = 0;     hdr.name = TarHeader.parseName(header, offset, TarHeader.NAMELEN);     offset += TarHeader.NAMELEN;     hdr.mode = (int) TarHeader.parseOctal(header, offset, TarHeader.MODELEN);     offset += TarHeader.MODELEN;     hdr.userId = (int) TarHeader.parseOctal(header, offset, TarHeader.UIDLEN);     offset += TarHeader.UIDLEN;     hdr.groupId = (int) TarHeader.parseOctal(header, offset, TarHeader.GIDLEN);     offset += TarHeader.GIDLEN;     hdr.size = TarHeader.parseOctal(header, offset, TarHeader.SIZELEN);     offset += TarHeader.SIZELEN;     hdr.modTime = TarHeader.parseOctal(header, offset, TarHeader.MODTIMELEN);     offset += TarHeader.MODTIMELEN;     hdr.checkSum = (int) TarHeader.parseOctal(header, offset, TarHeader.CHKSUMLEN);     offset += TarHeader.CHKSUMLEN;     hdr.linkFlag = header[offset++];     hdr.linkName = TarHeader.parseName(header, offset, TarHeader.NAMELEN);     offset += TarHeader.NAMELEN;     hdr.magic = TarHeader.parseName(header, offset, TarHeader.MAGICLEN);     offset += TarHeader.MAGICLEN;     hdr.userName = TarHeader.parseName(header, offset, TarHeader.UNAMELEN);     offset += TarHeader.UNAMELEN;     hdr.groupName = TarHeader.parseName(header, offset, TarHeader.GNAMELEN);     offset += TarHeader.GNAMELEN;     hdr.devMajor = (int) TarHeader.parseOctal(header, offset, TarHeader.DEVLEN);     offset += TarHeader.DEVLEN;     hdr.devMinor = (int) TarHeader.parseOctal(header, offset, TarHeader.DEVLEN);   }   /**    * Fill in a TarHeader given only the entry's name.    *     * @param hdr    *          The TarHeader to fill in.    * @param name    *          The tar entry name.    */   public void nameTarHeader(TarHeader hdr, String name) {     boolean isDir = name.endsWith("/");     hdr.checkSum = 0;     hdr.devMajor = 0;     hdr.devMinor = 0;     hdr.name = new StringBuffer(name);     hdr.mode = isDir ? 040755 : 0100644;     hdr.userId = 0;     hdr.groupId = 0;     hdr.size = 0;     hdr.checkSum = 0;     hdr.modTime = (new java.util.Date()).getTime() / 1000;     hdr.linkFlag = isDir ? TarHeader.LF_DIR : TarHeader.LF_NORMAL;     hdr.linkName = new StringBuffer("");     hdr.userName = new StringBuffer("");     hdr.groupName = new StringBuffer("");     hdr.devMajor = 0;     hdr.devMinor = 0;   } } /**  * The TarBuffer class implements the tar archive concept of a buffered input  * stream. This concept goes back to the days of blocked tape drives and special  * io devices. In the Java universe, the only real function that this class  * performs is to ensure that files have the correct "block" size, or other tars  * will complain.  * <p>  * You should never have a need to access this class directly. TarBuffers are  * created by Tar IO Streams.  *   * @version $Revision: 12504 $  * @author Timothy Gerard Endres, <a  *         href="mailto:time@gjt.org">time@trustice.com</a>.  * @see TarArchive  */ class TarBuffer extends Object {   public static final int DEFAULT_RCDSIZE = (512);   public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20);   private InputStream inStream;   private OutputStream outStream;   private byte[] blockBuffer;   private int currBlkIdx;   private int currRecIdx;   private int blockSize;   private int recordSize;   private int recsPerBlock;   private boolean debug;   public TarBuffer(InputStream inStream) {     this(inStream, TarBuffer.DEFAULT_BLKSIZE);   }   public TarBuffer(InputStream inStream, int blockSize) {     this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);   }   public TarBuffer(InputStream inStream, int blockSize, int recordSize) {     this.inStream = inStream;     this.outStream = null;     this.initialize(blockSize, recordSize);   }   public TarBuffer(OutputStream outStream) {     this(outStream, TarBuffer.DEFAULT_BLKSIZE);   }   public TarBuffer(OutputStream outStream, int blockSize) {     this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE);   }   public TarBuffer(OutputStream outStream, int blockSize, int recordSize) {     this.inStream = null;     this.outStream = outStream;     this.initialize(blockSize, recordSize);   }   /**    * Initialization common to all constructors.    */   private void initialize(int blockSize, int recordSize) {     this.debug = false;     this.blockSize = blockSize;     this.recordSize = recordSize;     this.recsPerBlock = (this.blockSize / this.recordSize);     this.blockBuffer = new byte[this.blockSize];     if (this.inStream != null) {       this.currBlkIdx = -1;       this.currRecIdx = this.recsPerBlock;     } else {       this.currBlkIdx = 0;       this.currRecIdx = 0;     }   }   /**    * Get the TAR Buffer's block size. Blocks consist of multiple records.    */   public int getBlockSize() {     return this.blockSize;   }   /**    * Get the TAR Buffer's record size.    */   public int getRecordSize() {     return this.recordSize;   }   /**    * Set the debugging flag for the buffer.    *     * @param debug    *          If true, print debugging output.    */   public void setDebug(boolean debug) {     this.debug = debug;   }   /**    * Determine if an archive record indicate End of Archive. End of archive is    * indicated by a record that consists entirely of null bytes.    *     * @param record    *          The record data to check.    */   public boolean isEOFRecord(byte[] record) {     for (int i = 0, sz = this.getRecordSize(); i < sz; ++i)       if (record[i] != 0)         return false;     return true;   }   /**    * Skip over a record on the input stream.    */   public void skipRecord() throws IOException {     if (this.debug) {       System.err           .println("SkipRecord: recIdx = " + this.currRecIdx + " blkIdx = " + this.currBlkIdx);     }     if (this.inStream == null)       throw new IOException("reading (via skip) from an output buffer");     if (this.currRecIdx >= this.recsPerBlock) {       if (!this.readBlock())         return; // UNDONE     }     this.currRecIdx++;   }   /**    * Read a record from the input stream and return the data.    *     * @return The record data.    */   public byte[] readRecord() throws IOException {     if (this.debug) {       System.err           .println("ReadRecord: recIdx = " + this.currRecIdx + " blkIdx = " + this.currBlkIdx);     }     if (this.inStream == null)       throw new IOException("reading from an output buffer");     if (this.currRecIdx >= this.recsPerBlock) {       if (!this.readBlock())         return null;     }     byte[] result = new byte[this.recordSize];     System.arraycopy(this.blockBuffer, (this.currRecIdx * this.recordSize), result, 0,         this.recordSize);     this.currRecIdx++;     return result;   }   /**    * @return false if End-Of-File, else true    */   private boolean readBlock() throws IOException {     if (this.debug) {       System.err.println("ReadBlock: blkIdx = " + this.currBlkIdx);     }     if (this.inStream == null)       throw new IOException("reading from an output buffer");     this.currRecIdx = 0;     int offset = 0;     int bytesNeeded = this.blockSize;     for (; bytesNeeded > 0;) {       long numBytes = this.inStream.read(this.blockBuffer, offset, bytesNeeded);       //       // NOTE       // We have fit EOF, and the block is not full!       //       // This is a broken archive. It does not follow the standard       // blocking algorithm. However, because we are generous, and       // it requires little effort, we will simply ignore the error       // and continue as if the entire block were read. This does       // not appear to break anything upstream. We used to return       // false in this case.       //       // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix.       //       if (numBytes == -1)         break;       offset += numBytes;       bytesNeeded -= numBytes;       if (numBytes != this.blockSize) {         if (this.debug) {           System.err.println("ReadBlock: INCOMPLETE READ " + numBytes + " of " + this.blockSize               + " bytes read.");         }       }     }     this.currBlkIdx++;     return true;   }   /**    * Get the current block number, zero based.    *     * @return The current zero based block number.    */   public int getCurrentBlockNum() {     return this.currBlkIdx;   }   /**    * Get the current record number, within the current block, zero based. Thus,    * current offset = (currentBlockNum * recsPerBlk) + currentRecNum.    *     * @return The current zero based record number.    */   public int getCurrentRecordNum() {     return this.currRecIdx - 1;   }   /**    * Write an archive record to the archive.    *     * @param record    *          The record data to write to the archive.    */   public void writeRecord(byte[] record) throws IOException {     if (this.debug) {       System.err.println("WriteRecord: recIdx = " + this.currRecIdx + " blkIdx = "           + this.currBlkIdx);     }     if (this.outStream == null)       throw new IOException("writing to an input buffer");     if (record.length != this.recordSize)       throw new IOException("record to write has length '" + record.length           + "' which is not the record size of '" + this.recordSize + "'");     if (this.currRecIdx >= this.recsPerBlock) {       this.writeBlock();     }     System.arraycopy(record, 0, this.blockBuffer, (this.currRecIdx * this.recordSize),         this.recordSize);     this.currRecIdx++;   }   /**    * Write an archive record to the archive, where the record may be inside of a    * larger array buffer. The buffer must be "offset plus record size" long.    *     * @param buf    *          The buffer containing the record data to write.    * @param offset    *          The offset of the record data within buf.    */   public void writeRecord(byte[] buf, int offset) throws IOException {     if (this.debug) {       System.err.println("WriteRecord: recIdx = " + this.currRecIdx + " blkIdx = "           + this.currBlkIdx);     }     if (this.outStream == null)       throw new IOException("writing to an input buffer");     if ((offset + this.recordSize) > buf.length)       throw new IOException("record has length '" + buf.length + "' with offset '" + offset           + "' which is less than the record size of '" + this.recordSize + "'");     if (this.currRecIdx >= this.recsPerBlock) {       this.writeBlock();     }     System.arraycopy(buf, offset, this.blockBuffer, (this.currRecIdx * this.recordSize),         this.recordSize);     this.currRecIdx++;   }   /**    * Write a TarBuffer block to the archive.    */   private void writeBlock() throws IOException {     if (this.debug) {       System.err.println("WriteBlock: blkIdx = " + this.currBlkIdx);     }     if (this.outStream == null)       throw new IOException("writing to an input buffer");     this.outStream.write(this.blockBuffer, 0, this.blockSize);     this.outStream.flush();     this.currRecIdx = 0;     this.currBlkIdx++;   }   /**    * Flush the current data block if it has any data in it.    */   private void flushBlock() throws IOException {     if (this.debug) {       System.err.println("TarBuffer.flushBlock() called.");     }     if (this.outStream == null)       throw new IOException("writing to an input buffer");     if (this.currRecIdx > 0) {       this.writeBlock();     }   }   /**    * Close the TarBuffer. If this is an output buffer, also flush the current    * block before closing.    */   public void close() throws IOException {     if (this.debug) {       System.err.println("TarBuffer.closeBuffer().");     }     if (this.outStream != null) {       this.flushBlock();       if (this.outStream != System.out && this.outStream != System.err) {         this.outStream.close();         this.outStream = null;       }     } else if (this.inStream != null) {       if (this.inStream != System.in) {         this.inStream.close();         this.inStream = null;       }     }   } } class InvalidHeaderException extends IOException {   public InvalidHeaderException() {     super();   }   public InvalidHeaderException(String msg) {     super(msg);   } }