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.IOException;
import java.math.BigInteger;
import java.util.Date;
/**
* This class represents an entry in a Tar archive.
*/
public class TarArchiveInputEntry implements TarConstants, ArchiveEntry {
/**
* Maximum length of a user's name in the tar file
*/
private static final int MAX_NAMELEN = 31;
/**
* Default permissions bits for directories
*/
private static final int DEFAULT_DIR_MODE = 040755;
/**
* Default permissions bits for files
*/
private static final int DEFAULT_FILE_MODE = 0100644;
/**
* Convert millis to seconds
*/
private 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 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;
private boolean isDir;
/**
* Construct an empty entry and prepares the header values.
*/
public TarArchiveInputEntry() {
this.version = VERSION_POSIX;
this.name = "";
this.linkName = "";
this.linkFlag = LF_GNUTYPE_LONGNAME;
String user = System.getProperty("user.name", "");
if (user.length() > MAX_NAMELEN) {
user = user.substring(0, MAX_NAMELEN);
}
this.userName = user;
this.groupName = "";
this.userId = 0;
this.groupId = 0;
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 TarArchiveInputEntry(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 TarArchiveInputEntry(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 TarArchiveInputEntry(String name, byte linkFlag) {
this(name);
this.linkFlag = linkFlag;
if (linkFlag == LF_GNUTYPE_LONGNAME) {
version = VERSION_GNU_SPACE;
}
}
/**
* Construct an entry from an archive's header bytes. File is set
* to null.
*
* @param headerBuf The header bytes from a tar archive entry.
* @param encoding encoding to use for file names
* @throws IllegalArgumentException if any of the numeric fields have an invalid format
*/
public TarArchiveInputEntry(byte[] headerBuf, ArchiveEntryEncoding encoding) throws IOException {
this();
parseTarHeader(headerBuf, encoding);
}
/**
* 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(TarArchiveInputEntry 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((TarArchiveInputEntry) it);
}
/**
* Hashcodes are based on entry names.
*
* @return the entry hashcode
*/
@Override
public int hashCode() {
return getName().hashCode();
}
/**
* 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 TarArchiveInputEntry setName(String name) {
this.name = ArchiveUtils.normalizeFileName(name, false);
this.isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
return this;
}
/**
* Set this entry's modification time
*
* @param date This entry's new modification time
*/
public TarArchiveInputEntry setLastModified(Date date) {
modTime = date.getTime() / MILLIS_PER_SECOND;
return this;
}
public Date getLastModified() {
return new Date(modTime * MILLIS_PER_SECOND);
}
@Override
public boolean isDirectory() {
return isDir;
}
/**
* Set this entry's file size.
*
* @param size This entry's new file size.
* @throws IllegalArgumentException if the size is < 0.
*/
public TarArchiveInputEntry setEntrySize(long size) {
if (size < 0) {
throw new IllegalArgumentException("size is out of range: " + size);
}
this.size = size;
return this;
}
/**
* Get this entry's file size.
*
* @return This entry's file size.
*/
public long getEntrySize() {
return size;
}
/**
* 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;
}
/**
* Get this entry's mode.
*
* @return This entry's mode.
*/
public int getMode() {
return mode;
}
/**
* 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 && GNU_LONGLINK.equals(name);
}
/**
* 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;
}
/**
* 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;
}
/**
* Parse an entry's header information from a header buffer.
*
* @param header The tar entry header buffer to get information from.
* @param encoding encoding to use for file names
* @throws IllegalArgumentException if any of the numeric fields
* have an invalid format
*/
public void parseTarHeader(byte[] header, ArchiveEntryEncoding encoding)
throws IOException {
parseTarHeader(header, encoding, false);
}
private void parseTarHeader(byte[] header, ArchiveEntryEncoding encoding, final boolean oldStyle)
throws IOException {
int offset = 0;
int type = evaluateType(header);
name = parseFileName(header);
offset += NAMELEN;
mode = (int) parseOctalOrBinary(header, offset, MODELEN);
offset += MODELEN;
userId = (int) parseOctalOrBinary(header, offset, UIDLEN);
offset += UIDLEN;
groupId = (int) parseOctalOrBinary(header, offset, GIDLEN);
offset += GIDLEN;
if (type == GNU_FORMAT) {
size = getSize(header, offset, SIZELEN);
} else {
size = parseOctalOrBinary(header, offset, SIZELEN);
}
offset += SIZELEN;
modTime = parseOctalOrBinary(header, offset, MODTIMELEN);
offset += MODTIMELEN;
offset += CHKSUMLEN;
linkFlag = header[offset++];
linkName = oldStyle ? parseName(header, offset, NAMELEN) : parseName(header, offset, NAMELEN, encoding);
offset += NAMELEN;
switch (type) {
case UNIX_FORMAT: {
offset += ATIMELEN_GNU;
offset += CTIMELEN_GNU;
offset += OFFSETLEN_GNU;
offset += LONGNAMESLEN_GNU;
offset += PAD2LEN_GNU;
offset += SPARSELEN_GNU;
isExtended = parseBoolean(header, offset);
offset += ISEXTENDEDLEN_GNU;
realSize = parseOctal(header, offset, REALSIZELEN_GNU);
offset += REALSIZELEN_GNU;
break;
}
case POSIX_FORMAT: {
parseName(header, offset, MAGICLEN); // magic
offset += MAGICLEN;
version = parseName(header, offset, VERSIONLEN);
offset += VERSIONLEN;
userName = oldStyle ? parseName(header, offset, UNAMELEN) : parseName(header, offset, UNAMELEN, encoding);
offset += UNAMELEN;
groupName = oldStyle ? parseName(header, offset, GNAMELEN) : parseName(header, offset, GNAMELEN, encoding);
offset += GNAMELEN;
devMajor = (int) parseOctalOrBinary(header, offset, DEVLEN);
offset += DEVLEN;
devMinor = (int) parseOctalOrBinary(header, offset, DEVLEN);
offset += DEVLEN;
}
}
}
/**
* Evaluate an entry's header format from a header buffer.
*
* @param header The tar entry header buffer to evaluate the format for.
* @return format type
*/
private int evaluateType(byte[] header) {
if (ArchiveUtils.matchAsciiBuffer(MAGIC_UNIX, header, MAGIC_OFFSET, MAGICLEN)) {
return UNIX_FORMAT;
}
if (ArchiveUtils.matchAsciiBuffer(MAGIC_POSIX, header, MAGIC_OFFSET, MAGICLEN)) {
return POSIX_FORMAT;
}
if (ArchiveUtils.matchAsciiBuffer(MAGIC_GNU, header, MAGIC_OFFSET, MAGICLEN)) {
return GNU_FORMAT;
}
return 0;
}
/**
* Parse an octal string from a buffer.
* <p>Leading spaces are ignored.
* The buffer must contain a trailing space or NUL,
* and may contain an additional trailing space or NUL.</p>
* <p>The input buffer is allowed to contain all NULs,
* in which case the method returns 0L
* (this allows for missing fields).</p>
* <p>To work-around some tar implementations that insert a
* leading NUL this method returns 0 if it detects a leading NUL.</p>
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse - must be at least 2 bytes.
* @return The long value of the octal string.
* @throws IllegalArgumentException if the trailing space/NUL is missing or if a invalid byte is detected.
*/
private long parseOctal(final byte[] buffer, final int offset, final int length) {
long result = 0;
int end = offset + length;
int start = offset;
if (length < 2) {
throw new IllegalArgumentException("Length " + length + " must be at least 2");
}
if (buffer[start] == 0) {
return 0L;
}
while (start < end) {
if (buffer[start] == ' ') {
start++;
} else {
break;
}
}
byte trailer;
trailer = buffer[end - 1];
if (trailer == 0 || trailer == ' ') {
end--;
} else {
throw new IllegalArgumentException(exceptionMessage(buffer, offset, length, end - 1, trailer));
}
trailer = buffer[end - 1];
if (trailer == 0 || trailer == ' ') {
end--;
}
for (; start < end; start++) {
final byte currentByte = buffer[start];
if (currentByte < '0' || currentByte > '7') {
throw new IllegalArgumentException(
exceptionMessage(buffer, offset, length, start, currentByte));
}
result = (result << 3) + (currentByte - '0'); // convert from ASCII
}
return result;
}
/**
* Compute the value contained in a byte buffer. If the most
* significant bit of the first byte in the buffer is set, this
* bit is ignored and the rest of the buffer is interpreted as a
* binary number. Otherwise, the buffer is interpreted as an
* octal number as per the parseOctal function above.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @return The long value of the octal or binary string.
* @throws IllegalArgumentException if the trailing space/NUL is
* missing or an invalid byte is detected in an octal number, or
* if a binary number would exceed the size of a signed long
* 64-bit integer.
*/
private long parseOctalOrBinary(final byte[] buffer, final int offset, final int length) {
if ((buffer[offset] & 0x80) == 0) {
return parseOctal(buffer, offset, length);
}
final boolean negative = buffer[offset] == (byte) 0xff;
if (length < 9) {
return parseBinaryLong(buffer, offset, length, negative);
}
return parseBinaryBigInteger(buffer, offset, length, negative);
}
private long parseBinaryLong(final byte[] buffer, final int offset, final int length, final boolean negative) {
if (length >= 9) {
throw new IllegalArgumentException("At offset " + offset + ", "
+ length + " byte binary number"
+ " exceeds maximum signed long"
+ " value");
}
long val = 0;
for (int i = 1; i < length; i++) {
val = (val << 8) + (buffer[offset + i] & 0xff);
}
if (negative) {
// 2's complement
val--;
val ^= ((long) Math.pow(2, (length - 1) * 8) - 1);
}
return negative ? -val : val;
}
private long parseBinaryBigInteger(final byte[] buffer, final int offset, final int length, final boolean negative) {
byte[] remainder = new byte[length - 1];
System.arraycopy(buffer, offset + 1, remainder, 0, length - 1);
BigInteger val = new BigInteger(remainder);
if (negative) {
// 2's complement
val = val.add(BigInteger.valueOf(-1)).not();
}
if (val.bitLength() > 63) {
throw new IllegalArgumentException("At offset " + offset + ", "
+ length + " byte binary number"
+ " exceeds maximum signed long"
+ " value");
}
return negative ? -val.longValue() : val.longValue();
}
/**
* Parse a boolean byte from a buffer.
* Leading spaces and NUL are ignored.
* The buffer may contain trailing spaces or NULs.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @return The boolean value of the bytes.
* @throws IllegalArgumentException if an invalid byte is detected.
*/
private boolean parseBoolean(final byte[] buffer, final int offset) {
return buffer[offset] == 1;
}
private String exceptionMessage(byte[] buffer, final int offset, final int length, int current, final byte currentByte) {
String string = new String(buffer, offset, length); // TODO default charset?
string = string.replaceAll("\0", "{NUL}"); // Replace NULs to allow string to be printed
return "Invalid byte " + currentByte + " at offset " + (current - offset) + " in '" + string + "' len=" + length;
}
/**
* Parse an entry name from a buffer.
* Parsing stops when a NUL is found
* or the buffer length is reached.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @return The entry name.
*/
private String parseName(byte[] buffer, final int offset, final int length) {
try {
return parseName(buffer, offset, length, ArchiveUtils.DEFAULT_ENCODING);
} catch (IOException ex) {
try {
return parseName(buffer, offset, length, ArchiveUtils.FALLBACK_ENCODING);
} catch (IOException ex2) {
// impossible
throw new RuntimeException(ex2);
}
}
}
/**
* Parse an entry name from a buffer.
* Parsing stops when a NUL is found
* or the buffer length is reached.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @param encoding name of the encoding to use for file names
* @return The entry name.
*/
private String parseName(byte[] buffer, final int offset, final int length, final ArchiveEntryEncoding encoding) throws IOException {
int len = length;
for (; len > 0; len--) {
if (buffer[offset + len - 1] != 0) {
break;
}
}
if (len > 0) {
byte[] b = new byte[len];
System.arraycopy(buffer, offset, b, 0, len);
return encoding.decode(b);
}
return "";
}
private long getSize(byte[] header, int offset, int length) {
long test = parseOctal(header, offset, length);
if (test <= 0 && header[offset] == (byte) 128) {
byte[] last = new byte[length];
System.arraycopy(header, offset, last, 0, length);
last[0] = (byte) 0;
long rSize = new BigInteger(last).longValue();
last = null;
return rSize;
}
return test;
}
private String parseFileName(byte[] header) {
StringBuilder result = new StringBuilder(256);
// If header[345] is not equal to zero, then it is the "prefix"
// that 'ustar' defines. It must be prepended to the "normal"
// name field. We are responsible for the separating '/'.
if (header[345] != 0) {
for (int i = 345; i < 500 && header[i] != 0; ++i) {
result.append((char) header[i]);
}
result.append("/");
}
for (int i = 0; i < 100 && header[i] != 0; ++i) {
result.append((char) header[i]);
}
return result.toString();
}
}