/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* * This package is based on the work done by Timothy Gerard Endres * (time@ice.com) to whom the Ant project is very grateful for his great code. */ /* * Modifications (Michael Christen) * - replaced StringBuffer with StringBuilder */ package org.apache.tools.tar; import java.io.File; import java.util.Date; import java.util.Locale; /** * 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; ** */ public class TarEntry implements TarConstants { /** The entry's name. */ private StringBuilder 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 StringBuilder linkName; /** The entry's magic tag. */ private StringBuilder magic; /** The entry's user name. */ private StringBuilder userName; /** The entry's group name. */ private StringBuilder groupName; /** The entry's major device number. */ private int devMajor; /** The entry's minor device number. */ private int devMinor; /** The entry's file reference */ private File file; /** 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; /** * Construct an empty entry and prepares the header values. */ private TarEntry () { this.magic = new StringBuilder(TMAGIC); this.name = new StringBuilder(); this.linkName = new StringBuilder(); 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 = new StringBuilder(user); this.groupName = new StringBuilder(""); this.file = null; } /** * 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 TarEntry(final String name) { this(); final boolean isDir = name.endsWith("/"); this.devMajor = 0; this.devMinor = 0; this.name = new StringBuilder(name); this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; this.linkFlag = isDir ? LF_DIR : LF_NORMAL; this.userId = 0; this.groupId = 0; this.size = 0; this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND; this.linkName = new StringBuilder(""); this.userName = new StringBuilder(""); this.groupName = new StringBuilder(""); this.devMajor = 0; this.devMinor = 0; } /** * Construct an entry with a name an a link flag. * * @param name the entry name * @param linkFlag the entry link flag. */ public TarEntry(final String name, final byte linkFlag) { this(name); this.linkFlag = linkFlag; } /** * 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(final File file) { this(); this.file = file; String fileName = file.getPath(); final String osname = System.getProperty("os.name").toLowerCase(Locale.US); if (osname != null) { // Strip off drive letters! // REVIEW Would a better check be "(File.separator == '\')"? if (osname.startsWith("windows")) { if (fileName.length() > 2) { final char ch1 = fileName.charAt(0); final char ch2 = fileName.charAt(1); if (ch2 == ':' && ((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z'))) { fileName = fileName.substring(2); } } } else if (osname.indexOf("netware",0) > -1) { final int colon = fileName.indexOf(':'); if (colon != -1) { fileName = fileName.substring(colon + 1); } } } fileName = fileName.replace(File.separatorChar, '/'); // No absolute pathnames // Windows (and Posix?) paths can start with "\\NetworkDrive\", // so we loop on starting /'s. while (fileName.length() > 0 && fileName.charAt(0) == '/') { fileName = fileName.substring(1); } this.linkName = new StringBuilder(""); this.name = new StringBuilder(fileName); if (file.isDirectory()) { this.mode = DEFAULT_DIR_MODE; this.linkFlag = LF_DIR; if (this.name.charAt(this.name.length() - 1) != '/') { this.name.append("/"); } } else { this.mode = DEFAULT_FILE_MODE; this.linkFlag = LF_NORMAL; } this.size = file.length(); this.modTime = file.lastModified() / MILLIS_PER_SECOND; this.devMajor = 0; this.devMinor = 0; } /** * 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(final byte[] headerBuf) { this(); parseTarHeader(headerBuf); } /** * 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(final Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof TarEntry)) return false; final TarEntry other = (TarEntry) obj; return getName().equals(other.getName()); } /** * 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(final TarEntry desc) { return desc.getName().startsWith(getName()); } /** * Get this entry's name. * * @return This entry's name. */ public String getName() { return this.name.toString(); } /** * Set this entry's name. * * @param name This entry's new name. */ public void setName(final String name) { this.name = new StringBuilder(name); } /** * Set the mode for this entry * * @param mode the mode for this entry */ public void setMode(final int mode) { this.mode = mode; } /** * Get this entry's link name. * * @return This entry's link name. */ public String getLinkName() { return this.linkName.toString(); } /** * Get this entry's user id. * * @return This entry's user id. */ public int getUserId() { return this.userId; } /** * Set this entry's user id. * * @param userId This entry's new user id. */ public void setUserId(final int userId) { this.userId = userId; } /** * Get this entry's group id. * * @return This entry's group id. */ public int getGroupId() { return this.groupId; } /** * Set this entry's group id. * * @param groupId This entry's new group id. */ public void setGroupId(final int groupId) { this.groupId = groupId; } /** * Get this entry's user name. * * @return This entry's user name. */ public String getUserName() { return this.userName.toString(); } /** * Set this entry's user name. * * @param userName This entry's new user name. */ public void setUserName(final String userName) { this.userName = new StringBuilder(userName); } /** * Get this entry's group name. * * @return This entry's group name. */ public String getGroupName() { return this.groupName.toString(); } /** * Set this entry's group name. * * @param groupName This entry's new group name. */ public void setGroupName(final String groupName) { this.groupName = new StringBuilder(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(final int userId, final 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(final String userName, final String groupName) { setUserName(userName); 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(final long time) { this.modTime = time / MILLIS_PER_SECOND; } /** * Set this entry's modification time. * * @param time This entry's new modification time. */ public void setModTime(final Date time) { this.modTime = time.getTime() / MILLIS_PER_SECOND; } /** * Set this entry's modification time. * * @return time This entry's new modification time. */ public Date getModTime() { return new Date(this.modTime * MILLIS_PER_SECOND); } /** * Get this entry's file. * * @return This entry's file. */ public File getFile() { return this.file; } /** * Get this entry's mode. * * @return This entry's mode. */ public int getMode() { return this.mode; } /** * Get this entry's file size. * * @return This entry's file size. */ public long getSize() { return this.size; } /** * Set this entry's file size. * * @param size This entry's new file size. */ public void setSize(final long size) { this.size = size; } /** * 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 this.linkFlag == LF_GNUTYPE_LONGNAME && this.name.toString().equals(GNU_LONGLINK); } /** * 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.linkFlag == LF_DIR) { return true; } if (getName().endsWith("/")) { return true; } return false; } /** * 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() { if (this.file == null || !this.file.isDirectory()) { return new TarEntry[0]; } final String[] list = this.file.list(); final 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; } /** * Write an entry's header information to a header buffer. * * @param outbuf The tar entry header buffer to fill in. */ public void writeEntryHeader(final byte[] outbuf) { int offset = 0; offset = TarUtils.getNameBytes(this.name, outbuf, offset, NAMELEN); offset = TarUtils.getOctalBytes(this.mode, outbuf, offset, MODELEN); offset = TarUtils.getOctalBytes(this.userId, outbuf, offset, UIDLEN); offset = TarUtils.getOctalBytes(this.groupId, outbuf, offset, GIDLEN); offset = TarUtils.getLongOctalBytes(this.size, outbuf, offset, SIZELEN); offset = TarUtils.getLongOctalBytes(this.modTime, outbuf, offset, MODTIMELEN); final int csOffset = offset; for (int c = 0; c < CHKSUMLEN; ++c) { outbuf[offset++] = (byte) ' '; } outbuf[offset++] = this.linkFlag; offset = TarUtils.getNameBytes(this.linkName, outbuf, offset, NAMELEN); offset = TarUtils.getNameBytes(this.magic, outbuf, offset, MAGICLEN); offset = TarUtils.getNameBytes(this.userName, outbuf, offset, UNAMELEN); offset = TarUtils.getNameBytes(this.groupName, outbuf, offset, GNAMELEN); offset = TarUtils.getOctalBytes(this.devMajor, outbuf, offset, DEVLEN); offset = TarUtils.getOctalBytes(this.devMinor, outbuf, offset, DEVLEN); while (offset < outbuf.length) { outbuf[offset++] = 0; } final long chk = TarUtils.computeCheckSum(outbuf); TarUtils.getCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); } /** * Parse an entry's header information from a header buffer. * * @param header The tar entry header buffer to get information from. */ public void parseTarHeader(final byte[] header) { int offset = 0; this.name = TarUtils.parseName(header, offset, NAMELEN); offset += NAMELEN; this.mode = (int) TarUtils.parseOctal(header, offset, MODELEN); offset += MODELEN; this.userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); offset += UIDLEN; this.groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); offset += GIDLEN; this.size = TarUtils.parseOctal(header, offset, SIZELEN); offset += SIZELEN; this.modTime = TarUtils.parseOctal(header, offset, MODTIMELEN); offset += MODTIMELEN; offset += CHKSUMLEN; this.linkFlag = header[offset++]; this.linkName = TarUtils.parseName(header, offset, NAMELEN); offset += NAMELEN; this.magic = TarUtils.parseName(header, offset, MAGICLEN); offset += MAGICLEN; this.userName = TarUtils.parseName(header, offset, UNAMELEN); offset += UNAMELEN; this.groupName = TarUtils.parseName(header, offset, GNAMELEN); offset += GNAMELEN; this.devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN); offset += DEVLEN; this.devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN); } }