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.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); } }