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