git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@1350857 13f79535-47bb-0310-9956-ffa450edef68master
| @@ -49,6 +49,9 @@ Other changes: | |||||
| Java VMs. | Java VMs. | ||||
| Bugzilla Report 52706. | 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 | Changes from Ant 1.8.3 TO Ant 1.8.4 | ||||
| =================================== | =================================== | ||||
| @@ -0,0 +1,63 @@ | |||||
| /* | |||||
| * 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. | |||||
| */ | |||||
| package org.apache.tools.tar; | |||||
| import java.io.IOException; | |||||
| /** | |||||
| * This class represents a sparse entry in a Tar archive. | |||||
| * | |||||
| * <p> | |||||
| * The C structure for a sparse entry is: | |||||
| * <pre> | |||||
| * struct posix_header { | |||||
| * struct sparse sp[21]; // TarConstants.SPARSELEN_GNU_SPARSE - offset 0 | |||||
| * char isextended; // TarConstants.ISEXTENDEDLEN_GNU_SPARSE - offset 504 | |||||
| * }; | |||||
| * </pre> | |||||
| * Whereas, "struct sparse" is: | |||||
| * <pre> | |||||
| * struct sparse { | |||||
| * char offset[12]; // offset 0 | |||||
| * char numbytes[12]; // offset 12 | |||||
| * }; | |||||
| * </pre> | |||||
| */ | |||||
| public class TarArchiveSparseEntry implements TarConstants { | |||||
| /** If an extension sparse header follows. */ | |||||
| private boolean isExtended; | |||||
| /** | |||||
| * Construct an entry from an archive's header bytes. File is set | |||||
| * to null. | |||||
| * | |||||
| * @param headerBuf The header bytes from a tar archive entry. | |||||
| * @throws IOException on unknown format | |||||
| */ | |||||
| public TarArchiveSparseEntry(byte[] headerBuf) throws IOException { | |||||
| int offset = 0; | |||||
| offset += SPARSELEN_GNU_SPARSE; | |||||
| isExtended = TarUtils.parseBoolean(headerBuf, offset); | |||||
| } | |||||
| public boolean isExtended() { | |||||
| return isExtended; | |||||
| } | |||||
| } | |||||
| @@ -51,12 +51,13 @@ public class TarBuffer { | |||||
| private InputStream inStream; | private InputStream inStream; | ||||
| private OutputStream outStream; | 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 currBlkIdx; | ||||
| private int currRecIdx; | private int currRecIdx; | ||||
| private int blockSize; | |||||
| private int recordSize; | |||||
| private int recsPerBlock; | |||||
| private boolean debug; | private boolean debug; | ||||
| /** | /** | ||||
| @@ -83,10 +84,7 @@ public class TarBuffer { | |||||
| * @param recordSize the record size to use | * @param recordSize the record size to use | ||||
| */ | */ | ||||
| public TarBuffer(InputStream inStream, int blockSize, int recordSize) { | 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 | * @param recordSize the record size to use | ||||
| */ | */ | ||||
| public TarBuffer(OutputStream outStream, int blockSize, int recordSize) { | 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.debug = false; | ||||
| this.blockSize = blockSize; | this.blockSize = blockSize; | ||||
| this.recordSize = recordSize; | this.recordSize = recordSize; | ||||
| @@ -194,10 +191,8 @@ public class TarBuffer { | |||||
| throw new IOException("reading (via skip) from an output buffer"); | throw new IOException("reading (via skip) from an output buffer"); | ||||
| } | } | ||||
| if (currRecIdx >= recsPerBlock) { | |||||
| if (!readBlock()) { | |||||
| return; // UNDONE | |||||
| } | |||||
| if (currRecIdx >= recsPerBlock && !readBlock()) { | |||||
| return; // UNDONE | |||||
| } | } | ||||
| currRecIdx++; | currRecIdx++; | ||||
| @@ -216,13 +211,14 @@ public class TarBuffer { | |||||
| } | } | ||||
| if (inStream == null) { | if (inStream == null) { | ||||
| if (outStream == null) { | |||||
| throw new IOException("input buffer is closed"); | |||||
| } | |||||
| throw new IOException("reading from an output buffer"); | 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]; | byte[] result = new byte[recordSize]; | ||||
| @@ -337,6 +333,9 @@ public class TarBuffer { | |||||
| } | } | ||||
| if (outStream == null) { | if (outStream == null) { | ||||
| if (inStream == null){ | |||||
| throw new IOException("Output buffer is closed"); | |||||
| } | |||||
| throw new IOException("writing to an input buffer"); | throw new IOException("writing to an input buffer"); | ||||
| } | } | ||||
| @@ -374,6 +373,9 @@ public class TarBuffer { | |||||
| } | } | ||||
| if (outStream == null) { | if (outStream == null) { | ||||
| if (inStream == null){ | |||||
| throw new IOException("Output buffer is closed"); | |||||
| } | |||||
| throw new IOException("writing to an input buffer"); | throw new IOException("writing to an input buffer"); | ||||
| } | } | ||||
| @@ -454,9 +456,8 @@ public class TarBuffer { | |||||
| } else if (inStream != null) { | } else if (inStream != null) { | ||||
| if (inStream != System.in) { | if (inStream != System.in) { | ||||
| inStream.close(); | inStream.close(); | ||||
| inStream = null; | |||||
| } | } | ||||
| inStream = null; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -26,10 +26,22 @@ package org.apache.tools.tar; | |||||
| /** | /** | ||||
| * This interface contains all the definitions used in the package. | * This interface contains all the definitions used in the package. | ||||
| * | * | ||||
| * For tar formats (FORMAT_OLDGNU, FORMAT_POSIX, etc.) see GNU tar | |||||
| * <I>tar.h</I> type <I>enum archive_format</I> | |||||
| */ | */ | ||||
| // CheckStyle:InterfaceIsTypeCheck OFF (bc) | // CheckStyle:InterfaceIsTypeCheck OFF (bc) | ||||
| public interface TarConstants { | 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. | * The length of the name field in a header buffer. | ||||
| */ | */ | ||||
| @@ -50,6 +62,12 @@ public interface TarConstants { | |||||
| */ | */ | ||||
| int GIDLEN = 8; | 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. | * The length of the checksum field in a header buffer. | ||||
| */ | */ | ||||
| @@ -57,19 +75,36 @@ public interface TarConstants { | |||||
| /** | /** | ||||
| * The length of the size field in a header buffer. | * The length of the size field in a header buffer. | ||||
| * Includes the trailing space or NUL. | |||||
| */ | */ | ||||
| int SIZELEN = 12; | 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; | 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; | 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. | * The length of the modification time field in a header buffer. | ||||
| */ | */ | ||||
| @@ -86,10 +121,76 @@ public interface TarConstants { | |||||
| int GNAMELEN = 32; | 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; | 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, | * 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. | * 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'; | 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"; | 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. | * The magic tag representing a GNU tar archive. | ||||
| */ | */ | ||||
| String GNU_TMAGIC = "ustar "; | 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"; | String GNU_LONGLINK = "././@LongLink"; | ||||
| /** | |||||
| * Identifies the *next* file on the tape as having a long name. | |||||
| */ | |||||
| byte LF_GNUTYPE_LONGNAME = (byte) 'L'; | |||||
| } | } | ||||
| @@ -24,9 +24,13 @@ | |||||
| package org.apache.tools.tar; | package org.apache.tools.tar; | ||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | |||||
| import java.io.UnsupportedEncodingException; | |||||
| import java.util.Date; | import java.util.Date; | ||||
| import java.util.Locale; | import java.util.Locale; | ||||
| import org.apache.tools.zip.ZipEncoding; | |||||
| /** | /** | ||||
| * This class represents an entry in a Tar archive. It consists | * This class represents an entry in a Tar archive. It consists | ||||
| * of the entry's header, as well as the entry's File. Entries | * 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 devmajor[8]; | ||||
| * char devminor[8]; | * char devminor[8]; | ||||
| * } header; | * } 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. | |||||
| * </pre> | |||||
| * | |||||
| * <p> | |||||
| * The C structure for a old GNU Tar Entry's header is: | |||||
| * <pre> | |||||
| * 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 | |||||
| * }; | |||||
| * </pre> | |||||
| * Whereas, "struct sparse" is: | |||||
| * <pre> | |||||
| * struct sparse { | |||||
| * char offset[12]; // offset 0 | |||||
| * char numbytes[12]; // offset 12 | |||||
| * }; | |||||
| * </pre> | * </pre> | ||||
| * | * | ||||
| */ | */ | ||||
| public class TarEntry implements TarConstants { | public class TarEntry implements TarConstants { | ||||
| /** The entry's name. */ | /** The entry's name. */ | ||||
| private StringBuffer name; | |||||
| private String name; | |||||
| /** The entry's permission mode. */ | /** The entry's permission mode. */ | ||||
| private int mode; | private int mode; | ||||
| @@ -99,16 +134,18 @@ public class TarEntry implements TarConstants { | |||||
| private byte linkFlag; | private byte linkFlag; | ||||
| /** The entry's link name. */ | /** The entry's link name. */ | ||||
| private StringBuffer linkName; | |||||
| private String linkName; | |||||
| /** The entry's magic tag. */ | /** The entry's magic tag. */ | ||||
| private StringBuffer magic; | |||||
| private String magic; | |||||
| /** The version of the format */ | |||||
| private String version; | |||||
| /** The entry's user name. */ | /** The entry's user name. */ | ||||
| private StringBuffer userName; | |||||
| private String userName; | |||||
| /** The entry's group name. */ | /** The entry's group name. */ | ||||
| private StringBuffer groupName; | |||||
| private String groupName; | |||||
| /** The entry's major device number. */ | /** The entry's major device number. */ | ||||
| private int devMajor; | private int devMajor; | ||||
| @@ -116,6 +153,12 @@ public class TarEntry implements TarConstants { | |||||
| /** The entry's minor device number. */ | /** The entry's minor device number. */ | ||||
| private int devMinor; | 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 */ | /** The entry's file reference */ | ||||
| private File file; | private File file; | ||||
| @@ -134,10 +177,11 @@ public class TarEntry implements TarConstants { | |||||
| /** | /** | ||||
| * Construct an empty entry and prepares the header values. | * 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", ""); | String user = System.getProperty("user.name", ""); | ||||
| @@ -147,8 +191,8 @@ public class TarEntry implements TarConstants { | |||||
| this.userId = 0; | this.userId = 0; | ||||
| this.groupId = 0; | this.groupId = 0; | ||||
| this.userName = new StringBuffer(user); | |||||
| this.groupName = new StringBuffer(""); | |||||
| this.userName = user; | |||||
| this.groupName = ""; | |||||
| this.file = null; | this.file = null; | ||||
| } | } | ||||
| @@ -178,19 +222,16 @@ public class TarEntry implements TarConstants { | |||||
| this.devMajor = 0; | this.devMajor = 0; | ||||
| this.devMinor = 0; | this.devMinor = 0; | ||||
| this.name = new StringBuffer(name); | |||||
| this.name = name; | |||||
| this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; | this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; | ||||
| this.linkFlag = isDir ? LF_DIR : LF_NORMAL; | this.linkFlag = isDir ? LF_DIR : LF_NORMAL; | ||||
| this.userId = 0; | this.userId = 0; | ||||
| this.groupId = 0; | this.groupId = 0; | ||||
| this.size = 0; | this.size = 0; | ||||
| this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND; | 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 TarConstants { | |||||
| this(name); | this(name); | ||||
| this.linkFlag = linkFlag; | this.linkFlag = linkFlag; | ||||
| if (linkFlag == LF_GNUTYPE_LONGNAME) { | 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 | * Construct an entry for a file. File is set to file, and the | ||||
| * header is constructed from information from the file. | * 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. | * @param file The file that the entry represents. | ||||
| */ | */ | ||||
| public TarEntry(File file) { | 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(); | ||||
| this.file = file; | this.file = file; | ||||
| String fileName = normalizeFileName(file.getPath(), false); | |||||
| this.linkName = new StringBuffer(""); | |||||
| this.name = new StringBuffer(fileName); | |||||
| this.linkName = ""; | |||||
| if (file.isDirectory()) { | if (file.isDirectory()) { | ||||
| this.mode = DEFAULT_DIR_MODE; | this.mode = DEFAULT_DIR_MODE; | ||||
| this.linkFlag = LF_DIR; | 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; | this.size = 0; | ||||
| } else { | } else { | ||||
| this.mode = DEFAULT_FILE_MODE; | this.mode = DEFAULT_FILE_MODE; | ||||
| this.linkFlag = LF_NORMAL; | this.linkFlag = LF_NORMAL; | ||||
| this.size = file.length(); | this.size = file.length(); | ||||
| this.name = fileName; | |||||
| } | } | ||||
| this.modTime = file.lastModified() / MILLIS_PER_SECOND; | this.modTime = file.lastModified() / MILLIS_PER_SECOND; | ||||
| @@ -247,12 +302,27 @@ public class TarEntry implements TarConstants { | |||||
| * to null. | * to null. | ||||
| * | * | ||||
| * @param headerBuf The header bytes from a tar archive entry. | * @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) { | public TarEntry(byte[] headerBuf) { | ||||
| this(); | this(); | ||||
| parseTarHeader(headerBuf); | parseTarHeader(headerBuf); | ||||
| } | } | ||||
| /** | |||||
| * 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 | * Determine if the two entries are equal. Equality is determined | ||||
| * by the header names being equal. | * by the header names being equal. | ||||
| @@ -271,6 +341,7 @@ public class TarEntry implements TarConstants { | |||||
| * @param it Entry to be checked for equality. | * @param it Entry to be checked for equality. | ||||
| * @return True if the entries are equal. | * @return True if the entries are equal. | ||||
| */ | */ | ||||
| @Override | |||||
| public boolean equals(Object it) { | public boolean equals(Object it) { | ||||
| if (it == null || getClass() != it.getClass()) { | if (it == null || getClass() != it.getClass()) { | ||||
| return false; | return false; | ||||
| @@ -283,6 +354,7 @@ public class TarEntry implements TarConstants { | |||||
| * | * | ||||
| * @return the entry hashcode | * @return the entry hashcode | ||||
| */ | */ | ||||
| @Override | |||||
| public int hashCode() { | public int hashCode() { | ||||
| return getName().hashCode(); | return getName().hashCode(); | ||||
| } | } | ||||
| @@ -314,7 +386,7 @@ public class TarEntry implements TarConstants { | |||||
| * @param name This entry's new name. | * @param name This entry's new name. | ||||
| */ | */ | ||||
| public void setName(String name) { | public void setName(String name) { | ||||
| this.name = new StringBuffer(normalizeFileName(name, false)); | |||||
| this.name = normalizeFileName(name, false); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -335,6 +407,15 @@ public class TarEntry implements TarConstants { | |||||
| return linkName.toString(); | return linkName.toString(); | ||||
| } | } | ||||
| /** | |||||
| * 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. | * Get this entry's user id. | ||||
| * | * | ||||
| @@ -386,7 +467,7 @@ public class TarEntry implements TarConstants { | |||||
| * @param userName This entry's new user name. | * @param userName This entry's new user name. | ||||
| */ | */ | ||||
| public void setUserName(String userName) { | public void setUserName(String userName) { | ||||
| this.userName = new StringBuffer(userName); | |||||
| this.userName = userName; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -404,7 +485,7 @@ public class TarEntry implements TarConstants { | |||||
| * @param groupName This entry's new group name. | * @param groupName This entry's new group name. | ||||
| */ | */ | ||||
| public void setGroupName(String groupName) { | public void setGroupName(String groupName) { | ||||
| this.groupName = new StringBuffer(groupName); | |||||
| this.groupName = groupName; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -488,11 +569,88 @@ public class TarEntry implements TarConstants { | |||||
| * Set this entry's file size. | * Set this entry's file size. | ||||
| * | * | ||||
| * @param size This entry's new file size. | * @param size This entry's new file size. | ||||
| * @throws IllegalArgumentException if the size is < 0. | |||||
| */ | */ | ||||
| public void setSize(long size) { | public void setSize(long size) { | ||||
| if (size < 0){ | |||||
| throw new IllegalArgumentException("Size is out of range: "+size); | |||||
| } | |||||
| this.size = 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 | * Indicate if this entry is a GNU long name block | ||||
| @@ -501,7 +659,26 @@ public class TarEntry implements TarConstants { | |||||
| */ | */ | ||||
| public boolean isGNULongNameEntry() { | public boolean isGNULongNameEntry() { | ||||
| return linkFlag == LF_GNUTYPE_LONGNAME | 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; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -525,6 +702,54 @@ public class TarEntry implements TarConstants { | |||||
| return false; | return false; | ||||
| } | } | ||||
| /** | |||||
| * 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 | * If this entry represents a file, and the file is a directory, return | ||||
| * an array of TarEntries for this entry's children. | * an array of TarEntries for this entry's children. | ||||
| @@ -549,17 +774,46 @@ public class TarEntry implements TarConstants { | |||||
| /** | /** | ||||
| * Write an entry's header information to a header buffer. | * Write an entry's header information to a header buffer. | ||||
| * | * | ||||
| * <p>This method does not use the star/GNU tar/BSD tar extensions.</p> | |||||
| * | |||||
| * @param outbuf The tar entry header buffer to fill in. | * @param outbuf The tar entry header buffer to fill in. | ||||
| */ | */ | ||||
| public void writeEntryHeader(byte[] outbuf) { | 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; | 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; | int csOffset = offset; | ||||
| @@ -568,12 +822,18 @@ public class TarEntry implements TarConstants { | |||||
| } | } | ||||
| outbuf[offset++] = linkFlag; | 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) { | while (offset < outbuf.length) { | ||||
| outbuf[offset++] = 0; | outbuf[offset++] = 0; | ||||
| @@ -581,42 +841,122 @@ public class TarEntry implements TarConstants { | |||||
| long chk = TarUtils.computeCheckSum(outbuf); | 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. | * Parse an entry's header information from a header buffer. | ||||
| * | * | ||||
| * @param header The tar entry header buffer to get information from. | * @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) { | 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; | int offset = 0; | ||||
| name = TarUtils.parseName(header, offset, NAMELEN); | |||||
| name = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) | |||||
| : TarUtils.parseName(header, offset, NAMELEN, encoding); | |||||
| offset += NAMELEN; | offset += NAMELEN; | ||||
| mode = (int) TarUtils.parseOctal(header, offset, MODELEN); | |||||
| mode = (int) TarUtils.parseOctalOrBinary(header, offset, MODELEN); | |||||
| offset += MODELEN; | offset += MODELEN; | ||||
| userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); | |||||
| userId = (int) TarUtils.parseOctalOrBinary(header, offset, UIDLEN); | |||||
| offset += UIDLEN; | offset += UIDLEN; | ||||
| groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); | |||||
| groupId = (int) TarUtils.parseOctalOrBinary(header, offset, GIDLEN); | |||||
| offset += GIDLEN; | offset += GIDLEN; | ||||
| size = TarUtils.parseOctal(header, offset, SIZELEN); | |||||
| size = TarUtils.parseOctalOrBinary(header, offset, SIZELEN); | |||||
| offset += SIZELEN; | offset += SIZELEN; | ||||
| modTime = TarUtils.parseOctal(header, offset, MODTIMELEN); | |||||
| modTime = TarUtils.parseOctalOrBinary(header, offset, MODTIMELEN); | |||||
| offset += MODTIMELEN; | offset += MODTIMELEN; | ||||
| offset += CHKSUMLEN; | offset += CHKSUMLEN; | ||||
| linkFlag = header[offset++]; | linkFlag = header[offset++]; | ||||
| linkName = TarUtils.parseName(header, offset, NAMELEN); | |||||
| linkName = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) | |||||
| : TarUtils.parseName(header, offset, NAMELEN, encoding); | |||||
| offset += NAMELEN; | 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; | offset += UNAMELEN; | ||||
| groupName = TarUtils.parseName(header, offset, GNAMELEN); | |||||
| groupName = oldStyle ? TarUtils.parseName(header, offset, GNAMELEN) | |||||
| : TarUtils.parseName(header, offset, GNAMELEN, encoding); | |||||
| offset += GNAMELEN; | offset += GNAMELEN; | ||||
| devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN); | |||||
| devMajor = (int) TarUtils.parseOctalOrBinary(header, offset, DEVLEN); | |||||
| offset += DEVLEN; | offset += DEVLEN; | ||||
| devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN); | |||||
| devMinor = (int) TarUtils.parseOctalOrBinary(header, offset, DEVLEN); | |||||
| 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 TarConstants { | |||||
| } | } | ||||
| return fileName; | 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; | |||||
| } | |||||
| } | } | ||||
| @@ -23,10 +23,17 @@ | |||||
| package org.apache.tools.tar; | package org.apache.tools.tar; | ||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.io.FilterInputStream; | import java.io.FilterInputStream; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.io.OutputStream; | 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. | * The TarInputStream reads a UNIX tar archive as an InputStream. | ||||
| @@ -59,6 +66,8 @@ public class TarInputStream extends FilterInputStream { | |||||
| // CheckStyle:VisibilityModifier ON | // CheckStyle:VisibilityModifier ON | ||||
| private final ZipEncoding encoding; | |||||
| /** | /** | ||||
| * Constructor for TarInputStream. | * Constructor for TarInputStream. | ||||
| * @param is the input stream to use | * @param is the input stream to use | ||||
| @@ -67,6 +76,15 @@ public class TarInputStream extends FilterInputStream { | |||||
| this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); | this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); | ||||
| } | } | ||||
| /** | |||||
| * 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. | * Constructor for TarInputStream. | ||||
| * @param is the input stream to use | * @param is the input stream to use | ||||
| @@ -76,6 +94,16 @@ public class TarInputStream extends FilterInputStream { | |||||
| this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE); | this(is, blockSize, TarBuffer.DEFAULT_RCDSIZE); | ||||
| } | } | ||||
| /** | |||||
| * 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. | * Constructor for TarInputStream. | ||||
| * @param is the input stream to use | * @param is the input stream to use | ||||
| @@ -83,13 +111,25 @@ public class TarInputStream extends FilterInputStream { | |||||
| * @param recordSize the record size to use | * @param recordSize the record size to use | ||||
| */ | */ | ||||
| public TarInputStream(InputStream is, int blockSize, int recordSize) { | 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.buffer = new TarBuffer(is, blockSize, recordSize); | ||||
| this.readBuf = null; | this.readBuf = null; | ||||
| this.oneBuf = new byte[1]; | this.oneBuf = new byte[1]; | ||||
| this.debug = false; | this.debug = false; | ||||
| this.hasHitEOF = false; | this.hasHitEOF = false; | ||||
| this.encoding = ZipEncodingHelper.getZipEncoding(encoding); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -106,6 +146,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * Closes this stream. Calls the TarBuffer's close() method. | * Closes this stream. Calls the TarBuffer's close() method. | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public void close() throws IOException { | public void close() throws IOException { | ||||
| buffer.close(); | buffer.close(); | ||||
| } | } | ||||
| @@ -131,6 +172,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * @return The number of available bytes for the current entry. | * @return The number of available bytes for the current entry. | ||||
| * @throws IOException for signature | * @throws IOException for signature | ||||
| */ | */ | ||||
| @Override | |||||
| public int available() throws IOException { | public int available() throws IOException { | ||||
| if (entrySize - entryOffset > Integer.MAX_VALUE) { | if (entrySize - entryOffset > Integer.MAX_VALUE) { | ||||
| return Integer.MAX_VALUE; | return Integer.MAX_VALUE; | ||||
| @@ -148,6 +190,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * @return the number actually skipped | * @return the number actually skipped | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public long skip(long numToSkip) throws IOException { | public long skip(long numToSkip) throws IOException { | ||||
| // REVIEW | // REVIEW | ||||
| // This is horribly inefficient, but it ensures that we | // This is horribly inefficient, but it ensures that we | ||||
| @@ -171,6 +214,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * | * | ||||
| * @return False. | * @return False. | ||||
| */ | */ | ||||
| @Override | |||||
| public boolean markSupported() { | public boolean markSupported() { | ||||
| return false; | return false; | ||||
| } | } | ||||
| @@ -180,12 +224,14 @@ public class TarInputStream extends FilterInputStream { | |||||
| * | * | ||||
| * @param markLimit The limit to mark. | * @param markLimit The limit to mark. | ||||
| */ | */ | ||||
| @Override | |||||
| public void mark(int markLimit) { | public void mark(int markLimit) { | ||||
| } | } | ||||
| /** | /** | ||||
| * Since we do not support marking just yet, we do nothing. | * Since we do not support marking just yet, we do nothing. | ||||
| */ | */ | ||||
| @Override | |||||
| public void reset() { | public void reset() { | ||||
| } | } | ||||
| @@ -230,44 +276,37 @@ public class TarInputStream extends FilterInputStream { | |||||
| readBuf = null; | 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) { | if (hasHitEOF) { | ||||
| currEntry = null; | 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()); | |||||
| } | |||||
| entryOffset = 0; | |||||
| entrySize = currEntry.getSize(); | |||||
| if (currEntry != null && currEntry.isGNULongNameEntry()) { | |||||
| if (currEntry.isGNULongNameEntry()) { | |||||
| // read in the name | // read in the name | ||||
| StringBuffer longName = new StringBuffer(); | StringBuffer longName = new StringBuffer(); | ||||
| byte[] buf = new byte[SMALL_BUFFER_SIZE]; | byte[] buf = new byte[SMALL_BUFFER_SIZE]; | ||||
| int length = 0; | int length = 0; | ||||
| while ((length = read(buf)) >= 0) { | while ((length = read(buf)) >= 0) { | ||||
| longName.append(new String(buf, 0, length)); | |||||
| longName.append(new String(buf, 0, length)); // TODO default charset? | |||||
| } | } | ||||
| getNextEntry(); | getNextEntry(); | ||||
| if (currEntry == null) { | if (currEntry == null) { | ||||
| @@ -283,9 +322,176 @@ public class TarInputStream extends FilterInputStream { | |||||
| currEntry.setName(longName.toString()); | 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; | 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<String, String> headers = parsePaxHeaders(this); | |||||
| getNextEntry(); // Get the actual file entry | |||||
| applyPaxHeadersToCurrentEntry(headers); | |||||
| } | |||||
| Map<String, String> parsePaxHeaders(InputStream i) throws IOException { | |||||
| Map<String, String> headers = new HashMap<String, String>(); | |||||
| // 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<String, String> 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<String, String> 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. | * Reads a byte from the current tar archive entry. | ||||
| * | * | ||||
| @@ -294,6 +500,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * @return The byte read, or -1 at EOF. | * @return The byte read, or -1 at EOF. | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public int read() throws IOException { | public int read() throws IOException { | ||||
| int num = read(oneBuf, 0, 1); | int num = read(oneBuf, 0, 1); | ||||
| return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK; | return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK; | ||||
| @@ -312,6 +519,7 @@ public class TarInputStream extends FilterInputStream { | |||||
| * @return The number of bytes read, or -1 at EOF. | * @return The number of bytes read, or -1 at EOF. | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public int read(byte[] buf, int offset, int numToRead) throws IOException { | public int read(byte[] buf, int offset, int numToRead) throws IOException { | ||||
| int totalRead = 0; | int totalRead = 0; | ||||
| @@ -399,4 +607,14 @@ public class TarInputStream extends FilterInputStream { | |||||
| out.write(buf, 0, numRead); | out.write(buf, 0, numRead); | ||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Whether this class is able to read the given entry. | |||||
| * | |||||
| * <p>May return false if the current entry is a sparse file.</p> | |||||
| */ | |||||
| public boolean canReadEntryData(TarEntry te) { | |||||
| return !te.isGNUSparse(); | |||||
| } | |||||
| } | } | ||||
| @@ -23,9 +23,15 @@ | |||||
| package org.apache.tools.tar; | package org.apache.tools.tar; | ||||
| import java.io.File; | |||||
| import java.io.FilterOutputStream; | import java.io.FilterOutputStream; | ||||
| import java.io.OutputStream; | |||||
| import java.io.IOException; | 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. | * The TarOutputStream writes a UNIX tar archive as an OutputStream. | ||||
| @@ -43,6 +49,18 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| /** GNU tar extensions are used to store long file names in the archive. */ | /** GNU tar extensions are used to store long file names in the archive. */ | ||||
| public static final int LONGFILE_GNU = 2; | 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 | // CheckStyle:VisibilityModifier OFF - bc | ||||
| protected boolean debug; | protected boolean debug; | ||||
| protected long currSize; | protected long currSize; | ||||
| @@ -56,8 +74,22 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| protected int longFileMode = LONGFILE_ERROR; | protected int longFileMode = LONGFILE_ERROR; | ||||
| // CheckStyle:VisibilityModifier ON | // CheckStyle:VisibilityModifier ON | ||||
| private int bigNumberMode = BIGNUMBER_ERROR; | |||||
| private boolean closed = false; | 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. | * Constructor for TarInputStream. | ||||
| * @param os the output stream to use | * @param os the output stream to use | ||||
| @@ -66,6 +98,15 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); | this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); | ||||
| } | } | ||||
| /** | |||||
| * 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. | * Constructor for TarInputStream. | ||||
| * @param os the output stream to use | * @param os the output stream to use | ||||
| @@ -75,6 +116,16 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE); | this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE); | ||||
| } | } | ||||
| /** | |||||
| * 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. | * Constructor for TarInputStream. | ||||
| * @param os the output stream to use | * @param os the output stream to use | ||||
| @@ -82,7 +133,20 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * @param recordSize the record size to use | * @param recordSize the record size to use | ||||
| */ | */ | ||||
| public TarOutputStream(OutputStream os, int blockSize, int recordSize) { | 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); | super(os); | ||||
| this.encoding = ZipEncodingHelper.getZipEncoding(encoding); | |||||
| this.buffer = new TarBuffer(os, blockSize, recordSize); | this.buffer = new TarBuffer(os, blockSize, recordSize); | ||||
| this.debug = false; | this.debug = false; | ||||
| @@ -103,6 +167,23 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| this.longFileMode = longFileMode; | 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. | * Sets the debugging flag. | ||||
| @@ -124,15 +205,25 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| /** | /** | ||||
| * Ends the TAR archive without closing the underlying OutputStream. | * 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 | * @throws IOException on error | ||||
| */ | */ | ||||
| public void finish() throws IOException { | 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(); | ||||
| writeEOFRecord(); | writeEOFRecord(); | ||||
| buffer.flushBlock(); | buffer.flushBlock(); | ||||
| finished = true; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -141,9 +232,13 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * TarBuffer's close(). | * TarBuffer's close(). | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public void close() throws IOException { | public void close() throws IOException { | ||||
| if (!closed) { | |||||
| if(!finished) { | |||||
| finish(); | finish(); | ||||
| } | |||||
| if (!closed) { | |||||
| buffer.close(); | buffer.close(); | ||||
| out.close(); | out.close(); | ||||
| closed = true; | closed = true; | ||||
| @@ -172,27 +267,59 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| public void putNextEntry(TarEntry entry) throws IOException { | 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<String, String> paxHeaders = new HashMap<String, String>(); | |||||
| 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 | // create a TarEntry for the LongLink, the contents | ||||
| // of which are the entry's name | // of which are the entry's name | ||||
| TarEntry longLinkEntry = new TarEntry(TarConstants.GNU_LONGLINK, | TarEntry longLinkEntry = new TarEntry(TarConstants.GNU_LONGLINK, | ||||
| TarConstants.LF_GNUTYPE_LONGNAME); | TarConstants.LF_GNUTYPE_LONGNAME); | ||||
| longLinkEntry.setSize(entry.getName().length() + 1); | |||||
| longLinkEntry.setSize(nameBytes.length + 1); // +1 for NUL | |||||
| putNextEntry(longLinkEntry); | putNextEntry(longLinkEntry); | ||||
| write(entry.getName().getBytes()); | |||||
| write(0); | |||||
| write(nameBytes); | |||||
| write(0); // NUL terminator | |||||
| closeEntry(); | closeEntry(); | ||||
| } else if (longFileMode != LONGFILE_TRUNCATE) { | } 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); | buffer.writeRecord(recordBuf); | ||||
| currBytes = 0; | currBytes = 0; | ||||
| @@ -202,7 +329,8 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| } else { | } else { | ||||
| currSize = entry.getSize(); | currSize = entry.getSize(); | ||||
| } | } | ||||
| currName = entry.getName(); | |||||
| currName = entryName; | |||||
| haveUnclosedEntry = true; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -216,6 +344,12 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| public void closeEntry() throws IOException { | 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) { | if (assemLen > 0) { | ||||
| for (int i = assemLen; i < assemBuf.length; ++i) { | for (int i = assemLen; i < assemBuf.length; ++i) { | ||||
| assemBuf[i] = 0; | assemBuf[i] = 0; | ||||
| @@ -233,6 +367,7 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| + "' before the '" + currSize | + "' before the '" + currSize | ||||
| + "' bytes specified in the header were written"); | + "' bytes specified in the header were written"); | ||||
| } | } | ||||
| haveUnclosedEntry = false; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -243,6 +378,7 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * @param b The byte written. | * @param b The byte written. | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public void write(int b) throws IOException { | public void write(int b) throws IOException { | ||||
| oneBuf[0] = (byte) b; | oneBuf[0] = (byte) b; | ||||
| @@ -275,6 +411,7 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| * @param numToWrite The number of bytes to write. | * @param numToWrite The number of bytes to write. | ||||
| * @throws IOException on error | * @throws IOException on error | ||||
| */ | */ | ||||
| @Override | |||||
| public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { | public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { | ||||
| if ((currBytes + numToWrite) > currSize) { | if ((currBytes + numToWrite) > currSize) { | ||||
| throw new IOException("request to write '" + numToWrite | throw new IOException("request to write '" + numToWrite | ||||
| @@ -340,6 +477,58 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Writes a PAX extended header with the given map as contents. | |||||
| */ | |||||
| void writePaxHeaders(String entryName, | |||||
| Map<String, String> 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<String, String> 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. | * Write an EOF (end of archive) record to the tar archive. | ||||
| * An EOF record consists of a record of all zeros. | * An EOF record consists of a record of all zeros. | ||||
| @@ -351,6 +540,53 @@ public class TarOutputStream extends FilterOutputStream { | |||||
| buffer.writeRecord(recordBuf); | buffer.writeRecord(recordBuf); | ||||
| } | } | ||||
| } | |||||
| private void addPaxHeadersForBigNumbers(Map<String, String> 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<String, String> 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 + " )"); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -23,6 +23,12 @@ | |||||
| package org.apache.tools.tar; | package org.apache.tools.tar; | ||||
| import java.io.IOException; | |||||
| import java.math.BigInteger; | |||||
| import java.nio.ByteBuffer; | |||||
| import org.apache.tools.zip.ZipEncoding; | |||||
| import org.apache.tools.zip.ZipEncodingHelper; | |||||
| /** | /** | ||||
| * This class provides static utility methods to work with byte streams. | * This class provides static utility methods to work with byte streams. | ||||
| * | * | ||||
| @@ -32,158 +38,505 @@ public class TarUtils { | |||||
| private static final int BYTE_MASK = 255; | private static final int BYTE_MASK = 255; | ||||
| static final ZipEncoding DEFAULT_ENCODING = | |||||
| ZipEncodingHelper.getZipEncoding(null); | |||||
| /** | |||||
| * Encapsulates the algorithms used up to Ant 1.8 as ZipEncoding. | |||||
| */ | |||||
| static final ZipEncoding FALLBACK_ENCODING = new ZipEncoding() { | |||||
| public boolean canEncode(String name) { return true; } | |||||
| public ByteBuffer encode(String name) { | |||||
| final int length = name.length(); | |||||
| byte[] buf = new byte[length]; | |||||
| // copy until end of input or output is reached. | |||||
| for (int i = 0; i < length; ++i) { | |||||
| buf[i] = (byte) name.charAt(i); | |||||
| } | |||||
| return ByteBuffer.wrap(buf); | |||||
| } | |||||
| public String decode(byte[] buffer) { | |||||
| final int length = buffer.length; | |||||
| StringBuffer result = new StringBuffer(length); | |||||
| for (int i = 0; i < length; ++i) { | |||||
| byte b = buffer[i]; | |||||
| if (b == 0) { // Trailing null | |||||
| break; | |||||
| } | |||||
| result.append((char) (b & 0xFF)); // Allow for sign-extension | |||||
| } | |||||
| return result.toString(); | |||||
| } | |||||
| }; | |||||
| /** Private constructor to prevent instantiation of this utility class. */ | |||||
| private TarUtils(){ | |||||
| } | |||||
| /** | /** | ||||
| * Parse an octal string from a header buffer. This is used for the | |||||
| * file permission mode value. | |||||
| * Parse an octal string from a buffer. | |||||
| * | |||||
| * <p>Leading spaces are ignored. | |||||
| * The buffer must contain a trailing space or NUL, | |||||
| * and may contain an additional trailing space or NUL.</p> | |||||
| * | |||||
| * <p>The input buffer is allowed to contain all NULs, | |||||
| * in which case the method returns 0L | |||||
| * (this allows for missing fields).</p> | |||||
| * | * | ||||
| * @param header The header buffer from which to parse. | |||||
| * <p>To work-around some tar implementations that insert a | |||||
| * leading NUL this method returns 0 if it detects a leading NUL | |||||
| * since Ant 1.9.</p> | |||||
| * | |||||
| * @param buffer The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | * @param offset The offset into the buffer from which to parse. | ||||
| * @param length The number of header bytes to parse. | |||||
| * @param length The maximum number of bytes to parse - must be at least 2 bytes. | |||||
| * @return The long value of the octal string. | * @return The long value of the octal string. | ||||
| * @throws IllegalArgumentException if the trailing space/NUL is missing or if a invalid byte is detected. | |||||
| */ | */ | ||||
| public static long parseOctal(byte[] header, int offset, int length) { | |||||
| public static long parseOctal(final byte[] buffer, final int offset, final int length) { | |||||
| long result = 0; | long result = 0; | ||||
| boolean stillPadding = true; | |||||
| int end = offset + length; | int end = offset + length; | ||||
| int start = offset; | |||||
| for (int i = offset; i < end; ++i) { | |||||
| if (header[i] == 0) { | |||||
| break; | |||||
| } | |||||
| if (length < 2){ | |||||
| throw new IllegalArgumentException("Length "+length+" must be at least 2"); | |||||
| } | |||||
| if (header[i] == (byte) ' ' || header[i] == '0') { | |||||
| if (stillPadding) { | |||||
| continue; | |||||
| } | |||||
| if (buffer[start] == 0) { | |||||
| return 0L; | |||||
| } | |||||
| if (header[i] == (byte) ' ') { | |||||
| break; | |||||
| } | |||||
| // Skip leading spaces | |||||
| while (start < end){ | |||||
| if (buffer[start] == ' '){ | |||||
| start++; | |||||
| } else { | |||||
| break; | |||||
| } | } | ||||
| } | |||||
| stillPadding = false; | |||||
| // Must have trailing NUL or space | |||||
| byte trailer; | |||||
| trailer = buffer[end-1]; | |||||
| if (trailer == 0 || trailer == ' '){ | |||||
| end--; | |||||
| } else { | |||||
| throw new IllegalArgumentException( | |||||
| exceptionMessage(buffer, offset, length, end-1, trailer)); | |||||
| } | |||||
| // May have additional NUL or space | |||||
| trailer = buffer[end-1]; | |||||
| if (trailer == 0 || trailer == ' '){ | |||||
| end--; | |||||
| } | |||||
| for ( ;start < end; start++) { | |||||
| final byte currentByte = buffer[start]; | |||||
| // CheckStyle:MagicNumber OFF | // CheckStyle:MagicNumber OFF | ||||
| result = (result << 3) + (header[i] - '0'); | |||||
| if (currentByte < '0' || currentByte > '7'){ | |||||
| throw new IllegalArgumentException( | |||||
| exceptionMessage(buffer, offset, length, start, currentByte)); | |||||
| } | |||||
| result = (result << 3) + (currentByte - '0'); // convert from ASCII | |||||
| // CheckStyle:MagicNumber ON | // CheckStyle:MagicNumber ON | ||||
| } | } | ||||
| return result; | return result; | ||||
| } | } | ||||
| /** | |||||
| * Parse an entry name from a header buffer. | |||||
| /** | |||||
| * Compute the value contained in a byte buffer. If the most | |||||
| * significant bit of the first byte in the buffer is set, this | |||||
| * bit is ignored and the rest of the buffer is interpreted as a | |||||
| * binary number. Otherwise, the buffer is interpreted as an | |||||
| * octal number as per the parseOctal function above. | |||||
| * | * | ||||
| * @param header The header buffer from which to parse. | |||||
| * @param buffer The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | * @param offset The offset into the buffer from which to parse. | ||||
| * @param length The number of header bytes to parse. | |||||
| * @return The header's entry name. | |||||
| * @param length The maximum number of bytes to parse. | |||||
| * @return The long value of the octal or binary string. | |||||
| * @throws IllegalArgumentException if the trailing space/NUL is | |||||
| * missing or an invalid byte is detected in an octal number, or | |||||
| * if a binary number would exceed the size of a signed long | |||||
| * 64-bit integer. | |||||
| */ | */ | ||||
| public static StringBuffer parseName(byte[] header, int offset, int length) { | |||||
| StringBuffer result = new StringBuffer(length); | |||||
| int end = offset + length; | |||||
| public static long parseOctalOrBinary(final byte[] buffer, final int offset, | |||||
| final int length) { | |||||
| for (int i = offset; i < end; ++i) { | |||||
| if (header[i] == 0) { | |||||
| break; | |||||
| } | |||||
| if ((buffer[offset] & 0x80) == 0) { | |||||
| return parseOctal(buffer, offset, length); | |||||
| } | |||||
| final boolean negative = buffer[offset] == (byte) 0xff; | |||||
| if (length < 9) { | |||||
| return parseBinaryLong(buffer, offset, length, negative); | |||||
| } | |||||
| return parseBinaryBigInteger(buffer, offset, length, negative); | |||||
| } | |||||
| result.append((char) header[i]); | |||||
| private static long parseBinaryLong(final byte[] buffer, final int offset, | |||||
| final int length, | |||||
| final boolean negative) { | |||||
| if (length >= 9) { | |||||
| throw new IllegalArgumentException("At offset " + offset + ", " | |||||
| + length + " byte binary number" | |||||
| + " exceeds maximum signed long" | |||||
| + " value"); | |||||
| } | |||||
| long val = 0; | |||||
| for (int i = 1; i < length; i++) { | |||||
| val = (val << 8) + (buffer[offset + i] & 0xff); | |||||
| } | } | ||||
| if (negative) { | |||||
| // 2's complement | |||||
| val--; | |||||
| val ^= ((long) Math.pow(2, (length - 1) * 8) - 1); | |||||
| } | |||||
| return negative ? -val : val; | |||||
| } | |||||
| return result; | |||||
| private static long parseBinaryBigInteger(final byte[] buffer, | |||||
| final int offset, | |||||
| final int length, | |||||
| final boolean negative) { | |||||
| byte[] remainder = new byte[length - 1]; | |||||
| System.arraycopy(buffer, offset + 1, remainder, 0, length - 1); | |||||
| BigInteger val = new BigInteger(remainder); | |||||
| if (negative) { | |||||
| // 2's complement | |||||
| val = val.add(BigInteger.valueOf(-1)).not(); | |||||
| } | |||||
| if (val.bitLength() > 63) { | |||||
| throw new IllegalArgumentException("At offset " + offset + ", " | |||||
| + length + " byte binary number" | |||||
| + " exceeds maximum signed long" | |||||
| + " value"); | |||||
| } | |||||
| return negative ? -val.longValue() : val.longValue(); | |||||
| } | } | ||||
| /** | /** | ||||
| * Determine the number of bytes in an entry name. | |||||
| * Parse a boolean byte from a buffer. | |||||
| * Leading spaces and NUL are ignored. | |||||
| * The buffer may contain trailing spaces or NULs. | |||||
| * | * | ||||
| * @param name The header name from which to parse. | |||||
| * @param buf The buffer from which to parse. | |||||
| * @param buffer The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | * @param offset The offset into the buffer from which to parse. | ||||
| * @param length The number of header bytes to parse. | |||||
| * @return The number of bytes in a header's entry name. | |||||
| * @return The boolean value of the bytes. | |||||
| * @throws IllegalArgumentException if an invalid byte is detected. | |||||
| */ | */ | ||||
| public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { | |||||
| int i; | |||||
| public static boolean parseBoolean(final byte[] buffer, final int offset) { | |||||
| return buffer[offset] == 1; | |||||
| } | |||||
| for (i = 0; i < length && i < name.length(); ++i) { | |||||
| buf[offset + i] = (byte) name.charAt(i); | |||||
| // Helper method to generate the exception message | |||||
| private static String exceptionMessage(byte[] buffer, final int offset, | |||||
| final int length, int current, final byte currentByte) { | |||||
| String string = new String(buffer, offset, length); // TODO default charset? | |||||
| string=string.replaceAll("\0", "{NUL}"); // Replace NULs to allow string to be printed | |||||
| final String s = "Invalid byte "+currentByte+" at offset "+(current-offset)+" in '"+string+"' len="+length; | |||||
| return s; | |||||
| } | |||||
| /** | |||||
| * Parse an entry name from a buffer. | |||||
| * Parsing stops when a NUL is found | |||||
| * or the buffer length is reached. | |||||
| * | |||||
| * @param buffer The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | |||||
| * @param length The maximum number of bytes to parse. | |||||
| * @return The entry name. | |||||
| */ | |||||
| public static String parseName(byte[] buffer, final int offset, final int length) { | |||||
| try { | |||||
| return parseName(buffer, offset, length, DEFAULT_ENCODING); | |||||
| } catch (IOException ex) { | |||||
| try { | |||||
| return parseName(buffer, offset, length, FALLBACK_ENCODING); | |||||
| } catch (IOException ex2) { | |||||
| // impossible | |||||
| throw new RuntimeException(ex2); | |||||
| } | |||||
| } | } | ||||
| } | |||||
| for (; i < length; ++i) { | |||||
| buf[offset + i] = 0; | |||||
| /** | |||||
| * Parse an entry name from a buffer. | |||||
| * Parsing stops when a NUL is found | |||||
| * or the buffer length is reached. | |||||
| * | |||||
| * @param buffer The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | |||||
| * @param length The maximum number of bytes to parse. | |||||
| * @param encoding name of the encoding to use for file names | |||||
| * @return The entry name. | |||||
| */ | |||||
| public static String parseName(byte[] buffer, final int offset, | |||||
| final int length, | |||||
| final ZipEncoding encoding) | |||||
| throws IOException { | |||||
| int len = length; | |||||
| for (; len > 0; len--) { | |||||
| if (buffer[offset + len - 1] != 0) { | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (len > 0) { | |||||
| byte[] b = new byte[len]; | |||||
| System.arraycopy(buffer, offset, b, 0, len); | |||||
| return encoding.decode(b); | |||||
| } | } | ||||
| return ""; | |||||
| } | |||||
| return offset + length; | |||||
| /** | |||||
| * Copy a name into a buffer. | |||||
| * Copies characters from the name into the buffer | |||||
| * starting at the specified offset. | |||||
| * If the buffer is longer than the name, the buffer | |||||
| * is filled with trailing NULs. | |||||
| * If the name is longer than the buffer, | |||||
| * the output is truncated. | |||||
| * | |||||
| * @param name The header name from which to copy the characters. | |||||
| * @param buf The buffer where the name is to be stored. | |||||
| * @param offset The starting offset into the buffer | |||||
| * @param length The maximum number of header bytes to copy. | |||||
| * @return The updated offset, i.e. offset + length | |||||
| */ | |||||
| public static int formatNameBytes(String name, byte[] buf, final int offset, final int length) { | |||||
| try { | |||||
| return formatNameBytes(name, buf, offset, length, DEFAULT_ENCODING); | |||||
| } catch (IOException ex) { | |||||
| try { | |||||
| return formatNameBytes(name, buf, offset, length, | |||||
| FALLBACK_ENCODING); | |||||
| } catch (IOException ex2) { | |||||
| // impossible | |||||
| throw new RuntimeException(ex2); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| /** | /** | ||||
| * Parse an octal integer from a header buffer. | |||||
| * Copy a name into a buffer. | |||||
| * Copies characters from the name into the buffer | |||||
| * starting at the specified offset. | |||||
| * If the buffer is longer than the name, the buffer | |||||
| * is filled with trailing NULs. | |||||
| * If the name is longer than the buffer, | |||||
| * the output is truncated. | |||||
| * | * | ||||
| * @param value The header value | |||||
| * @param buf The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | |||||
| * @param length The number of header bytes to parse. | |||||
| * @return The integer value of the octal bytes. | |||||
| * @param name The header name from which to copy the characters. | |||||
| * @param buf The buffer where the name is to be stored. | |||||
| * @param offset The starting offset into the buffer | |||||
| * @param length The maximum number of header bytes to copy. | |||||
| * @param encoding name of the encoding to use for file names | |||||
| * @return The updated offset, i.e. offset + length | |||||
| */ | */ | ||||
| public static int getOctalBytes(long value, byte[] buf, int offset, int length) { | |||||
| int idx = length - 1; | |||||
| public static int formatNameBytes(String name, byte[] buf, final int offset, | |||||
| final int length, | |||||
| final ZipEncoding encoding) | |||||
| throws IOException { | |||||
| int len = name.length(); | |||||
| ByteBuffer b = encoding.encode(name); | |||||
| while (b.limit() > length && len > 0) { | |||||
| b = encoding.encode(name.substring(0, --len)); | |||||
| } | |||||
| final int limit = b.limit(); | |||||
| System.arraycopy(b.array(), b.arrayOffset(), buf, offset, limit); | |||||
| buf[offset + idx] = 0; | |||||
| --idx; | |||||
| buf[offset + idx] = (byte) ' '; | |||||
| --idx; | |||||
| // Pad any remaining output bytes with NUL | |||||
| for (int i = limit; i < length; ++i) { | |||||
| buf[offset + i] = 0; | |||||
| } | |||||
| return offset + length; | |||||
| } | |||||
| /** | |||||
| * Fill buffer with unsigned octal number, padded with leading zeroes. | |||||
| * | |||||
| * @param value number to convert to octal - treated as unsigned | |||||
| * @param buffer destination buffer | |||||
| * @param offset starting offset in buffer | |||||
| * @param length length of buffer to fill | |||||
| * @throws IllegalArgumentException if the value will not fit in the buffer | |||||
| */ | |||||
| public static void formatUnsignedOctalString(final long value, byte[] buffer, | |||||
| final int offset, final int length) { | |||||
| int remaining = length; | |||||
| remaining--; | |||||
| if (value == 0) { | if (value == 0) { | ||||
| buf[offset + idx] = (byte) '0'; | |||||
| --idx; | |||||
| buffer[offset + remaining--] = (byte) '0'; | |||||
| } else { | } else { | ||||
| for (long val = value; idx >= 0 && val > 0; --idx) { | |||||
| long val = value; | |||||
| for (; remaining >= 0 && val != 0; --remaining) { | |||||
| // CheckStyle:MagicNumber OFF | // CheckStyle:MagicNumber OFF | ||||
| buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7)); | |||||
| val = val >> 3; | |||||
| buffer[offset + remaining] = (byte) ((byte) '0' + (byte) (val & 7)); | |||||
| val = val >>> 3; | |||||
| // CheckStyle:MagicNumber ON | // CheckStyle:MagicNumber ON | ||||
| } | } | ||||
| if (val != 0){ | |||||
| throw new IllegalArgumentException | |||||
| (value+"="+Long.toOctalString(value)+ " will not fit in octal number buffer of length "+length); | |||||
| } | |||||
| } | } | ||||
| for (; idx >= 0; --idx) { | |||||
| buf[offset + idx] = (byte) ' '; | |||||
| for (; remaining >= 0; --remaining) { // leading zeros | |||||
| buffer[offset + remaining] = (byte) '0'; | |||||
| } | } | ||||
| } | |||||
| /** | |||||
| * Write an octal integer into a buffer. | |||||
| * | |||||
| * Uses {@link #formatUnsignedOctalString} to format | |||||
| * the value as an octal string with leading zeros. | |||||
| * The converted number is followed by space and NUL | |||||
| * | |||||
| * @param value The value to write | |||||
| * @param buf The buffer to receive the output | |||||
| * @param offset The starting offset into the buffer | |||||
| * @param length The size of the output buffer | |||||
| * @return The updated offset, i.e offset+length | |||||
| * @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer | |||||
| */ | |||||
| public static int formatOctalBytes(final long value, byte[] buf, final int offset, final int length) { | |||||
| int idx=length-2; // For space and trailing null | |||||
| formatUnsignedOctalString(value, buf, offset, idx); | |||||
| buf[offset + idx++] = (byte) ' '; // Trailing space | |||||
| buf[offset + idx] = 0; // Trailing null | |||||
| return offset + length; | return offset + length; | ||||
| } | } | ||||
| /** | /** | ||||
| * Parse an octal long integer from a header buffer. | |||||
| * | |||||
| * @param value The header value | |||||
| * @param buf The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | |||||
| * @param length The number of header bytes to parse. | |||||
| * @return The long value of the octal bytes. | |||||
| * Write an octal long integer into a buffer. | |||||
| * | |||||
| * Uses {@link #formatUnsignedOctalString} to format | |||||
| * the value as an octal string with leading zeros. | |||||
| * The converted number is followed by a space. | |||||
| * | |||||
| * @param value The value to write as octal | |||||
| * @param buf The destinationbuffer. | |||||
| * @param offset The starting offset into the buffer. | |||||
| * @param length The length of the buffer | |||||
| * @return The updated offset | |||||
| * @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer | |||||
| */ | */ | ||||
| public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { | |||||
| byte[] temp = new byte[length + 1]; | |||||
| public static int formatLongOctalBytes(final long value, byte[] buf, final int offset, final int length) { | |||||
| getOctalBytes(value, temp, 0, length + 1); | |||||
| System.arraycopy(temp, 0, buf, offset, length); | |||||
| int idx=length-1; // For space | |||||
| formatUnsignedOctalString(value, buf, offset, idx); | |||||
| buf[offset + idx] = (byte) ' '; // Trailing space | |||||
| return offset + length; | return offset + length; | ||||
| } | } | ||||
| /** | /** | ||||
| * Parse the checksum octal integer from a header buffer. | |||||
| * Write an long integer into a buffer as an octal string if this | |||||
| * will fit, or as a binary number otherwise. | |||||
| * | |||||
| * Uses {@link #formatUnsignedOctalString} to format | |||||
| * the value as an octal string with leading zeros. | |||||
| * The converted number is followed by a space. | |||||
| * | |||||
| * @param value The value to write into the buffer. | |||||
| * @param buf The destination buffer. | |||||
| * @param offset The starting offset into the buffer. | |||||
| * @param length The length of the buffer. | |||||
| * @return The updated offset. | |||||
| * @throws IllegalArgumentException if the value (and trailer) | |||||
| * will not fit in the buffer. | |||||
| */ | |||||
| public static int formatLongOctalOrBinaryBytes( | |||||
| final long value, byte[] buf, final int offset, final int length) { | |||||
| // Check whether we are dealing with UID/GID or SIZE field | |||||
| final long maxAsOctalChar = length == TarConstants.UIDLEN ? TarConstants.MAXID : TarConstants.MAXSIZE; | |||||
| final boolean negative = value < 0; | |||||
| if (!negative && value <= maxAsOctalChar) { // OK to store as octal chars | |||||
| return formatLongOctalBytes(value, buf, offset, length); | |||||
| } | |||||
| if (length < 9) { | |||||
| formatLongBinary(value, buf, offset, length, negative); | |||||
| } | |||||
| formatBigIntegerBinary(value, buf, offset, length, negative); | |||||
| buf[offset] = (byte) (negative ? 0xff : 0x80); | |||||
| return offset + length; | |||||
| } | |||||
| private static void formatLongBinary(final long value, byte[] buf, | |||||
| final int offset, final int length, | |||||
| final boolean negative) { | |||||
| final int bits = (length - 1) * 8; | |||||
| final long max = 1l << bits; | |||||
| long val = Math.abs(value); | |||||
| if (val >= max) { | |||||
| throw new IllegalArgumentException("Value " + value + | |||||
| " is too large for " + length + " byte field."); | |||||
| } | |||||
| if (negative) { | |||||
| val ^= max - 1; | |||||
| val |= 0xff << bits; | |||||
| val++; | |||||
| } | |||||
| for (int i = offset + length - 1; i >= offset; i--) { | |||||
| buf[i] = (byte) val; | |||||
| val >>= 8; | |||||
| } | |||||
| } | |||||
| private static void formatBigIntegerBinary(final long value, byte[] buf, | |||||
| final int offset, | |||||
| final int length, | |||||
| final boolean negative) { | |||||
| BigInteger val = BigInteger.valueOf(value); | |||||
| final byte[] b = val.toByteArray(); | |||||
| final int len = b.length; | |||||
| final int off = offset + length - len; | |||||
| System.arraycopy(b, 0, buf, off, len); | |||||
| final byte fill = (byte) (negative ? 0xff : 0); | |||||
| for (int i = offset + 1; i < off; i++) { | |||||
| buf[i] = fill; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Writes an octal value into a buffer. | |||||
| * | |||||
| * Uses {@link #formatUnsignedOctalString} to format | |||||
| * the value as an octal string with leading zeros. | |||||
| * The converted number is followed by NUL and then space. | |||||
| * | * | ||||
| * @param value The header value | |||||
| * @param buf The buffer from which to parse. | |||||
| * @param offset The offset into the buffer from which to parse. | |||||
| * @param length The number of header bytes to parse. | |||||
| * @return The integer value of the entry's checksum. | |||||
| * @param value The value to convert | |||||
| * @param buf The destination buffer | |||||
| * @param offset The starting offset into the buffer. | |||||
| * @param length The size of the buffer. | |||||
| * @return The updated value of offset, i.e. offset+length | |||||
| * @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer | |||||
| */ | */ | ||||
| public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { | |||||
| getOctalBytes(value, buf, offset, length); | |||||
| public static int formatCheckSumOctalBytes(final long value, byte[] buf, final int offset, final int length) { | |||||
| int idx=length-2; // for NUL and space | |||||
| formatUnsignedOctalString(value, buf, offset, idx); | |||||
| buf[offset + length - 1] = (byte) ' '; | |||||
| buf[offset + length - 2] = 0; | |||||
| buf[offset + idx++] = 0; // Trailing null | |||||
| buf[offset + idx] = (byte) ' '; // Trailing space | |||||
| return offset + length; | return offset + length; | ||||
| } | } | ||||
| @@ -194,7 +547,7 @@ public class TarUtils { | |||||
| * @param buf The tar entry's header buffer. | * @param buf The tar entry's header buffer. | ||||
| * @return The computed checksum. | * @return The computed checksum. | ||||
| */ | */ | ||||
| public static long computeCheckSum(byte[] buf) { | |||||
| public static long computeCheckSum(final byte[] buf) { | |||||
| long sum = 0; | long sum = 0; | ||||
| for (int i = 0; i < buf.length; ++i) { | for (int i = 0; i < buf.length; ++i) { | ||||
| @@ -203,4 +556,5 @@ public class TarUtils { | |||||
| return sum; | return sum; | ||||
| } | } | ||||
| } | } | ||||
| @@ -41,7 +41,7 @@ import java.nio.ByteBuffer; | |||||
| * <p>All implementations should implement this interface in a | * <p>All implementations should implement this interface in a | ||||
| * reentrant way.</p> | * reentrant way.</p> | ||||
| */ | */ | ||||
| interface ZipEncoding { | |||||
| public interface ZipEncoding { | |||||
| /** | /** | ||||
| * Check, whether the given string may be losslessly encoded using this | * Check, whether the given string may be losslessly encoded using this | ||||
| * encoding. | * encoding. | ||||
| @@ -27,7 +27,7 @@ import java.util.Map; | |||||
| /** | /** | ||||
| * Static helper functions for robustly encoding filenames in zip files. | * Static helper functions for robustly encoding filenames in zip files. | ||||
| */ | */ | ||||
| abstract class ZipEncodingHelper { | |||||
| public abstract class ZipEncodingHelper { | |||||
| /** | /** | ||||
| * A class, which holds the high characters of a simple encoding | * A class, which holds the high characters of a simple encoding | ||||
| @@ -207,7 +207,7 @@ abstract class ZipEncodingHelper { | |||||
| * the platform's default encoding. | * the platform's default encoding. | ||||
| * @return A zip encoding for the given encoding name. | * @return A zip encoding for the given encoding name. | ||||
| */ | */ | ||||
| static ZipEncoding getZipEncoding(String name) { | |||||
| public static ZipEncoding getZipEncoding(String name) { | |||||
| // fallback encoding is good enough for utf-8. | // fallback encoding is good enough for utf-8. | ||||
| if (isUTF8(name)) { | if (isUTF8(name)) { | ||||