Return-Path: X-Original-To: apmail-ant-notifications-archive@minotaur.apache.org Delivered-To: apmail-ant-notifications-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 2C1869F7F for ; Sat, 16 Jun 2012 04:13:08 +0000 (UTC) Received: (qmail 82642 invoked by uid 500); 16 Jun 2012 04:13:07 -0000 Delivered-To: apmail-ant-notifications-archive@ant.apache.org Received: (qmail 82431 invoked by uid 500); 16 Jun 2012 04:13:05 -0000 Mailing-List: contact notifications-help@ant.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@ant.apache.org Delivered-To: mailing list notifications@ant.apache.org Received: (qmail 82354 invoked by uid 99); 16 Jun 2012 04:13:02 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 16 Jun 2012 04:13:02 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 16 Jun 2012 04:12:59 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id DD7C22388962 for ; Sat, 16 Jun 2012 04:12:38 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1350857 [1/2] - in /ant/core/trunk: ./ src/main/org/apache/tools/tar/ src/main/org/apache/tools/zip/ Date: Sat, 16 Jun 2012 04:12:38 -0000 To: notifications@ant.apache.org From: bodewig@apache.org X-Mailer: svnmailer-1.0.8-patched Message-Id: <20120616041238.DD7C22388962@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Author: bodewig Date: Sat Jun 16 04:12:37 2012 New Revision: 1350857 URL: http://svn.apache.org/viewvc?rev=1350857&view=rev Log: merge tar package from Compress, bringing some POSIX tar support Added: ant/core/trunk/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java - copied, changed from r1348527, commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java Modified: ant/core/trunk/WHATSNEW ant/core/trunk/src/main/org/apache/tools/tar/TarBuffer.java ant/core/trunk/src/main/org/apache/tools/tar/TarConstants.java ant/core/trunk/src/main/org/apache/tools/tar/TarEntry.java ant/core/trunk/src/main/org/apache/tools/tar/TarInputStream.java ant/core/trunk/src/main/org/apache/tools/tar/TarOutputStream.java ant/core/trunk/src/main/org/apache/tools/tar/TarUtils.java ant/core/trunk/src/main/org/apache/tools/zip/ZipEncoding.java ant/core/trunk/src/main/org/apache/tools/zip/ZipEncodingHelper.java Modified: ant/core/trunk/WHATSNEW URL: http://svn.apache.org/viewvc/ant/core/trunk/WHATSNEW?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/WHATSNEW (original) +++ ant/core/trunk/WHATSNEW Sat Jun 16 04:12:37 2012 @@ -49,6 +49,9 @@ Other changes: Java VMs. Bugzilla Report 52706. + * merged the TAR package from Commons Compress, it can now read + archives using POSIX extension headers and STAR extensions. + Changes from Ant 1.8.3 TO Ant 1.8.4 =================================== Copied: ant/core/trunk/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java (from r1348527, commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java) URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java?p2=ant/core/trunk/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java&p1=commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java&r1=1348527&r2=1350857&rev=1350857&view=diff ============================================================================== --- commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java Sat Jun 16 04:12:37 2012 @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.commons.compress.archivers.tar; +package org.apache.tools.tar; import java.io.IOException; Modified: ant/core/trunk/src/main/org/apache/tools/tar/TarBuffer.java URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarBuffer.java?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/src/main/org/apache/tools/tar/TarBuffer.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarBuffer.java Sat Jun 16 04:12:37 2012 @@ -51,12 +51,13 @@ public class TarBuffer { private InputStream inStream; private OutputStream outStream; - private byte[] blockBuffer; + private final int blockSize; + private final int recordSize; + private final int recsPerBlock; + private final byte[] blockBuffer; + private int currBlkIdx; private int currRecIdx; - private int blockSize; - private int recordSize; - private int recsPerBlock; private boolean debug; /** @@ -83,10 +84,7 @@ public class TarBuffer { * @param recordSize the record size to use */ public TarBuffer(InputStream inStream, int blockSize, int recordSize) { - this.inStream = inStream; - this.outStream = null; - - this.initialize(blockSize, recordSize); + this(inStream, null, blockSize, recordSize); } /** @@ -113,16 +111,15 @@ public class TarBuffer { * @param recordSize the record size to use */ public TarBuffer(OutputStream outStream, int blockSize, int recordSize) { - this.inStream = null; - this.outStream = outStream; - - this.initialize(blockSize, recordSize); + this(null, outStream, blockSize, recordSize); } /** - * Initialization common to all constructors. + * Private constructor to perform common setup. */ - private void initialize(int blockSize, int recordSize) { + private TarBuffer(InputStream inStream, OutputStream outStream, int blockSize, int recordSize) { + this.inStream = inStream; + this.outStream = outStream; this.debug = false; this.blockSize = blockSize; this.recordSize = recordSize; @@ -194,10 +191,8 @@ public class TarBuffer { throw new IOException("reading (via skip) from an output buffer"); } - if (currRecIdx >= recsPerBlock) { - if (!readBlock()) { - return; // UNDONE - } + if (currRecIdx >= recsPerBlock && !readBlock()) { + return; // UNDONE } currRecIdx++; @@ -216,13 +211,14 @@ public class TarBuffer { } if (inStream == null) { + if (outStream == null) { + throw new IOException("input buffer is closed"); + } throw new IOException("reading from an output buffer"); } - if (currRecIdx >= recsPerBlock) { - if (!readBlock()) { - return null; - } + if (currRecIdx >= recsPerBlock && !readBlock()) { + return null; } byte[] result = new byte[recordSize]; @@ -337,6 +333,9 @@ public class TarBuffer { } if (outStream == null) { + if (inStream == null){ + throw new IOException("Output buffer is closed"); + } throw new IOException("writing to an input buffer"); } @@ -374,6 +373,9 @@ public class TarBuffer { } if (outStream == null) { + if (inStream == null){ + throw new IOException("Output buffer is closed"); + } throw new IOException("writing to an input buffer"); } @@ -454,9 +456,8 @@ public class TarBuffer { } else if (inStream != null) { if (inStream != System.in) { inStream.close(); - - inStream = null; } + inStream = null; } } } Modified: ant/core/trunk/src/main/org/apache/tools/tar/TarConstants.java URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarConstants.java?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/src/main/org/apache/tools/tar/TarConstants.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarConstants.java Sat Jun 16 04:12:37 2012 @@ -26,11 +26,23 @@ package org.apache.tools.tar; /** * This interface contains all the definitions used in the package. * + * For tar formats (FORMAT_OLDGNU, FORMAT_POSIX, etc.) see GNU tar + * tar.h type enum archive_format */ // CheckStyle:InterfaceIsTypeCheck OFF (bc) public interface TarConstants { /** + * GNU format as per before tar 1.12. + */ + int FORMAT_OLDGNU = 2; + + /** + * Pure Posix format. + */ + int FORMAT_POSIX = 3; + + /** * The length of the name field in a header buffer. */ int NAMELEN = 100; @@ -51,26 +63,49 @@ public interface TarConstants { int GIDLEN = 8; /** + * The maximum value of gid/uid in a tar archive which can + * be expressed in octal char notation (that's 7 sevens, octal). + */ + long MAXID = 07777777L; + + /** * The length of the checksum field in a header buffer. */ int CHKSUMLEN = 8; /** * The length of the size field in a header buffer. + * Includes the trailing space or NUL. */ int SIZELEN = 12; /** - * The maximum size of a file in a tar archive (That's 11 sevens, octal). + * The maximum size of a file in a tar archive + * which can be expressed in octal char notation (that's 11 sevens, octal). */ long MAXSIZE = 077777777777L; + /** Offset of start of magic field within header record */ + int MAGIC_OFFSET = 257; /** - * The length of the magic field in a header buffer. + * The length of the magic field in a header buffer including the version. */ int MAGICLEN = 8; /** + * The length of the magic field in a header buffer. + */ + int PURE_MAGICLEN = 6; + + /** Offset of start of magic field within header record */ + int VERSION_OFFSET = 263; + /** + * Previously this was regarded as part of "magic" field, but it + * is separate. + */ + int VERSIONLEN = 2; + + /** * The length of the modification time field in a header buffer. */ int MODTIMELEN = 12; @@ -86,11 +121,77 @@ public interface TarConstants { int GNAMELEN = 32; /** - * The length of the devices field in a header buffer. + * The length of each of the device fields (major and minor) in a header buffer. */ int DEVLEN = 8; /** + * Length of the prefix field. + * + */ + int PREFIXLEN = 155; + + /** + * The length of the access time field in an old GNU header buffer. + * + */ + int ATIMELEN_GNU = 12; + + /** + * The length of the created time field in an old GNU header buffer. + * + */ + int CTIMELEN_GNU = 12; + + /** + * The length of the multivolume start offset field in an old GNU header buffer. + * + */ + int OFFSETLEN_GNU = 12; + + /** + * The length of the long names field in an old GNU header buffer. + * + */ + int LONGNAMESLEN_GNU = 4; + + /** + * The length of the padding field in an old GNU header buffer. + * + */ + int PAD2LEN_GNU = 1; + + /** + * The sum of the length of all sparse headers in an old GNU header buffer. + * + */ + int SPARSELEN_GNU = 96; + + /** + * The length of the is extension field in an old GNU header buffer. + * + */ + int ISEXTENDEDLEN_GNU = 1; + + /** + * The length of the real size field in an old GNU header buffer. + * + */ + int REALSIZELEN_GNU = 12; + + /** + * The sum of the length of all sparse headers in a sparse header buffer. + * + */ + int SPARSELEN_GNU_SPARSE = 504; + + /** + * The length of the is extension field in a sparse header buffer. + * + */ + int ISEXTENDEDLEN_GNU_SPARSE = 1; + + /** * 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. */ @@ -137,22 +238,51 @@ public interface TarConstants { byte LF_CONTIG = (byte) '7'; /** - * The magic tag representing a POSIX tar archive. + * Identifies the *next* file on the tape as having a long name. */ + byte LF_GNUTYPE_LONGNAME = (byte) 'L'; + + /** + * Sparse file type. + */ + byte LF_GNUTYPE_SPARSE = (byte) 'S'; + + // See "http://www.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_04_100_13_02" + + /** + * Identifies the entry as a Pax extended header. + */ + byte LF_PAX_EXTENDED_HEADER_LC = (byte) 'x'; + + /** + * Identifies the entry as a Pax extended header (SunOS tar -E). + */ + byte LF_PAX_EXTENDED_HEADER_UC = (byte) 'X'; + + /** + * Identifies the entry as a Pax global extended header. + */ + byte LF_PAX_GLOBAL_EXTENDED_HEADER = (byte) 'g'; + String TMAGIC = "ustar"; /** + * The magic tag representing a POSIX tar archive. + */ + String MAGIC_POSIX = "ustar\0"; + String VERSION_POSIX = "00"; + + /** * The magic tag representing a GNU tar archive. */ String GNU_TMAGIC = "ustar "; + // Appear to be two possible GNU versions + String VERSION_GNU_SPACE = " \0"; + String VERSION_GNU_ZERO = "0\0"; /** - * The namr of the GNU tar entry which contains a long name. + * The name of the GNU tar entry which contains a long name. */ String GNU_LONGLINK = "././@LongLink"; - /** - * Identifies the *next* file on the tape as having a long name. - */ - byte LF_GNUTYPE_LONGNAME = (byte) 'L'; } Modified: ant/core/trunk/src/main/org/apache/tools/tar/TarEntry.java URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarEntry.java?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/src/main/org/apache/tools/tar/TarEntry.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarEntry.java Sat Jun 16 04:12:37 2012 @@ -24,9 +24,13 @@ package org.apache.tools.tar; import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.Locale; +import org.apache.tools.zip.ZipEncoding; + /** * This class represents an entry in a Tar archive. It consists * of the entry's header, as well as the entry's File. Entries @@ -72,13 +76,44 @@ import java.util.Locale; * char devmajor[8]; * char devminor[8]; * } header; + * All unused bytes are set to null. + * New-style GNU tar files are slightly different from the above. + * For values of size larger than 077777777777L (11 7s) + * or uid and gid larger than 07777777L (7 7s) + * the sign bit of the first byte is set, and the rest of the + * field is the binary representation of the number. + * See TarUtils.parseOctalOrBinary. + * + * + *

+ * The C structure for a old GNU Tar Entry's header is: + *

+ * struct oldgnu_header {
+ * char unused_pad1[345]; // TarConstants.PAD1LEN_GNU       - offset 0
+ * char atime[12];        // TarConstants.ATIMELEN_GNU      - offset 345
+ * char ctime[12];        // TarConstants.CTIMELEN_GNU      - offset 357
+ * char offset[12];       // TarConstants.OFFSETLEN_GNU     - offset 369
+ * char longnames[4];     // TarConstants.LONGNAMESLEN_GNU  - offset 381
+ * char unused_pad2;      // TarConstants.PAD2LEN_GNU       - offset 385
+ * struct sparse sp[4];   // TarConstants.SPARSELEN_GNU     - offset 386
+ * char isextended;       // TarConstants.ISEXTENDEDLEN_GNU - offset 482
+ * char realsize[12];     // TarConstants.REALSIZELEN_GNU   - offset 483
+ * char unused_pad[17];   // TarConstants.PAD3LEN_GNU       - offset 495
+ * };
+ * 
+ * Whereas, "struct sparse" is: + *
+ * struct sparse {
+ * char offset[12];   // offset 0
+ * char numbytes[12]; // offset 12
+ * };
  * 
* */ public class TarEntry implements TarConstants { /** The entry's name. */ - private StringBuffer name; + private String name; /** The entry's permission mode. */ private int mode; @@ -99,16 +134,18 @@ public class TarEntry implements TarCons private byte linkFlag; /** The entry's link name. */ - private StringBuffer linkName; + private String linkName; /** The entry's magic tag. */ - private StringBuffer magic; + private String magic; + /** The version of the format */ + private String version; /** The entry's user name. */ - private StringBuffer userName; + private String userName; /** The entry's group name. */ - private StringBuffer groupName; + private String groupName; /** The entry's major device number. */ private int devMajor; @@ -116,6 +153,12 @@ public class TarEntry implements TarCons /** The entry's minor device number. */ private int devMinor; + /** If an extension sparse header follows. */ + private boolean isExtended; + + /** The entry's real size in case of a sparse file. */ + private long realSize; + /** The entry's file reference */ private File file; @@ -134,10 +177,11 @@ public class TarEntry implements TarCons /** * Construct an empty entry and prepares the header values. */ - private TarEntry () { - this.magic = new StringBuffer(TMAGIC); - this.name = new StringBuffer(); - this.linkName = new StringBuffer(); + private TarEntry() { + this.magic = MAGIC_POSIX; + this.version = VERSION_POSIX; + this.name = ""; + this.linkName = ""; String user = System.getProperty("user.name", ""); @@ -147,8 +191,8 @@ public class TarEntry implements TarCons this.userId = 0; this.groupId = 0; - this.userName = new StringBuffer(user); - this.groupName = new StringBuffer(""); + this.userName = user; + this.groupName = ""; this.file = null; } @@ -178,19 +222,16 @@ public class TarEntry implements TarCons this.devMajor = 0; this.devMinor = 0; - this.name = new StringBuffer(name); + this.name = 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 StringBuffer(""); - this.userName = new StringBuffer(""); - this.groupName = new StringBuffer(""); - this.devMajor = 0; - this.devMinor = 0; - + this.linkName = ""; + this.userName = ""; + this.groupName = ""; } /** @@ -203,38 +244,52 @@ public class TarEntry implements TarCons this(name); this.linkFlag = linkFlag; if (linkFlag == LF_GNUTYPE_LONGNAME) { - magic = new StringBuffer(GNU_TMAGIC); + magic = GNU_TMAGIC; + version = VERSION_GNU_SPACE; } } /** * Construct an entry for a file. File is set to file, and the * header is constructed from information from the file. + * The name is set from the normalized file path. * * @param file The file that the entry represents. */ public TarEntry(File file) { + this(file, normalizeFileName(file.getPath(), false)); + } + + /** + * Construct an entry for a file. File is set to file, and the + * header is constructed from information from the file. + * + * @param file The file that the entry represents. + * @param fileName the name to be used for the entry. + */ + public TarEntry(File file, String fileName) { this(); this.file = file; - String fileName = normalizeFileName(file.getPath(), false); - this.linkName = new StringBuffer(""); - this.name = new StringBuffer(fileName); + this.linkName = ""; if (file.isDirectory()) { this.mode = DEFAULT_DIR_MODE; this.linkFlag = LF_DIR; - int nameLength = name.length(); - if (nameLength == 0 || name.charAt(nameLength - 1) != '/') { - this.name.append("/"); + int nameLength = fileName.length(); + if (nameLength == 0 || fileName.charAt(nameLength - 1) != '/') { + this.name = fileName + "/"; + } else { + this.name = fileName; } this.size = 0; } else { this.mode = DEFAULT_FILE_MODE; this.linkFlag = LF_NORMAL; this.size = file.length(); + this.name = fileName; } this.modTime = file.lastModified() / MILLIS_PER_SECOND; @@ -247,6 +302,7 @@ public class TarEntry implements TarCons * to null. * * @param headerBuf The header bytes from a tar archive entry. + * @throws IllegalArgumentException if any of the numeric fields have an invalid format */ public TarEntry(byte[] headerBuf) { this(); @@ -254,6 +310,20 @@ public class TarEntry implements TarCons } /** + * 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 TarEntry(byte[] headerBuf, ZipEncoding encoding) + throws IOException { + this(); + parseTarHeader(headerBuf, encoding); + } + + /** * Determine if the two entries are equal. Equality is determined * by the header names being equal. * @@ -271,6 +341,7 @@ public class TarEntry implements TarCons * @param it Entry to be checked for equality. * @return True if the entries are equal. */ + @Override public boolean equals(Object it) { if (it == null || getClass() != it.getClass()) { return false; @@ -283,6 +354,7 @@ public class TarEntry implements TarCons * * @return the entry hashcode */ + @Override public int hashCode() { return getName().hashCode(); } @@ -314,7 +386,7 @@ public class TarEntry implements TarCons * @param name This entry's new name. */ public void setName(String name) { - this.name = new StringBuffer(normalizeFileName(name, false)); + this.name = normalizeFileName(name, false); } /** @@ -336,6 +408,15 @@ public class TarEntry implements TarCons } /** + * 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. @@ -386,7 +467,7 @@ public class TarEntry implements TarCons * @param userName This entry's new user name. */ public void setUserName(String userName) { - this.userName = new StringBuffer(userName); + this.userName = userName; } /** @@ -404,7 +485,7 @@ public class TarEntry implements TarCons * @param groupName This entry's new group name. */ public void setGroupName(String groupName) { - this.groupName = new StringBuffer(groupName); + this.groupName = groupName; } /** @@ -488,11 +569,88 @@ public class TarEntry implements TarCons * Set this entry's file size. * * @param size This entry's new file size. + * @throws IllegalArgumentException if the size is < 0. */ public void setSize(long size) { + if (size < 0){ + throw new IllegalArgumentException("Size is out of range: "+size); + } this.size = size; } + /** + * 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 @@ -501,7 +659,26 @@ public class TarEntry implements TarCons */ public boolean isGNULongNameEntry() { return linkFlag == LF_GNUTYPE_LONGNAME - && name.toString().equals(GNU_LONGLINK); + && name.equals(GNU_LONGLINK); + } + + /** + * Check if this is a Pax header. + * + * @return {@code true} if this is a Pax header. + */ + public boolean isPaxHeader(){ + return linkFlag == LF_PAX_EXTENDED_HEADER_LC + || linkFlag == LF_PAX_EXTENDED_HEADER_UC; + } + + /** + * Check if this is a Pax header. + * + * @return {@code true} if this is a Pax header. + */ + public boolean isGlobalPaxHeader(){ + return linkFlag == LF_PAX_GLOBAL_EXTENDED_HEADER; } /** @@ -526,6 +703,54 @@ public class TarEntry implements TarCons } /** + * Check if this is a "normal file" + */ + public boolean isFile() { + if (file != null) { + return file.isFile(); + } + if (linkFlag == LF_OLDNORM || linkFlag == LF_NORMAL) { + return true; + } + return !getName().endsWith("/"); + } + + /** + * Check if this is a symbolic link entry. + */ + public boolean isSymbolicLink() { + return linkFlag == LF_SYMLINK; + } + + /** + * Check if this is a link entry. + */ + public boolean isLink() { + return linkFlag == LF_LINK; + } + + /** + * Check if this is a character device entry. + */ + public boolean isCharacterDevice() { + return linkFlag == LF_CHR; + } + + /** + * Check if this is a block device entry. + */ + public boolean isBlockDevice() { + return linkFlag == LF_BLK; + } + + /** + * Check if this is a FIFO (pipe) entry. + */ + public boolean isFIFO() { + return linkFlag == LF_FIFO; + } + + /** * If this entry represents a file, and the file is a directory, return * an array of TarEntries for this entry's children. * @@ -549,17 +774,46 @@ public class TarEntry implements TarCons /** * Write an entry's header information to a header buffer. * + *

This method does not use the star/GNU tar/BSD tar extensions.

+ * * @param outbuf The tar entry header buffer to fill in. */ public void writeEntryHeader(byte[] outbuf) { + try { + writeEntryHeader(outbuf, TarUtils.DEFAULT_ENCODING, false); + } catch (IOException ex) { + try { + writeEntryHeader(outbuf, TarUtils.FALLBACK_ENCODING, false); + } catch (IOException ex2) { + // impossible + throw new RuntimeException(ex2); + } + } + } + + /** + * Write an entry's header information to a header buffer. + * + * @param outbuf The tar entry header buffer to fill in. + * @param encoding encoding to use when writing the file name. + * @param starMode whether to use the star/GNU tar/BSD tar + * extension for numeric fields if their value doesn't fit in the + * maximum size of standard tar archives + */ + public void writeEntryHeader(byte[] outbuf, ZipEncoding encoding, + boolean starMode) throws IOException { int offset = 0; - offset = TarUtils.getNameBytes(name, outbuf, offset, NAMELEN); - offset = TarUtils.getOctalBytes(mode, outbuf, offset, MODELEN); - offset = TarUtils.getOctalBytes(userId, outbuf, offset, UIDLEN); - offset = TarUtils.getOctalBytes(groupId, outbuf, offset, GIDLEN); - offset = TarUtils.getLongOctalBytes(size, outbuf, offset, SIZELEN); - offset = TarUtils.getLongOctalBytes(modTime, outbuf, offset, MODTIMELEN); + offset = TarUtils.formatNameBytes(name, outbuf, offset, NAMELEN, + encoding); + offset = writeEntryHeaderField(mode, outbuf, offset, MODELEN, starMode); + offset = writeEntryHeaderField(userId, outbuf, offset, UIDLEN, + starMode); + offset = writeEntryHeaderField(groupId, outbuf, offset, GIDLEN, + starMode); + offset = writeEntryHeaderField(size, outbuf, offset, SIZELEN, starMode); + offset = writeEntryHeaderField(modTime, outbuf, offset, MODTIMELEN, + starMode); int csOffset = offset; @@ -568,12 +822,18 @@ public class TarEntry implements TarCons } outbuf[offset++] = linkFlag; - offset = TarUtils.getNameBytes(linkName, outbuf, offset, NAMELEN); - offset = TarUtils.getNameBytes(magic, outbuf, offset, MAGICLEN); - offset = TarUtils.getNameBytes(userName, outbuf, offset, UNAMELEN); - offset = TarUtils.getNameBytes(groupName, outbuf, offset, GNAMELEN); - offset = TarUtils.getOctalBytes(devMajor, outbuf, offset, DEVLEN); - offset = TarUtils.getOctalBytes(devMinor, outbuf, offset, DEVLEN); + offset = TarUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN, + encoding); + offset = TarUtils.formatNameBytes(magic, outbuf, offset, PURE_MAGICLEN); + offset = TarUtils.formatNameBytes(version, outbuf, offset, VERSIONLEN); + offset = TarUtils.formatNameBytes(userName, outbuf, offset, UNAMELEN, + encoding); + offset = TarUtils.formatNameBytes(groupName, outbuf, offset, GNAMELEN, + encoding); + offset = writeEntryHeaderField(devMajor, outbuf, offset, DEVLEN, + starMode); + offset = writeEntryHeaderField(devMinor, outbuf, offset, DEVLEN, + starMode); while (offset < outbuf.length) { outbuf[offset++] = 0; @@ -581,42 +841,122 @@ public class TarEntry implements TarCons long chk = TarUtils.computeCheckSum(outbuf); - TarUtils.getCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); + TarUtils.formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); + } + + private int writeEntryHeaderField(long value, byte[] outbuf, int offset, + int length, boolean starMode) { + if (!starMode && (value < 0 + || value >= (1l << (3 * (length - 1))))) { + // value doesn't fit into field when written as octal + // number, will be written to PAX header or causes an + // error + return TarUtils.formatLongOctalBytes(0, outbuf, offset, length); + } + return TarUtils.formatLongOctalOrBinaryBytes(value, outbuf, offset, + length); } /** * Parse an entry's header information from a header buffer. * * @param header The tar entry header buffer to get information from. + * @throws IllegalArgumentException if any of the numeric fields have an invalid format */ public void parseTarHeader(byte[] header) { + try { + parseTarHeader(header, TarUtils.DEFAULT_ENCODING); + } catch (IOException ex) { + try { + parseTarHeader(header, TarUtils.DEFAULT_ENCODING, true); + } catch (IOException ex2) { + // not really possible + throw new RuntimeException(ex2); + } + } + } + + /** + * 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, ZipEncoding encoding) + throws IOException { + parseTarHeader(header, encoding, false); + } + + private void parseTarHeader(byte[] header, ZipEncoding encoding, + final boolean oldStyle) + throws IOException { int offset = 0; - name = TarUtils.parseName(header, offset, NAMELEN); + name = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) + : TarUtils.parseName(header, offset, NAMELEN, encoding); offset += NAMELEN; - mode = (int) TarUtils.parseOctal(header, offset, MODELEN); + mode = (int) TarUtils.parseOctalOrBinary(header, offset, MODELEN); offset += MODELEN; - userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); + userId = (int) TarUtils.parseOctalOrBinary(header, offset, UIDLEN); offset += UIDLEN; - groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); + groupId = (int) TarUtils.parseOctalOrBinary(header, offset, GIDLEN); offset += GIDLEN; - size = TarUtils.parseOctal(header, offset, SIZELEN); + size = TarUtils.parseOctalOrBinary(header, offset, SIZELEN); offset += SIZELEN; - modTime = TarUtils.parseOctal(header, offset, MODTIMELEN); + modTime = TarUtils.parseOctalOrBinary(header, offset, MODTIMELEN); offset += MODTIMELEN; offset += CHKSUMLEN; linkFlag = header[offset++]; - linkName = TarUtils.parseName(header, offset, NAMELEN); + linkName = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) + : TarUtils.parseName(header, offset, NAMELEN, encoding); offset += NAMELEN; - magic = TarUtils.parseName(header, offset, MAGICLEN); - offset += MAGICLEN; - userName = TarUtils.parseName(header, offset, UNAMELEN); + magic = TarUtils.parseName(header, offset, PURE_MAGICLEN); + offset += PURE_MAGICLEN; + version = TarUtils.parseName(header, offset, VERSIONLEN); + offset += VERSIONLEN; + userName = oldStyle ? TarUtils.parseName(header, offset, UNAMELEN) + : TarUtils.parseName(header, offset, UNAMELEN, encoding); offset += UNAMELEN; - groupName = TarUtils.parseName(header, offset, GNAMELEN); + groupName = oldStyle ? TarUtils.parseName(header, offset, GNAMELEN) + : TarUtils.parseName(header, offset, GNAMELEN, encoding); offset += GNAMELEN; - devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN); + devMajor = (int) TarUtils.parseOctalOrBinary(header, offset, DEVLEN); + offset += DEVLEN; + devMinor = (int) TarUtils.parseOctalOrBinary(header, offset, DEVLEN); offset += DEVLEN; - devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN); + + int type = evaluateType(header); + switch (type) { + case FORMAT_OLDGNU: { + offset += ATIMELEN_GNU; + offset += CTIMELEN_GNU; + offset += OFFSETLEN_GNU; + offset += LONGNAMESLEN_GNU; + offset += PAD2LEN_GNU; + offset += SPARSELEN_GNU; + isExtended = TarUtils.parseBoolean(header, offset); + offset += ISEXTENDEDLEN_GNU; + realSize = TarUtils.parseOctal(header, offset, REALSIZELEN_GNU); + offset += REALSIZELEN_GNU; + break; + } + case FORMAT_POSIX: + default: { + String prefix = oldStyle + ? TarUtils.parseName(header, offset, PREFIXLEN) + : TarUtils.parseName(header, offset, PREFIXLEN, encoding); + // SunOS tar -E does not add / to directory names, so fix + // up to be consistent + if (isDirectory() && !name.endsWith("/")){ + name = name + "/"; + } + if (prefix.length() > 0){ + name = prefix + "/" + name; + } + } + } } /** @@ -661,4 +1001,85 @@ public class TarEntry implements TarCons } return fileName; } + + /** + * 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 (matchAsciiBuffer(GNU_TMAGIC, header, MAGIC_OFFSET, PURE_MAGICLEN)) { + return FORMAT_OLDGNU; + } + if (matchAsciiBuffer(MAGIC_POSIX, header, MAGIC_OFFSET, PURE_MAGICLEN)) { + return FORMAT_POSIX; + } + return 0; + } + + /** + * Check if buffer contents matches Ascii String. + * + * @param expected + * @param buffer + * @param offset + * @param length + * @return {@code true} if buffer is the same as the expected string + */ + private static boolean matchAsciiBuffer(String expected, byte[] buffer, + int offset, int length){ + byte[] buffer1; + try { + buffer1 = expected.getBytes("ASCII"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); // Should not happen + } + return isEqual(buffer1, 0, buffer1.length, buffer, offset, length, + false); + } + + /** + * Compare byte buffers, optionally ignoring trailing nulls + * + * @param buffer1 + * @param offset1 + * @param length1 + * @param buffer2 + * @param offset2 + * @param length2 + * @param ignoreTrailingNulls + * @return {@code true} if buffer1 and buffer2 have same contents, having regard to trailing nulls + */ + private static boolean isEqual( + final byte[] buffer1, final int offset1, final int length1, + final byte[] buffer2, final int offset2, final int length2, + boolean ignoreTrailingNulls){ + int minLen=length1 < length2 ? length1 : length2; + for (int i=0; i < minLen; i++){ + if (buffer1[offset1+i] != buffer2[offset2+i]){ + return false; + } + } + if (length1 == length2){ + return true; + } + if (ignoreTrailingNulls){ + if (length1 > length2){ + for(int i = length2; i < length1; i++){ + if (buffer1[offset1+i] != 0){ + return false; + } + } + } else { + for(int i = length1; i < length2; i++){ + if (buffer2[offset2+i] != 0){ + return false; + } + } + } + return true; + } + return false; + } } Modified: ant/core/trunk/src/main/org/apache/tools/tar/TarInputStream.java URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarInputStream.java?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/src/main/org/apache/tools/tar/TarInputStream.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarInputStream.java Sat Jun 16 04:12:37 2012 @@ -23,10 +23,17 @@ package org.apache.tools.tar; +import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.tools.zip.ZipEncoding; +import org.apache.tools.zip.ZipEncodingHelper; /** * The TarInputStream reads a UNIX tar archive as an InputStream. @@ -59,6 +66,8 @@ public class TarInputStream extends Filt // CheckStyle:VisibilityModifier ON + private final ZipEncoding encoding; + /** * Constructor for TarInputStream. * @param is the input stream to use @@ -70,6 +79,15 @@ public class TarInputStream extends Filt /** * Constructor for TarInputStream. * @param is the input stream to use + * @param encoding name of the encoding to use for file names + */ + public TarInputStream(InputStream is, String encoding) { + this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, encoding); + } + + /** + * Constructor for TarInputStream. + * @param is the input stream to use * @param blockSize the block size to use */ public TarInputStream(InputStream is, int blockSize) { @@ -80,16 +98,38 @@ public class TarInputStream extends Filt * Constructor for TarInputStream. * @param is the input stream to use * @param blockSize the block size to use + * @param encoding name of the encoding to use for file names + */ + public TarInputStream(InputStream is, int blockSize, String encoding) { + this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE, encoding); + } + + /** + * Constructor for TarInputStream. + * @param is the input stream to use + * @param blockSize the block size to use * @param recordSize the record size to use */ public TarInputStream(InputStream is, int blockSize, int recordSize) { - super(is); + this(is, blockSize, recordSize, null); + } + /** + * Constructor for TarInputStream. + * @param is the input stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + * @param encoding name of the encoding to use for file names + */ + public TarInputStream(InputStream is, int blockSize, int recordSize, + String encoding) { + super(is); this.buffer = new TarBuffer(is, blockSize, recordSize); this.readBuf = null; this.oneBuf = new byte[1]; this.debug = false; this.hasHitEOF = false; + this.encoding = ZipEncodingHelper.getZipEncoding(encoding); } /** @@ -106,6 +146,7 @@ public class TarInputStream extends Filt * Closes this stream. Calls the TarBuffer's close() method. * @throws IOException on error */ + @Override public void close() throws IOException { buffer.close(); } @@ -131,6 +172,7 @@ public class TarInputStream extends Filt * @return The number of available bytes for the current entry. * @throws IOException for signature */ + @Override public int available() throws IOException { if (entrySize - entryOffset > Integer.MAX_VALUE) { return Integer.MAX_VALUE; @@ -148,6 +190,7 @@ public class TarInputStream extends Filt * @return the number actually skipped * @throws IOException on error */ + @Override public long skip(long numToSkip) throws IOException { // REVIEW // This is horribly inefficient, but it ensures that we @@ -171,6 +214,7 @@ public class TarInputStream extends Filt * * @return False. */ + @Override public boolean markSupported() { return false; } @@ -180,12 +224,14 @@ public class TarInputStream extends Filt * * @param markLimit The limit to mark. */ + @Override public void mark(int markLimit) { } /** * Since we do not support marking just yet, we do nothing. */ + @Override public void reset() { } @@ -230,44 +276,37 @@ public class TarInputStream extends Filt readBuf = null; } - byte[] headerBuf = buffer.readRecord(); - - if (headerBuf == null) { - if (debug) { - System.err.println("READ NULL RECORD"); - } - hasHitEOF = true; - } else if (buffer.isEOFRecord(headerBuf)) { - if (debug) { - System.err.println("READ EOF RECORD"); - } - hasHitEOF = true; - } + byte[] headerBuf = getRecord(); if (hasHitEOF) { currEntry = null; - } else { - currEntry = new TarEntry(headerBuf); - - if (debug) { - System.err.println("TarInputStream: SET CURRENTRY '" - + currEntry.getName() - + "' size = " - + currEntry.getSize()); - } - - entryOffset = 0; + return null; + } - entrySize = currEntry.getSize(); + try { + currEntry = new TarEntry(headerBuf, encoding); + } catch (IllegalArgumentException e) { + IOException ioe = new IOException("Error detected parsing the header"); + ioe.initCause(e); + throw ioe; + } + if (debug) { + System.err.println("TarInputStream: SET CURRENTRY '" + + currEntry.getName() + + "' size = " + + currEntry.getSize()); } - if (currEntry != null && currEntry.isGNULongNameEntry()) { + entryOffset = 0; + entrySize = currEntry.getSize(); + + if (currEntry.isGNULongNameEntry()) { // read in the name StringBuffer longName = new StringBuffer(); byte[] buf = new byte[SMALL_BUFFER_SIZE]; int length = 0; while ((length = read(buf)) >= 0) { - longName.append(new String(buf, 0, length)); + longName.append(new String(buf, 0, length)); // TODO default charset? } getNextEntry(); if (currEntry == null) { @@ -283,10 +322,177 @@ public class TarInputStream extends Filt currEntry.setName(longName.toString()); } + if (currEntry.isPaxHeader()){ // Process Pax headers + paxHeaders(); + } + + if (currEntry.isGNUSparse()){ // Process sparse files + readGNUSparse(); + } + + // If the size of the next element in the archive has changed + // due to a new size being reported in the posix header + // information, we update entrySize here so that it contains + // the correct value. + entrySize = currEntry.getSize(); return currEntry; } /** + * Get the next record 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. + * 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 header in the archive, or null. + * @throws IOException on error + */ + private byte[] getRecord() throws IOException { + if (hasHitEOF) { + return null; + } + + byte[] headerBuf = buffer.readRecord(); + + if (headerBuf == null) { + if (debug) { + System.err.println("READ NULL RECORD"); + } + hasHitEOF = true; + } else if (buffer.isEOFRecord(headerBuf)) { + if (debug) { + System.err.println("READ EOF RECORD"); + } + hasHitEOF = true; + } + + return hasHitEOF ? null : headerBuf; + } + + private void paxHeaders() throws IOException{ + Map headers = parsePaxHeaders(this); + getNextEntry(); // Get the actual file entry + applyPaxHeadersToCurrentEntry(headers); + } + + Map parsePaxHeaders(InputStream i) throws IOException { + Map headers = new HashMap(); + // Format is "length keyword=value\n"; + while(true){ // get length + int ch; + int len = 0; + int read = 0; + while((ch = i.read()) != -1) { + read++; + if (ch == ' '){ // End of length string + // Get keyword + ByteArrayOutputStream coll = new ByteArrayOutputStream(); + while((ch = i.read()) != -1) { + read++; + if (ch == '='){ // end of keyword + String keyword = coll.toString("UTF-8"); + // Get rest of entry + byte[] rest = new byte[len - read]; + int got = i.read(rest); + if (got != len - read){ + throw new IOException("Failed to read " + + "Paxheader. Expected " + + (len - read) + + " bytes, read " + + got); + } + // Drop trailing NL + String value = new String(rest, 0, + len - read - 1, "UTF-8"); + headers.put(keyword, value); + break; + } + coll.write((byte) ch); + } + break; // Processed single header + } + len *= 10; + len += ch - '0'; + } + if (ch == -1){ // EOF + break; + } + } + return headers; + } + + private void applyPaxHeadersToCurrentEntry(Map headers) { + /* + * The following headers are defined for Pax. + * atime, ctime, charset: cannot use these without changing TarEntry fields + * mtime + * comment + * gid, gname + * linkpath + * size + * uid,uname + * SCHILY.devminor, SCHILY.devmajor: don't have setters/getters for those + */ + for (Entry ent : headers.entrySet()){ + String key = ent.getKey(); + String val = ent.getValue(); + if ("path".equals(key)){ + currEntry.setName(val); + } else if ("linkpath".equals(key)){ + currEntry.setLinkName(val); + } else if ("gid".equals(key)){ + currEntry.setGroupId(Integer.parseInt(val)); + } else if ("gname".equals(key)){ + currEntry.setGroupName(val); + } else if ("uid".equals(key)){ + currEntry.setUserId(Integer.parseInt(val)); + } else if ("uname".equals(key)){ + currEntry.setUserName(val); + } else if ("size".equals(key)){ + currEntry.setSize(Long.parseLong(val)); + } else if ("mtime".equals(key)){ + currEntry.setModTime((long) (Double.parseDouble(val) * 1000)); + } else if ("SCHILY.devminor".equals(key)){ + currEntry.setDevMinor(Integer.parseInt(val)); + } else if ("SCHILY.devmajor".equals(key)){ + currEntry.setDevMajor(Integer.parseInt(val)); + } + } + } + + /** + * Adds the sparse chunks from the current entry to the sparse chunks, + * including any additional sparse entries following the current entry. + * + * @throws IOException on error + * + * @todo Sparse files get not yet really processed. + */ + private void readGNUSparse() throws IOException { + /* we do not really process sparse files yet + sparses = new ArrayList(); + sparses.addAll(currEntry.getSparses()); + */ + if (currEntry.isExtended()) { + TarArchiveSparseEntry entry; + do { + byte[] headerBuf = getRecord(); + if (hasHitEOF) { + currEntry = null; + break; + } + entry = new TarArchiveSparseEntry(headerBuf); + /* we do not really process sparse files yet + sparses.addAll(entry.getSparses()); + */ + } while (entry.isExtended()); + } + } + + /** * Reads a byte from the current tar archive entry. * * This method simply calls read( byte[], int, int ). @@ -294,6 +500,7 @@ public class TarInputStream extends Filt * @return The byte read, or -1 at EOF. * @throws IOException on error */ + @Override public int read() throws IOException { int num = read(oneBuf, 0, 1); return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK; @@ -312,6 +519,7 @@ public class TarInputStream extends Filt * @return The number of bytes read, or -1 at EOF. * @throws IOException on error */ + @Override public int read(byte[] buf, int offset, int numToRead) throws IOException { int totalRead = 0; @@ -399,4 +607,14 @@ public class TarInputStream extends Filt out.write(buf, 0, numRead); } } + + /** + * Whether this class is able to read the given entry. + * + *

May return false if the current entry is a sparse file.

+ */ + public boolean canReadEntryData(TarEntry te) { + return !te.isGNUSparse(); + } } + Modified: ant/core/trunk/src/main/org/apache/tools/tar/TarOutputStream.java URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/tar/TarOutputStream.java?rev=1350857&r1=1350856&r2=1350857&view=diff ============================================================================== --- ant/core/trunk/src/main/org/apache/tools/tar/TarOutputStream.java (original) +++ ant/core/trunk/src/main/org/apache/tools/tar/TarOutputStream.java Sat Jun 16 04:12:37 2012 @@ -23,9 +23,15 @@ package org.apache.tools.tar; +import java.io.File; import java.io.FilterOutputStream; -import java.io.OutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import org.apache.tools.zip.ZipEncoding; +import org.apache.tools.zip.ZipEncodingHelper; /** * The TarOutputStream writes a UNIX tar archive as an OutputStream. @@ -43,6 +49,18 @@ public class TarOutputStream extends Fil /** GNU tar extensions are used to store long file names in the archive. */ public static final int LONGFILE_GNU = 2; + /** POSIX/PAX extensions are used to store long file names in the archive. */ + public static final int LONGFILE_POSIX = 3; + + /** Fail if a big number (e.g. size > 8GiB) is required in the archive. */ + public static final int BIGNUMBER_ERROR = 0; + + /** star/GNU tar/BSD tar extensions are used to store big number in the archive. */ + public static final int BIGNUMBER_STAR = 1; + + /** POSIX/PAX extensions are used to store big numbers in the archive. */ + public static final int BIGNUMBER_POSIX = 2; + // CheckStyle:VisibilityModifier OFF - bc protected boolean debug; protected long currSize; @@ -56,8 +74,22 @@ public class TarOutputStream extends Fil protected int longFileMode = LONGFILE_ERROR; // CheckStyle:VisibilityModifier ON + private int bigNumberMode = BIGNUMBER_ERROR; + private boolean closed = false; + /** Indicates if putNextEntry has been called without closeEntry */ + private boolean haveUnclosedEntry = false; + + /** indicates if this archive is finished */ + private boolean finished = false; + + private final ZipEncoding encoding; + + private boolean addPaxHeadersForNonAsciiNames = false; + private static final ZipEncoding ASCII = + ZipEncodingHelper.getZipEncoding("ASCII"); + /** * Constructor for TarInputStream. * @param os the output stream to use @@ -69,6 +101,15 @@ public class TarOutputStream extends Fil /** * Constructor for TarInputStream. * @param os the output stream to use + * @param encoding name of the encoding to use for file names + */ + public TarOutputStream(OutputStream os, String encoding) { + this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, encoding); + } + + /** + * Constructor for TarInputStream. + * @param os the output stream to use * @param blockSize the block size to use */ public TarOutputStream(OutputStream os, int blockSize) { @@ -79,10 +120,33 @@ public class TarOutputStream extends Fil * Constructor for TarInputStream. * @param os the output stream to use * @param blockSize the block size to use + * @param encoding name of the encoding to use for file names + */ + public TarOutputStream(OutputStream os, int blockSize, String encoding) { + this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE, encoding); + } + + /** + * Constructor for TarInputStream. + * @param os the output stream to use + * @param blockSize the block size to use * @param recordSize the record size to use */ public TarOutputStream(OutputStream os, int blockSize, int recordSize) { + this(os, blockSize, recordSize, null); + } + + /** + * Constructor for TarInputStream. + * @param os the output stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + * @param encoding name of the encoding to use for file names + */ + public TarOutputStream(OutputStream os, int blockSize, int recordSize, + String encoding) { super(os); + this.encoding = ZipEncodingHelper.getZipEncoding(encoding); this.buffer = new TarBuffer(os, blockSize, recordSize); this.debug = false; @@ -103,6 +167,23 @@ public class TarOutputStream extends Fil this.longFileMode = longFileMode; } + /** + * Set the big number mode. + * This can be BIGNUMBER_ERROR(0), BIGNUMBER_POSIX(1) or BIGNUMBER_STAR(2). + * This specifies the treatment of big files (sizes > TarConstants.MAXSIZE) and other numeric values to big to fit into a traditional tar header. + * Default is BIGNUMBER_ERROR. + * @param bigNumberMode the mode to use + */ + public void setBigNumberMode(int bigNumberMode) { + this.bigNumberMode = bigNumberMode; + } + + /** + * Whether to add a PAX extension header for non-ASCII file names. + */ + public void setAddPaxHeadersForNonAsciiNames(boolean b) { + addPaxHeadersForNonAsciiNames = b; + } /** * Sets the debugging flag. @@ -124,15 +205,25 @@ public class TarOutputStream extends Fil /** * Ends the TAR archive without closing the underlying OutputStream. - * The result is that the two EOF records of nulls are written. + * + * An archive consists of a series of file entries terminated by an + * end-of-archive entry, which consists of two 512 blocks of zero bytes. + * POSIX.1 requires two EOF records, like some other implementations. + * * @throws IOException on error */ public void finish() throws IOException { - // See Bugzilla 28776 for a discussion on this - // http://issues.apache.org/bugzilla/show_bug.cgi?id=28776 + if (finished) { + throw new IOException("This archive has already been finished"); + } + + if (haveUnclosedEntry) { + throw new IOException("This archives contains unclosed entries."); + } writeEOFRecord(); writeEOFRecord(); buffer.flushBlock(); + finished = true; } /** @@ -141,9 +232,13 @@ public class TarOutputStream extends Fil * TarBuffer's close(). * @throws IOException on error */ + @Override public void close() throws IOException { - if (!closed) { + if(!finished) { finish(); + } + + if (!closed) { buffer.close(); out.close(); closed = true; @@ -172,27 +267,59 @@ public class TarOutputStream extends Fil * @throws IOException on error */ public void putNextEntry(TarEntry entry) throws IOException { - if (entry.getName().length() >= TarConstants.NAMELEN) { - - if (longFileMode == LONGFILE_GNU) { + if(finished) { + throw new IOException("Stream has already been finished"); + } + Map paxHeaders = new HashMap(); + final String entryName = entry.getName(); + final byte[] nameBytes = encoding.encode(entryName).array(); + boolean paxHeaderContainsPath = false; + if (nameBytes.length >= TarConstants.NAMELEN) { + + if (longFileMode == LONGFILE_POSIX) { + paxHeaders.put("path", entryName); + paxHeaderContainsPath = true; + } else if (longFileMode == LONGFILE_GNU) { // create a TarEntry for the LongLink, the contents // of which are the entry's name TarEntry longLinkEntry = new TarEntry(TarConstants.GNU_LONGLINK, TarConstants.LF_GNUTYPE_LONGNAME); - longLinkEntry.setSize(entry.getName().length() + 1); + longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL putNextEntry(longLinkEntry); - write(entry.getName().getBytes()); - write(0); + write(nameBytes); + write(0); // NUL terminator closeEntry(); } else if (longFileMode != LONGFILE_TRUNCATE) { - throw new RuntimeException("file name '" + entry.getName() - + "' is too long ( > " - + TarConstants.NAMELEN + " bytes)"); + throw new RuntimeException("file name '" + entryName + + "' is too long ( > " + + TarConstants.NAMELEN + " bytes)"); } } - entry.writeEntryHeader(recordBuf); + if (bigNumberMode == BIGNUMBER_POSIX) { + addPaxHeadersForBigNumbers(paxHeaders, entry); + } else if (bigNumberMode != BIGNUMBER_STAR) { + failForBigNumbers(entry); + } + + if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsPath + && !ASCII.canEncode(entryName)) { + paxHeaders.put("path", entryName); + } + + if (addPaxHeadersForNonAsciiNames + && (entry.isLink() || entry.isSymbolicLink()) + && !ASCII.canEncode(entry.getLinkName())) { + paxHeaders.put("linkpath", entry.getLinkName()); + } + + if (paxHeaders.size() > 0) { + writePaxHeaders(entryName, paxHeaders); + } + + entry.writeEntryHeader(recordBuf, encoding, + bigNumberMode == BIGNUMBER_STAR); buffer.writeRecord(recordBuf); currBytes = 0; @@ -202,7 +329,8 @@ public class TarOutputStream extends Fil } else { currSize = entry.getSize(); } - currName = entry.getName(); + currName = entryName; + haveUnclosedEntry = true; } /** @@ -216,6 +344,12 @@ public class TarOutputStream extends Fil * @throws IOException on error */ public void closeEntry() throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + if (!haveUnclosedEntry){ + throw new IOException("No current entry to close"); + } if (assemLen > 0) { for (int i = assemLen; i < assemBuf.length; ++i) { assemBuf[i] = 0; @@ -233,6 +367,7 @@ public class TarOutputStream extends Fil + "' before the '" + currSize + "' bytes specified in the header were written"); } + haveUnclosedEntry = false; } /** @@ -243,6 +378,7 @@ public class TarOutputStream extends Fil * @param b The byte written. * @throws IOException on error */ + @Override public void write(int b) throws IOException { oneBuf[0] = (byte) b; @@ -275,6 +411,7 @@ public class TarOutputStream extends Fil * @param numToWrite The number of bytes to write. * @throws IOException on error */ + @Override public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { if ((currBytes + numToWrite) > currSize) { throw new IOException("request to write '" + numToWrite @@ -341,6 +478,58 @@ public class TarOutputStream extends Fil } /** + * Writes a PAX extended header with the given map as contents. + */ + void writePaxHeaders(String entryName, + Map headers) throws IOException { + String name = "./PaxHeaders.X/" + stripTo7Bits(entryName); + if (name.length() >= TarConstants.NAMELEN) { + name = name.substring(0, TarConstants.NAMELEN - 1); + } + TarEntry pex = new TarEntry(name, + TarConstants.LF_PAX_EXTENDED_HEADER_LC); + + StringWriter w = new StringWriter(); + for (Map.Entry h : headers.entrySet()) { + String key = h.getKey(); + String value = h.getValue(); + int len = key.length() + value.length() + + 3 /* blank, equals and newline */ + + 2 /* guess 9 < actual length < 100 */; + String line = len + " " + key + "=" + value + "\n"; + int actualLength = line.getBytes("UTF-8").length; + while (len != actualLength) { + // Adjust for cases where length < 10 or > 100 + // or where UTF-8 encoding isn't a single octet + // per character. + // Must be in loop as size may go from 99 to 100 in + // first pass so we'd need a second. + len = actualLength; + line = len + " " + key + "=" + value + "\n"; + actualLength = line.getBytes("UTF-8").length; + } + w.write(line); + } + byte[] data = w.toString().getBytes("UTF-8"); + pex.setSize(data.length); + putNextEntry(pex); + write(data); + closeEntry(); + } + + private String stripTo7Bits(String name) { + final int length = name.length(); + StringBuffer result = new StringBuffer(length); + for (int i = 0; i < length; i++) { + char stripped = (char) (name.charAt(i) & 0x7F); + if (stripped != 0) { // would be read as Trailing null + result.append(stripped); + } + } + return result.toString(); + } + + /** * Write an EOF (end of archive) record to the tar archive. * An EOF record consists of a record of all zeros. */ @@ -351,6 +540,53 @@ public class TarOutputStream extends Fil buffer.writeRecord(recordBuf); } -} + private void addPaxHeadersForBigNumbers(Map paxHeaders, + TarEntry entry) { + addPaxHeaderForBigNumber(paxHeaders, "size", entry.getSize(), + TarConstants.MAXSIZE); + addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getGroupId(), + TarConstants.MAXID); + addPaxHeaderForBigNumber(paxHeaders, "mtime", entry.getModTime().getTime() / 1000, + TarConstants.MAXSIZE); + addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getUserId(), + TarConstants.MAXID); + // star extensions by J\u00f6rg Schilling + addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor", + entry.getDevMajor(), TarConstants.MAXID); + addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor", + entry.getDevMinor(), TarConstants.MAXID); + // there is no PAX header for file mode + failForBigNumber("mode", entry.getMode(), TarConstants.MAXID); + } + + private void addPaxHeaderForBigNumber(Map paxHeaders, + String header, long value, + long maxValue) { + if (value < 0 || value > maxValue) { + paxHeaders.put(header, String.valueOf(value)); + } + } + private void failForBigNumbers(TarEntry entry) { + failForBigNumber("entry size", entry.getSize(), TarConstants.MAXSIZE); + failForBigNumber("group id", entry.getGroupId(), TarConstants.MAXID); + failForBigNumber("last modification time", + entry.getModTime().getTime() / 1000, + TarConstants.MAXSIZE); + failForBigNumber("user id", entry.getUserId(), TarConstants.MAXID); + failForBigNumber("mode", entry.getMode(), TarConstants.MAXID); + failForBigNumber("major device number", entry.getDevMajor(), + TarConstants.MAXID); + failForBigNumber("minor device number", entry.getDevMinor(), + TarConstants.MAXID); + } + + private void failForBigNumber(String field, long value, long maxValue) { + if (value < 0 || value > maxValue) { + throw new RuntimeException(field + " '" + value + + "' is too big ( > " + + maxValue + " )"); + } + } +}