package org.xbib.io.archive.tar;
import org.xbib.io.archive.ArchiveEntry;
import org.xbib.io.archive.ArchiveUtils;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Date;
/**
* This class represents an entry in a Tar archive for output
*/
public class TarArchiveOutputEntry implements TarConstants, ArchiveEntry {
private static final int BYTE_MASK = 255;
/**
* Maximum length of a user's name in the tar file
*/
public static final int MAX_NAMELEN = 31;
/**
* Default permissions bits for directories
*/
public static final int DEFAULT_DIR_MODE = 040755;
/**
* Default permissions bits for files
*/
public static final int DEFAULT_FILE_MODE = 0100644;
/**
* Convert millis to seconds
*/
public static final int MILLIS_PER_SECOND = 1000;
/**
* The entry's name.
*/
private String name;
/**
* The entry's permission mode.
*/
private int mode;
/**
* The entry's user id.
*/
private int userId;
/**
* The entry's group id.
*/
private int groupId;
/**
* The entry's size.
*/
private long size;
/**
* The entry's modification time.
*/
private long modTime;
/**
* The entry's link flag.
*/
private byte linkFlag;
/**
* The entry's link name.
*/
private String linkName;
/**
* The entry's magic tag.
*/
private String magic;
/**
* The version of the format
*/
private String version;
/**
* The entry's user name.
*/
private String userName;
/**
* The entry's group name.
*/
private String groupName;
/**
* The entry's major device number.
*/
private int devMajor;
/**
* The entry's minor device number.
*/
private int devMinor;
/**
* If an extension sparse header follows.
*/
private boolean isExtended;
/**
* The entry's real size in case of a sparse file.
*/
private long realSize;
/**
* The entry's file reference
*/
private File file;
/**
* Construct an empty entry and prepares the header values.
*/
public TarArchiveOutputEntry() {
this.magic = MAGIC_POSIX;
this.version = VERSION_POSIX;
this.name = "";
this.linkName = "";
String user = System.getProperty("user.name", "");
if (user.length() > MAX_NAMELEN) {
user = user.substring(0, MAX_NAMELEN);
}
this.userId = 0;
this.groupId = 0;
this.userName = user;
this.groupName = "";
this.file = null;
this.mode = DEFAULT_FILE_MODE;
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
*/
public TarArchiveOutputEntry(String name) {
this(name, false);
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
* @param preserveLeadingSlashes whether to allow leading slashes
* in the name.
*/
public TarArchiveOutputEntry(String name, boolean preserveLeadingSlashes) {
this();
name = ArchiveUtils.normalizeFileName(name, preserveLeadingSlashes);
this.name = name;
boolean isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
this.devMajor = 0;
this.devMinor = 0;
this.userId = 0;
this.groupId = 0;
this.size = 0;
this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND;
this.linkName = "";
this.userName = "";
this.groupName = "";
}
/**
* Construct an entry with a name and a link flag.
*
* @param name the entry name
* @param linkFlag the entry link flag.
*/
public TarArchiveOutputEntry(String name, byte linkFlag) {
this(name);
this.linkFlag = linkFlag;
if (linkFlag == LF_GNUTYPE_LONGNAME) {
magic = MAGIC_GNU;
version = VERSION_GNU_SPACE;
}
}
/**
* Construct an entry for a file. File is set to file, and the
* header is constructed from information from the file.
* The name is set from the normalized file path.
*
* @param file The file that the entry represents.
*/
public TarArchiveOutputEntry(File file) {
this(file, ArchiveUtils.normalizeFileName(file.getPath(), false));
}
/**
* 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.
* @param fileName the name to be used for the entry.
*/
public TarArchiveOutputEntry(File file, String fileName) {
this();
this.file = file;
this.linkName = "";
if (file.isDirectory()) {
this.mode = DEFAULT_DIR_MODE;
this.linkFlag = LF_DIR;
int nameLength = fileName.length();
if (nameLength == 0 || fileName.charAt(nameLength - 1) != '/') {
this.name = fileName + "/";
} else {
this.name = fileName;
}
this.size = 0;
} else {
this.mode = DEFAULT_FILE_MODE;
this.linkFlag = LF_NORMAL;
this.size = file.length();
this.name = fileName;
}
this.modTime = file.lastModified() / MILLIS_PER_SECOND;
this.devMajor = 0;
this.devMinor = 0;
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
public boolean equals(TarArchiveOutputEntry it) {
return getName().equals(it.getName());
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
@Override
public boolean equals(Object it) {
return !(it == null || getClass() != it.getClass()) && equals((TarArchiveOutputEntry) it);
}
/**
* Hashcodes are based on entry names.
*
* @return the entry hashcode
*/
@Override
public int hashCode() {
return getName().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(TarArchiveOutputEntry desc) {
return desc.getName().startsWith(getName());
}
/**
* Get this entry's name.
*
* @return This entry's name.
*/
public String getName() {
return name;
}
/**
* Set this entry's name.
*
* @param name This entry's new name.
*/
public TarArchiveOutputEntry setName(String name) {
this.name = ArchiveUtils.normalizeFileName(name, false);
boolean isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
return this;
}
/**
* Set the mode for this entry
*
* @param mode the mode for this entry
*/
public void setMode(int mode) {
this.mode = mode;
}
/**
* Get this entry's link name.
*
* @return This entry's link name.
*/
public String getLinkName() {
return linkName;
}
/**
* Set this entry's link name.
*
* @param link the link name to use.
*/
public void setLinkName(String link) {
this.linkName = link;
}
/**
* Get this entry's user id.
*
* @return This entry's user id.
*/
public int getUserId() {
return userId;
}
/**
* Set this entry's user id.
*
* @param userId This entry's new user id.
*/
public void setUserId(int userId) {
this.userId = userId;
}
/**
* Get this entry's group id.
*
* @return This entry's group id.
*/
public int getGroupId() {
return groupId;
}
/**
* Set this entry's group id.
*
* @param groupId This entry's new group id.
*/
public void setGroupId(int groupId) {
this.groupId = groupId;
}
/**
* Get this entry's user name.
*
* @return This entry's user name.
*/
public String getUserName() {
return userName;
}
/**
* Set this entry's user name.
*
* @param userName This entry's new user name.
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* Get this entry's group name.
*
* @return This entry's group name.
*/
public String getGroupName() {
return groupName;
}
/**
* Set this entry's group name.
*
* @param groupName This entry's new group name.
*/
public void setGroupName(String groupName) {
this.groupName = 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) {
setUserId(userId);
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) {
setUserName(userName);
setGroupName(groupName);
}
/**
* Set this entry's modification time. The parameter passed
* to this method is in "Java time".
*
* @param date This entry's new modification time.
*/
public TarArchiveOutputEntry setLastModified(Date date) {
modTime = date.getTime() / MILLIS_PER_SECOND;
return this;
}
public Date getLastModified() {
return new Date(modTime * MILLIS_PER_SECOND);
}
/**
* Get this entry's file.
*
* @return This entry's file.
*/
public File getFile() {
return file;
}
/**
* Get this entry's mode.
*
* @return This entry's mode.
*/
public int getMode() {
return mode;
}
/**
* Get this entry's file size.
*
* @return This entry's file size.
*/
public long getEntrySize() {
return size;
}
/**
* Set this entry's file size.
*
* @param size This entry's new file size.
* @throws IllegalArgumentException if the size is < 0.
*/
public TarArchiveOutputEntry setEntrySize(long size) {
if (size < 0) {
throw new IllegalArgumentException("Size is out of range: " + size);
}
this.size = size;
return this;
}
/**
* Get this entry's major device number.
*
* @return This entry's major device number.
*/
public int getDevMajor() {
return devMajor;
}
/**
* Set this entry's major device number.
*
* @param devNo This entry's major device number.
* @throws IllegalArgumentException if the devNo is < 0.
*/
public void setDevMajor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Major device number is out of " + "range: " + devNo);
}
this.devMajor = devNo;
}
/**
* Get this entry's minor device number.
*
* @return This entry's minor device number.
*/
public int getDevMinor() {
return devMinor;
}
/**
* Set this entry's minor device number.
*
* @param devNo This entry's minor device number.
* @throws IllegalArgumentException if the devNo is < 0.
*/
public void setDevMinor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Minor device number is out of "
+ "range: " + devNo);
}
this.devMinor = devNo;
}
/**
* Indicates in case of a sparse file if an extension sparse header
* follows.
*
* @return true if an extension sparse header follows.
*/
public boolean isExtended() {
return isExtended;
}
/**
* Get this entry's real file size in case of a sparse file.
*
* @return This entry's real file size.
*/
public long getRealSize() {
return realSize;
}
/**
* Indicate if this entry is a GNU sparse block
*
* @return true if this is a sparse extension provided by GNU tar
*/
public boolean isGNUSparse() {
return linkFlag == LF_GNUTYPE_SPARSE;
}
/**
* Indicate if this entry is a GNU long name block
*
* @return true if this is a long name extension provided by GNU tar
*/
public boolean isGNULongNameEntry() {
return linkFlag == LF_GNUTYPE_LONGNAME
&& name.equals(GNU_LONGLINK);
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isPaxHeader() {
return linkFlag == LF_PAX_EXTENDED_HEADER_LC
|| linkFlag == LF_PAX_EXTENDED_HEADER_UC;
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isGlobalPaxHeader() {
return linkFlag == LF_PAX_GLOBAL_EXTENDED_HEADER;
}
/**
* Return whether or not this entry represents a directory.
*
* @return True if this entry is a directory.
*/
public boolean isDirectory() {
if (file != null) {
return file.isDirectory();
}
return linkFlag == LF_DIR || getName().endsWith("/");
}
/**
* Check if this is a "normal file"
*/
public boolean isFile() {
if (file != null) {
return file.isFile();
}
return linkFlag == LF_OLDNORM || linkFlag == LF_NORMAL || !getName().endsWith("/");
}
/**
* Check if this is a symbolic link entry.
*/
public boolean isSymbolicLink() {
return linkFlag == LF_SYMLINK;
}
/**
* Check if this is a link entry.
*/
public boolean isLink() {
return linkFlag == LF_LINK;
}
/**
* Check if this is a character device entry.
*/
public boolean isCharacterDevice() {
return linkFlag == LF_CHR;
}
/**
* Check if this is a block device entry.
*/
public boolean isBlockDevice() {
return linkFlag == LF_BLK;
}
/**
* Check if this is a FIFO (pipe) entry.
*/
public boolean isFIFO() {
return linkFlag == LF_FIFO;
}
/**
* 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 TarArchiveOutputEntry[] getDirectoryEntries() {
if (file == null || !file.isDirectory()) {
return new TarArchiveOutputEntry[0];
}
String[] list = file.list();
TarArchiveOutputEntry[] result = new TarArchiveOutputEntry[list.length];
for (int i = 0; i < list.length; ++i) {
result[i] = new TarArchiveOutputEntry(new File(file, list[i]));
}
return result;
}
/**
* Write an entry's header information to a header buffer.
*
* @param outbuf The tar entry header buffer to fill in.
* @param encoding encoding to use when writing the file name.
* @param starMode whether to use the star/GNU tar/BSD tar
* extension for numeric fields if their value doesn't fit in the
* maximum size of standard tar archives
*/
public void writeEntryHeader(byte[] outbuf, ArchiveEntryEncoding encoding, boolean starMode) throws IOException {
int offset = 0;
offset = ArchiveUtils.formatNameBytes(name, outbuf, offset, NAMELEN, encoding);
offset = writeEntryHeaderField(mode, outbuf, offset, MODELEN, starMode);
offset = writeEntryHeaderField(userId, outbuf, offset, UIDLEN, starMode);
offset = writeEntryHeaderField(groupId, outbuf, offset, GIDLEN, starMode);
offset = writeEntryHeaderField(size, outbuf, offset, SIZELEN, starMode);
offset = writeEntryHeaderField(modTime, outbuf, offset, MODTIMELEN, starMode);
int csOffset = offset;
for (int c = 0; c < CHKSUMLEN; ++c) {
outbuf[offset++] = (byte) ' ';
}
outbuf[offset++] = linkFlag;
offset = ArchiveUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN, encoding);
offset = ArchiveUtils.formatNameBytes(magic, outbuf, offset, MAGICLEN);
offset = ArchiveUtils.formatNameBytes(version, outbuf, offset, VERSIONLEN);
offset = ArchiveUtils.formatNameBytes(userName, outbuf, offset, UNAMELEN, encoding);
offset = ArchiveUtils.formatNameBytes(groupName, outbuf, offset, GNAMELEN, encoding);
offset = writeEntryHeaderField(devMajor, outbuf, offset, DEVLEN, starMode);
offset = writeEntryHeaderField(devMinor, outbuf, offset, DEVLEN, starMode);
while (offset < outbuf.length) {
outbuf[offset++] = 0;
}
long chk = computeCheckSum(outbuf);
formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN);
}
private int writeEntryHeaderField(long value, byte[] outbuf, int offset, int length, boolean starMode) {
if (!starMode && (value < 0
|| value >= (1l << (3 * (length - 1))))) {
// value doesn't fit into field when written as octal
// number, will be written to PAX header or causes an
// error
return formatLongOctalBytes(0, outbuf, offset, length);
}
return formatLongOctalOrBinaryBytes(value, outbuf, offset, length);
}
/**
* Fill buffer with unsigned octal number, padded with leading zeroes.
*
* @param value number to convert to octal - treated as unsigned
* @param buffer destination buffer
* @param offset starting offset in buffer
* @param length length of buffer to fill
* @throws IllegalArgumentException if the value will not fit in the buffer
*/
private void formatUnsignedOctalString(final long value, byte[] buffer, final int offset, final int length) {
int remaining = length;
remaining--;
if (value == 0) {
buffer[offset + remaining--] = (byte) '0';
} else {
long val = value;
for (; remaining >= 0 && val != 0; --remaining) {
buffer[offset + remaining] = (byte) ((byte) '0' + (byte) (val & 7));
val = val >>> 3;
}
if (val != 0) {
throw new IllegalArgumentException(value + "=" + Long.toOctalString(value) + " will not fit in octal number buffer of length " + length);
}
}
for (; remaining >= 0; --remaining) { // leading zeros
buffer[offset + remaining] = (byte) '0';
}
}
/**
* Write an octal long integer into a buffer.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by a space.
*
* @param value The value to write as octal
* @param buf The destinationbuffer.
* @param offset The starting offset into the buffer.
* @param length The length of the buffer
* @return The updated offset
* @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer
*/
private int formatLongOctalBytes(final long value, byte[] buf, final int offset, final int length) {
int idx = length - 1; // For space
formatUnsignedOctalString(value, buf, offset, idx);
buf[offset + idx] = (byte) ' '; // Trailing space
return offset + length;
}
/**
* Write an long integer into a buffer as an octal string if this
* will fit, or as a binary number otherwise.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by a space.
*
* @param value The value to write into the buffer.
* @param buf The destination buffer.
* @param offset The starting offset into the buffer.
* @param length The length of the buffer.
* @return The updated offset.
* @throws IllegalArgumentException if the value (and trailer)
* will not fit in the buffer.
*/
private int formatLongOctalOrBinaryBytes(final long value, byte[] buf, final int offset, final int length) {
// Check whether we are dealing with UID/GID or SIZE field
final long maxAsOctalChar = length == TarConstants.UIDLEN ? TarConstants.MAXID : TarConstants.MAXSIZE;
final boolean negative = value < 0;
if (!negative && value <= maxAsOctalChar) { // OK to store as octal chars
return formatLongOctalBytes(value, buf, offset, length);
}
if (length < 9) {
formatLongBinary(value, buf, offset, length, negative);
}
formatBigIntegerBinary(value, buf, offset, length, negative);
buf[offset] = (byte) (negative ? 0xff : 0x80);
return offset + length;
}
private void formatLongBinary(final long value, byte[] buf, final int offset, final int length, final boolean negative) {
final int bits = (length - 1) * 8;
final long max = 1l << bits;
long val = Math.abs(value);
if (val >= max) {
throw new IllegalArgumentException("Value " + value +
" is too large for " + length + " byte field.");
}
if (negative) {
val ^= max - 1;
val |= 0xff << bits;
val++;
}
for (int i = offset + length - 1; i >= offset; i--) {
buf[i] = (byte) val;
val >>= 8;
}
}
private void formatBigIntegerBinary(final long value, byte[] buf,
final int offset,
final int length,
final boolean negative) {
BigInteger val = BigInteger.valueOf(value);
final byte[] b = val.toByteArray();
final int len = b.length;
final int off = offset + length - len;
System.arraycopy(b, 0, buf, off, len);
final byte fill = (byte) (negative ? 0xff : 0);
for (int i = offset + 1; i < off; i++) {
buf[i] = fill;
}
}
/**
* Writes an octal value into a buffer.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by NUL and then space.
*
* @param value The value to convert
* @param buf The destination buffer
* @param offset The starting offset into the buffer.
* @param length The size of the buffer.
* @return The updated value of offset, i.e. offset+length
* @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer
*/
private int formatCheckSumOctalBytes(final long value, byte[] buf, final int offset, final int length) {
int idx = length - 2;
formatUnsignedOctalString(value, buf, offset, idx);
buf[offset + idx++] = 0;
buf[offset + idx] = (byte) ' ';
return offset + length;
}
/**
* Compute the checksum of a tar entry header.
*
* @param buf The tar entry's header buffer.
* @return The computed checksum.
*/
private long computeCheckSum(final byte[] buf) {
long sum = 0;
for (byte aBuf : buf) {
sum += BYTE_MASK & aBuf;
}
return sum;
}
}