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