Mega Code Archive

 
Categories / Java / File Input Output
 

TarOutputStream writes a UNIX tar archive as an OutputStream

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