/* ** 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. ** */ package org.jboss.shrinkwrap.impl.base.io.tar; import java.io.File; import java.util.Date; /** * * 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. * * <pre> * * Original Unix Tar Header: * * Field Field Field * Width Name Meaning * ----- --------- --------------------------- * 100 name name of file * 8 mode file mode * 8 uid owner user ID * 8 gid owner group ID * 12 size length of file in bytes * 12 mtime modify time of file * 8 chksum checksum for header * 1 link indicator for links * 100 linkname name of linked file * * </pre> * * <pre> * * POSIX "ustar" Style Tar Header: * * Field Field Field * Width Name Meaning * ----- --------- --------------------------- * 100 name name of file * 8 mode file mode * 8 uid owner user ID * 8 gid owner group ID * 12 size length of file in bytes * 12 mtime modify time of file * 8 chksum checksum for header * 1 typeflag type of file * 100 linkname name of linked file * 6 magic USTAR indicator * 2 version USTAR version * 32 uname owner user name * 32 gname owner group name * 8 devmajor device major number * 8 devminor device minor number * 155 prefix prefix for file name * * struct posix_header * { byte offset * char name[100]; 0 * char mode[8]; 100 * char uid[8]; 108 * char gid[8]; 116 * char size[12]; 124 * char mtime[12]; 136 * char chksum[8]; 148 * char typeflag; 156 * char linkname[100]; 157 * char magic[6]; 257 * char version[2]; 263 * char uname[32]; 265 * char gname[32]; 297 * char devmajor[8]; 329 * char devminor[8]; 337 * char prefix[155]; 345 * }; 500 * * </pre> * * Note that while the class does recognize GNU formatted headers, it does not perform proper processing of GNU * archives. I hope to add the GNU support someday. * * Directory "size" fix contributed by: Bert Becker <becker@informatik.hu-berlin.de> * * @see TarHeader * @author Timothy Gerard Endres, <time@gjt.org> */ public class TarEntry extends Object implements Cloneable { /** * If this entry represents a File, this references it. */ protected File file; /** * This is the entry's header information. */ protected TarHeader header; /** * Set to true if this is a "old-unix" format entry. */ protected boolean unixFormat; /** * Set to true if this is a 'ustar' format entry. */ protected boolean ustarFormat; /** * Set to true if this is a GNU 'ustar' format entry. */ protected boolean gnuFormat; /** * The default constructor is protected for use only by subclasses. */ protected TarEntry() { } /** * 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(); this.gnuFormat = false; this.ustarFormat = true; // REVIEW What we prefer to use... this.unixFormat = false; } /** * Clone the entry. */ public Object clone() { TarEntry entry = null; try { entry = (TarEntry) super.clone(); if (this.header != null) { entry.header = (TarHeader) this.header.clone(); } if (this.file != null) { entry.file = new File(this.file.getAbsolutePath()); } } catch (CloneNotSupportedException ex) { ex.printStackTrace(System.err); } return entry; } /** * Returns true if this entry's header is in "ustar" format. * * @return True if the entry's header is in "ustar" format. */ public boolean isUSTarFormat() { return this.ustarFormat; } /** * Sets this entry's header format to "ustar". */ public void setUSTarFormat() { this.ustarFormat = true; this.gnuFormat = false; this.unixFormat = false; } /** * Returns true if this entry's header is in the GNU 'ustar' format. * * @return True if the entry's header is in the GNU 'ustar' format. */ public boolean isGNUTarFormat() { return this.gnuFormat; } /** * Sets this entry's header format to GNU "ustar". */ public void setGNUTarFormat() { this.gnuFormat = true; this.ustarFormat = false; this.unixFormat = false; } /** * Returns true if this entry's header is in the old "unix-tar" format. * * @return True if the entry's header is in the old "unix-tar" format. */ public boolean isUnixTarFormat() { return this.unixFormat; } /** * Sets this entry's header format to "unix-style". */ public void setUnixTarFormat() { this.unixFormat = true; this.ustarFormat = false; this.gnuFormat = false; } /** * 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()); } @Override public boolean equals(final Object obj) { if (!(obj instanceof TarEntry)) { return false; } return this.equals((TarEntry) obj); } /** * Value equality done by name only * {@inheritDoc} * @see java.lang.Object#hashCode() */ @Override public int hashCode(){ return this.header.name.hashCode(); } /** * 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; } /** * 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 == '\\' ) // Windows OS check was contributed by // Patrick Beard <beard@netscape.com> 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.size = 0; hdr.mode = 040755; hdr.linkFlag = TarHeader.LF_DIR; if (hdr.name.charAt(hdr.name.length() - 1) != '/') { hdr.name.append("/"); } } else { hdr.size = file.length(); hdr.mode = 0100644; hdr.linkFlag = TarHeader.LF_NORMAL; } // UNDONE When File lets us get the userName, use it! 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. This method can throw an InvalidHeaderException * * @param outbuf * The tar entry header buffer to fill in. * @throws InvalidHeaderException * If the name will not fit in the header. */ public void writeEntryHeader(byte[] outbuf) throws InvalidHeaderException { int offset = 0; if (this.isUnixTarFormat()) { if (this.header.name.length() > 100) { throw new InvalidHeaderException("file path is greater than 100 characters, " + this.header.name); } } offset = TarHeader.getFileNameBytes(this.header.name.toString(), outbuf); 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); if (this.unixFormat) { for (int i = 0; i < TarHeader.MAGICLEN; ++i) { outbuf[offset++] = 0; } } else { 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. * * Old unix-style code contributed by David Mehringer <dmehring@astro.uiuc.edu>. * * @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[] headerBuf) throws InvalidHeaderException { int offset = 0; // // NOTE Recognize archive header format. // if (headerBuf[257] == 0 && headerBuf[258] == 0 && headerBuf[259] == 0 && headerBuf[260] == 0 && headerBuf[261] == 0) { this.unixFormat = true; this.ustarFormat = false; this.gnuFormat = false; } else if (headerBuf[257] == 'u' && headerBuf[258] == 's' && headerBuf[259] == 't' && headerBuf[260] == 'a' && headerBuf[261] == 'r' && headerBuf[262] == 0) { this.ustarFormat = true; this.gnuFormat = false; this.unixFormat = false; } else if (headerBuf[257] == 'u' && headerBuf[258] == 's' && headerBuf[259] == 't' && headerBuf[260] == 'a' && headerBuf[261] == 'r' && headerBuf[262] != 0 && headerBuf[263] != 0) { // REVIEW this.gnuFormat = true; this.unixFormat = false; this.ustarFormat = false; } else { StringBuffer buf = new StringBuffer(128); buf.append("header magic is not 'ustar' or unix-style zeros, it is '"); buf.append(headerBuf[257]); buf.append(headerBuf[258]); buf.append(headerBuf[259]); buf.append(headerBuf[260]); buf.append(headerBuf[261]); buf.append(headerBuf[262]); buf.append(headerBuf[263]); buf.append("', or (dec) "); buf.append((int) headerBuf[257]); buf.append(", "); buf.append((int) headerBuf[258]); buf.append(", "); buf.append((int) headerBuf[259]); buf.append(", "); buf.append((int) headerBuf[260]); buf.append(", "); buf.append((int) headerBuf[261]); buf.append(", "); buf.append((int) headerBuf[262]); buf.append(", "); buf.append((int) headerBuf[263]); throw new InvalidHeaderException(buf.toString()); } hdr.name = TarHeader.parseFileName(headerBuf); offset = TarHeader.NAMELEN; hdr.mode = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.MODELEN); offset += TarHeader.MODELEN; hdr.userId = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.UIDLEN); offset += TarHeader.UIDLEN; hdr.groupId = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.GIDLEN); offset += TarHeader.GIDLEN; hdr.size = TarHeader.parseOctal(headerBuf, offset, TarHeader.SIZELEN); offset += TarHeader.SIZELEN; hdr.modTime = TarHeader.parseOctal(headerBuf, offset, TarHeader.MODTIMELEN); offset += TarHeader.MODTIMELEN; hdr.checkSum = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.CHKSUMLEN); offset += TarHeader.CHKSUMLEN; hdr.linkFlag = headerBuf[offset++]; hdr.linkName = TarHeader.parseName(headerBuf, offset, TarHeader.NAMELEN); offset += TarHeader.NAMELEN; if (this.ustarFormat) { hdr.magic = TarHeader.parseName(headerBuf, offset, TarHeader.MAGICLEN); offset += TarHeader.MAGICLEN; hdr.userName = TarHeader.parseName(headerBuf, offset, TarHeader.UNAMELEN); offset += TarHeader.UNAMELEN; hdr.groupName = TarHeader.parseName(headerBuf, offset, TarHeader.GNAMELEN); offset += TarHeader.GNAMELEN; hdr.devMajor = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.DEVLEN); offset += TarHeader.DEVLEN; hdr.devMinor = (int) TarHeader.parseOctal(headerBuf, offset, TarHeader.DEVLEN); } else { hdr.devMajor = 0; hdr.devMinor = 0; hdr.magic = new StringBuffer(""); hdr.userName = new StringBuffer(""); hdr.groupName = new StringBuffer(""); } } /** * 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("/"); this.gnuFormat = false; this.ustarFormat = true; this.unixFormat = false; 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; } public String toString() { StringBuffer result = new StringBuffer(128); return result.append("[TarEntry name=").append(this.getName()).append(", isDir=").append(this.isDirectory()) .append(", size=").append(this.getSize()).append(", userId=").append(this.getUserId()).append(", user=") .append(this.getUserName()).append(", groupId=").append(this.getGroupId()).append(", group=") .append(this.getGroupName()).append("]").toString(); } }