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