/*
** Authored by Timothy Gerard Endres
**
**
** 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, * href="mailto:time@gjt.org">time@trustice.com.
* @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 *
* * * 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 *
* * * 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.
*
* 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.
*
* 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.
*
* 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.
*
*
* The C structure for a Tar Entry's header is:
*
*
* 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;
*
*
* @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.
*
* 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, * href="mailto:time@gjt.org">time@trustice.com.
* @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);
}
}