diff options
Diffstat (limited to 'framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar')
7 files changed, 3849 insertions, 0 deletions
diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java new file mode 100644 index 00000000..2e76fb69 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarArchiveSparseEntry.java @@ -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; + } +} diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarBuffer.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarBuffer.java new file mode 100644 index 00000000..b089d9bf --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarBuffer.java @@ -0,0 +1,463 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * The TarBuffer class implements the tar archive concept + * of a buffered input stream. This concept goes back to the + * days of blocked tape drives and special io devices. In the + * Java universe, the only real function that this class + * performs is to ensure that files have the correct "block" + * size, or other tars will complain. + * <p> + * You should never have a need to access this class directly. + * TarBuffers are created by Tar IO Streams. + * + */ + +public class TarBuffer { + + /** Default record size */ + public static final int DEFAULT_RCDSIZE = (512); + + /** Default block size */ + public static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20); + + private InputStream inStream; + private OutputStream outStream; + private final int blockSize; + private final int recordSize; + private final int recsPerBlock; + private final byte[] blockBuffer; + + private int currBlkIdx; + private int currRecIdx; + private boolean debug; + + /** + * Constructor for a TarBuffer on an input stream. + * @param inStream the input stream to use + */ + public TarBuffer(InputStream inStream) { + this(inStream, TarBuffer.DEFAULT_BLKSIZE); + } + + /** + * Constructor for a TarBuffer on an input stream. + * @param inStream the input stream to use + * @param blockSize the block size to use + */ + public TarBuffer(InputStream inStream, int blockSize) { + this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + /** + * Constructor for a TarBuffer on an input stream. + * @param inStream the input stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + */ + public TarBuffer(InputStream inStream, int blockSize, int recordSize) { + this(inStream, null, blockSize, recordSize); + } + + /** + * Constructor for a TarBuffer on an output stream. + * @param outStream the output stream to use + */ + public TarBuffer(OutputStream outStream) { + this(outStream, TarBuffer.DEFAULT_BLKSIZE); + } + + /** + * Constructor for a TarBuffer on an output stream. + * @param outStream the output stream to use + * @param blockSize the block size to use + */ + public TarBuffer(OutputStream outStream, int blockSize) { + this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); + } + + /** + * Constructor for a TarBuffer on an output stream. + * @param outStream the output stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + */ + public TarBuffer(OutputStream outStream, int blockSize, int recordSize) { + this(null, outStream, blockSize, recordSize); + } + + /** + * Private constructor to perform common setup. + */ + private TarBuffer(InputStream inStream, OutputStream outStream, int blockSize, int recordSize) { + this.inStream = inStream; + this.outStream = outStream; + this.debug = false; + this.blockSize = blockSize; + this.recordSize = recordSize; + this.recsPerBlock = (this.blockSize / this.recordSize); + this.blockBuffer = new byte[this.blockSize]; + + if (this.inStream != null) { + this.currBlkIdx = -1; + this.currRecIdx = this.recsPerBlock; + } else { + this.currBlkIdx = 0; + this.currRecIdx = 0; + } + } + + /** + * Get the TAR Buffer's block size. Blocks consist of multiple records. + * @return the block size + */ + public int getBlockSize() { + return this.blockSize; + } + + /** + * Get the TAR Buffer's record size. + * @return the record size + */ + public int getRecordSize() { + return this.recordSize; + } + + /** + * Set the debugging flag for the buffer. + * + * @param debug If true, print debugging output. + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Determine if an archive record indicate End of Archive. End of + * archive is indicated by a record that consists entirely of null bytes. + * + * @param record The record data to check. + * @return true if the record data is an End of Archive + */ + public boolean isEOFRecord(byte[] record) { + for (int i = 0, sz = getRecordSize(); i < sz; ++i) { + if (record[i] != 0) { + return false; + } + } + + return true; + } + + /** + * Skip over a record on the input stream. + * @throws IOException on error + */ + public void skipRecord() throws IOException { + if (debug) { + System.err.println("SkipRecord: recIdx = " + currRecIdx + + " blkIdx = " + currBlkIdx); + } + + if (inStream == null) { + throw new IOException("reading (via skip) from an output buffer"); + } + + if (currRecIdx >= recsPerBlock && !readBlock()) { + return; // UNDONE + } + + currRecIdx++; + } + + /** + * Read a record from the input stream and return the data. + * + * @return The record data. + * @throws IOException on error + */ + public byte[] readRecord() throws IOException { + if (debug) { + System.err.println("ReadRecord: recIdx = " + currRecIdx + + " blkIdx = " + currBlkIdx); + } + + if (inStream == null) { + if (outStream == null) { + throw new IOException("input buffer is closed"); + } + throw new IOException("reading from an output buffer"); + } + + if (currRecIdx >= recsPerBlock && !readBlock()) { + return null; + } + + byte[] result = new byte[recordSize]; + + System.arraycopy(blockBuffer, + (currRecIdx * recordSize), result, 0, + recordSize); + + currRecIdx++; + + return result; + } + + /** + * @return false if End-Of-File, else true + */ + private boolean readBlock() throws IOException { + if (debug) { + System.err.println("ReadBlock: blkIdx = " + currBlkIdx); + } + + if (inStream == null) { + throw new IOException("reading from an output buffer"); + } + + currRecIdx = 0; + + int offset = 0; + int bytesNeeded = blockSize; + + while (bytesNeeded > 0) { + long numBytes = inStream.read(blockBuffer, offset, + bytesNeeded); + + // + // NOTE + // We have fit EOF, and the block is not full! + // + // This is a broken archive. It does not follow the standard + // blocking algorithm. However, because we are generous, and + // it requires little effort, we will simply ignore the error + // and continue as if the entire block were read. This does + // not appear to break anything upstream. We used to return + // false in this case. + // + // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix. + // + if (numBytes == -1) { + if (offset == 0) { + // Ensure that we do not read gigabytes of zeros + // for a corrupt tar file. + // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924 + return false; + } + // However, just leaving the unread portion of the buffer dirty does + // cause problems in some cases. This problem is described in + // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877 + // + // The solution is to fill the unused portion of the buffer with zeros. + + Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0); + + break; + } + + offset += numBytes; + bytesNeeded -= numBytes; + + if (numBytes != blockSize) { + if (debug) { + System.err.println("ReadBlock: INCOMPLETE READ " + + numBytes + " of " + blockSize + + " bytes read."); + } + } + } + + currBlkIdx++; + + return true; + } + + /** + * Get the current block number, zero based. + * + * @return The current zero based block number. + */ + public int getCurrentBlockNum() { + return currBlkIdx; + } + + /** + * Get the current record number, within the current block, zero based. + * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum. + * + * @return The current zero based record number. + */ + public int getCurrentRecordNum() { + return currRecIdx - 1; + } + + /** + * Write an archive record to the archive. + * + * @param record The record data to write to the archive. + * @throws IOException on error + */ + public void writeRecord(byte[] record) throws IOException { + if (debug) { + System.err.println("WriteRecord: recIdx = " + currRecIdx + + " blkIdx = " + currBlkIdx); + } + + if (outStream == null) { + if (inStream == null){ + throw new IOException("Output buffer is closed"); + } + throw new IOException("writing to an input buffer"); + } + + if (record.length != recordSize) { + throw new IOException("record to write has length '" + + record.length + + "' which is not the record size of '" + + recordSize + "'"); + } + + if (currRecIdx >= recsPerBlock) { + writeBlock(); + } + + System.arraycopy(record, 0, blockBuffer, + (currRecIdx * recordSize), + recordSize); + + currRecIdx++; + } + + /** + * Write an archive record to the archive, where the record may be + * inside of a larger array buffer. The buffer must be "offset plus + * record size" long. + * + * @param buf The buffer containing the record data to write. + * @param offset The offset of the record data within buf. + * @throws IOException on error + */ + public void writeRecord(byte[] buf, int offset) throws IOException { + if (debug) { + System.err.println("WriteRecord: recIdx = " + currRecIdx + + " blkIdx = " + currBlkIdx); + } + + if (outStream == null) { + if (inStream == null){ + throw new IOException("Output buffer is closed"); + } + throw new IOException("writing to an input buffer"); + } + + if ((offset + recordSize) > buf.length) { + throw new IOException("record has length '" + buf.length + + "' with offset '" + offset + + "' which is less than the record size of '" + + recordSize + "'"); + } + + if (currRecIdx >= recsPerBlock) { + writeBlock(); + } + + System.arraycopy(buf, offset, blockBuffer, + (currRecIdx * recordSize), + recordSize); + + currRecIdx++; + } + + /** + * Write a TarBuffer block to the archive. + */ + private void writeBlock() throws IOException { + if (debug) { + System.err.println("WriteBlock: blkIdx = " + currBlkIdx); + } + + if (outStream == null) { + throw new IOException("writing to an input buffer"); + } + + outStream.write(blockBuffer, 0, blockSize); + outStream.flush(); + + currRecIdx = 0; + currBlkIdx++; + Arrays.fill(blockBuffer, (byte) 0); + } + + /** + * Flush the current data block if it has any data in it. + */ + void flushBlock() throws IOException { + if (debug) { + System.err.println("TarBuffer.flushBlock() called."); + } + + if (outStream == null) { + throw new IOException("writing to an input buffer"); + } + + if (currRecIdx > 0) { + writeBlock(); + } + } + + /** + * Close the TarBuffer. If this is an output buffer, also flush the + * current block before closing. + * @throws IOException on error + */ + public void close() throws IOException { + if (debug) { + System.err.println("TarBuffer.closeBuffer()."); + } + + if (outStream != null) { + flushBlock(); + + if (outStream != System.out + && outStream != System.err) { + outStream.close(); + + outStream = null; + } + } else if (inStream != null) { + if (inStream != System.in) { + inStream.close(); + } + inStream = null; + } + } +} diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarConstants.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarConstants.java new file mode 100644 index 00000000..06d0faca --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarConstants.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +/** + * This interface contains all the definitions used in the package. + * + * For tar formats (FORMAT_OLDGNU, FORMAT_POSIX, etc.) see GNU tar + * <I>tar.h</I> type <I>enum archive_format</I> + */ +// CheckStyle:InterfaceIsTypeCheck OFF (bc) +public interface TarConstants { + + /** + * GNU format as per before tar 1.12. + */ + int FORMAT_OLDGNU = 2; + + /** + * Pure Posix format. + */ + int FORMAT_POSIX = 3; + + /** + * The length of the name field in a header buffer. + */ + int NAMELEN = 100; + + /** + * The length of the mode field in a header buffer. + */ + int MODELEN = 8; + + /** + * The length of the user id field in a header buffer. + */ + int UIDLEN = 8; + + /** + * The length of the group id field in a header buffer. + */ + int GIDLEN = 8; + + /** + * The maximum value of gid/uid in a tar archive which can + * be expressed in octal char notation (that's 7 sevens, octal). + */ + long MAXID = 07777777L; + + /** + * The length of the checksum field in a header buffer. + */ + int CHKSUMLEN = 8; + + /** + * The length of the size field in a header buffer. + * Includes the trailing space or NUL. + */ + int SIZELEN = 12; + + /** + * The maximum size of a file in a tar archive + * which can be expressed in octal char notation (that's 11 sevens, octal). + */ + long MAXSIZE = 077777777777L; + + /** Offset of start of magic field within header record */ + int MAGIC_OFFSET = 257; + /** + * The length of the magic field in a header buffer including the version. + */ + int MAGICLEN = 8; + + /** + * The length of the magic field in a header buffer. + */ + int PURE_MAGICLEN = 6; + + /** Offset of start of magic field within header record */ + int VERSION_OFFSET = 263; + /** + * Previously this was regarded as part of "magic" field, but it + * is separate. + */ + int VERSIONLEN = 2; + + /** + * The length of the modification time field in a header buffer. + */ + int MODTIMELEN = 12; + + /** + * The length of the user name field in a header buffer. + */ + int UNAMELEN = 32; + + /** + * The length of the group name field in a header buffer. + */ + int GNAMELEN = 32; + + /** + * The length of each of the device fields (major and minor) in a header buffer. + */ + int DEVLEN = 8; + + /** + * Length of the prefix field. + * + */ + int PREFIXLEN = 155; + + /** + * The length of the access time field in an old GNU header buffer. + * + */ + int ATIMELEN_GNU = 12; + + /** + * The length of the created time field in an old GNU header buffer. + * + */ + int CTIMELEN_GNU = 12; + + /** + * The length of the multivolume start offset field in an old GNU header buffer. + * + */ + int OFFSETLEN_GNU = 12; + + /** + * The length of the long names field in an old GNU header buffer. + * + */ + int LONGNAMESLEN_GNU = 4; + + /** + * The length of the padding field in an old GNU header buffer. + * + */ + int PAD2LEN_GNU = 1; + + /** + * The sum of the length of all sparse headers in an old GNU header buffer. + * + */ + int SPARSELEN_GNU = 96; + + /** + * The length of the is extension field in an old GNU header buffer. + * + */ + int ISEXTENDEDLEN_GNU = 1; + + /** + * The length of the real size field in an old GNU header buffer. + * + */ + int REALSIZELEN_GNU = 12; + + /** + * The sum of the length of all sparse headers in a sparse header buffer. + * + */ + int SPARSELEN_GNU_SPARSE = 504; + + /** + * The length of the is extension field in a sparse header buffer. + * + */ + int ISEXTENDEDLEN_GNU_SPARSE = 1; + + /** + * LF_ constants represent the "link flag" of an entry, or more commonly, + * the "entry type". This is the "old way" of indicating a normal file. + */ + byte LF_OLDNORM = 0; + + /** + * Normal file type. + */ + byte LF_NORMAL = (byte) '0'; + + /** + * Link file type. + */ + byte LF_LINK = (byte) '1'; + + /** + * Symbolic link file type. + */ + byte LF_SYMLINK = (byte) '2'; + + /** + * Character device file type. + */ + byte LF_CHR = (byte) '3'; + + /** + * Block device file type. + */ + byte LF_BLK = (byte) '4'; + + /** + * Directory file type. + */ + byte LF_DIR = (byte) '5'; + + /** + * FIFO (pipe) file type. + */ + byte LF_FIFO = (byte) '6'; + + /** + * Contiguous file type. + */ + byte LF_CONTIG = (byte) '7'; + + /** + * Identifies the *next* file on the tape as having a long linkname. + */ + byte LF_GNUTYPE_LONGLINK = (byte) 'K'; + + /** + * Identifies the *next* file on the tape as having a long name. + */ + byte LF_GNUTYPE_LONGNAME = (byte) 'L'; + + /** + * Sparse file type. + */ + byte LF_GNUTYPE_SPARSE = (byte) 'S'; + + // See "http://www.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_04_100_13_02" + + /** + * Identifies the entry as a Pax extended header. + */ + byte LF_PAX_EXTENDED_HEADER_LC = (byte) 'x'; + + /** + * Identifies the entry as a Pax extended header (SunOS tar -E). + */ + byte LF_PAX_EXTENDED_HEADER_UC = (byte) 'X'; + + /** + * Identifies the entry as a Pax global extended header. + */ + byte LF_PAX_GLOBAL_EXTENDED_HEADER = (byte) 'g'; + + String TMAGIC = "ustar"; + + /** + * The magic tag representing a POSIX tar archive. + */ + String MAGIC_POSIX = "ustar\0"; + String VERSION_POSIX = "00"; + + /** + * The magic tag representing a GNU tar archive. + */ + String GNU_TMAGIC = "ustar "; + // Appear to be two possible GNU versions + String VERSION_GNU_SPACE = " \0"; + String VERSION_GNU_ZERO = "0\0"; + + /** + * The name of the GNU tar entry which contains a long name. + */ + String GNU_LONGLINK = "././@LongLink"; + +} diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarEntry.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarEntry.java new file mode 100644 index 00000000..a9c96f13 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarEntry.java @@ -0,0 +1,1149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.util.Locale; + +import org.apache.tools.zip.ZipEncoding; + +/** + * This class represents an entry in a Tar archive. It consists + * of the entry's header, as well as the entry's File. Entries + * can be instantiated in one of three ways, depending on how + * they are to be used. + * <p> + * TarEntries that are created from the header bytes read from + * an archive are instantiated with the TarEntry( byte[] ) + * constructor. These entries will be used when extracting from + * or listing the contents of an archive. These entries have their + * header filled in using the header bytes. They also set the File + * to null, since they reference an archive entry not a file. + * <p> + * TarEntries that are created from Files that are to be written + * into an archive are instantiated with the TarEntry( File ) + * constructor. These entries have their header filled in using + * the File's information. They also keep a reference to the File + * for convenience when writing entries. + * <p> + * Finally, TarEntries can be constructed from nothing but a name. + * This allows the programmer to construct the entry by hand, for + * instance when only an InputStream is available for writing to + * the archive, and the header information is constructed from + * other information. In this case the header fields are set to + * defaults and the File is set to null. + * + * <p> + * The C structure for a Tar Entry's header is: + * <pre> + * struct header { + * char name[NAMSIZ]; + * char mode[8]; + * char uid[8]; + * char gid[8]; + * char size[12]; + * char mtime[12]; + * char chksum[8]; + * char linkflag; + * char linkname[NAMSIZ]; + * char magic[8]; + * char uname[TUNMLEN]; + * char gname[TGNMLEN]; + * char devmajor[8]; + * char devminor[8]; + * } header; + * 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> + * + */ + +public class TarEntry implements TarConstants { + /** The entry's name. */ + private String name; + + /** The entry's permission mode. */ + private int mode; + + /** The entry's user id. */ + private long userId; + + /** The entry's group id. */ + private long groupId; + + /** The entry's size. */ + private long size; + + /** The entry's modification time. */ + private long modTime; + + /** The entry's link flag. */ + private byte linkFlag; + + /** The entry's link name. */ + private String linkName; + + /** The entry's magic tag. */ + private String magic; + /** The version of the format */ + private String version; + + /** The entry's user name. */ + private String userName; + + /** The entry's group name. */ + private String groupName; + + /** The entry's major device number. */ + private int devMajor; + + /** The entry's minor device number. */ + private int devMinor; + + /** If an extension sparse header follows. */ + private boolean isExtended; + + /** The entry's real size in case of a sparse file. */ + private long realSize; + + /** The entry's file reference */ + private File file; + + /** Maximum length of a user's name in the tar file */ + public static final int MAX_NAMELEN = 31; + + /** Default permissions bits for directories */ + public static final int DEFAULT_DIR_MODE = 040755; + + /** Default permissions bits for files */ + public static final int DEFAULT_FILE_MODE = 0100644; + + /** Convert millis to seconds */ + public static final int MILLIS_PER_SECOND = 1000; + + /** + * Construct an empty entry and prepares the header values. + */ + private TarEntry() { + this.magic = MAGIC_POSIX; + this.version = VERSION_POSIX; + this.name = ""; + this.linkName = ""; + + String user = System.getProperty("user.name", ""); + + if (user.length() > MAX_NAMELEN) { + user = user.substring(0, MAX_NAMELEN); + } + + this.userId = 0; + this.groupId = 0; + this.userName = user; + this.groupName = ""; + this.file = null; + } + + /** + * Construct an entry with only a name. This allows the programmer + * to construct the entry's header "by hand". File is set to null. + * + * @param name the entry name + */ + public TarEntry(String name) { + this(name, false); + } + + /** + * Construct an entry with only a name. This allows the programmer + * to construct the entry's header "by hand". File is set to null. + * + * @param name the entry name + * @param preserveLeadingSlashes whether to allow leading slashes + * in the name. + */ + public TarEntry(String name, boolean preserveLeadingSlashes) { + this(); + + name = normalizeFileName(name, preserveLeadingSlashes); + boolean isDir = name.endsWith("/"); + + this.devMajor = 0; + this.devMinor = 0; + this.name = name; + this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; + this.linkFlag = isDir ? LF_DIR : LF_NORMAL; + this.userId = 0; + this.groupId = 0; + this.size = 0; + this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND; + this.linkName = ""; + this.userName = ""; + this.groupName = ""; + } + + /** + * Construct an entry with a name and a link flag. + * + * @param name the entry name + * @param linkFlag the entry link flag. + */ + public TarEntry(String name, byte linkFlag) { + this(name); + this.linkFlag = linkFlag; + if (linkFlag == LF_GNUTYPE_LONGNAME) { + magic = GNU_TMAGIC; + version = VERSION_GNU_SPACE; + } + } + + /** + * Construct an entry for a file. File is set to file, and the + * header is constructed from information from the file. + * The name is set from the normalized file path. + * + * @param file The file that the entry represents. + */ + public TarEntry(File file) { + this(file, file.getPath()); + } + + /** + * 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(); + + String normalizedName = normalizeFileName(fileName, false); + this.file = file; + + this.linkName = ""; + + if (file.isDirectory()) { + this.mode = DEFAULT_DIR_MODE; + this.linkFlag = LF_DIR; + + int nameLength = normalizedName.length(); + if (nameLength == 0 || normalizedName.charAt(nameLength - 1) != '/') { + this.name = normalizedName + "/"; + } else { + this.name = normalizedName; + } + this.size = 0; + } else { + this.mode = DEFAULT_FILE_MODE; + this.linkFlag = LF_NORMAL; + this.size = file.length(); + this.name = normalizedName; + } + + this.modTime = file.lastModified() / MILLIS_PER_SECOND; + this.devMajor = 0; + this.devMinor = 0; + } + + /** + * Construct an entry from an archive's header bytes. File is set + * to null. + * + * @param headerBuf The header bytes from a tar archive entry. + * @throws IllegalArgumentException if any of the numeric fields have an invalid format + */ + public TarEntry(byte[] headerBuf) { + this(); + 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 + * @throws IOException if an error occurs during reading the archive + */ + public TarEntry(byte[] headerBuf, ZipEncoding encoding) + throws IOException { + this(); + parseTarHeader(headerBuf, encoding); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + public boolean equals(TarEntry it) { + return getName().equals(it.getName()); + } + + /** + * Determine if the two entries are equal. Equality is determined + * by the header names being equal. + * + * @param it Entry to be checked for equality. + * @return True if the entries are equal. + */ + @Override + public boolean equals(Object it) { + if (it == null || getClass() != it.getClass()) { + return false; + } + return equals((TarEntry) it); + } + + /** + * Hashcodes are based on entry names. + * + * @return the entry hashcode + */ + @Override + public int hashCode() { + return getName().hashCode(); + } + + /** + * Determine if the given entry is a descendant of this entry. + * Descendancy is determined by the name of the descendant + * starting with this entry's name. + * + * @param desc Entry to be checked as a descendant of this. + * @return True if entry is a descendant of this. + */ + public boolean isDescendent(TarEntry desc) { + return desc.getName().startsWith(getName()); + } + + /** + * Get this entry's name. + * + * @return This entry's name. + */ + public String getName() { + return name.toString(); + } + + /** + * Set this entry's name. + * + * @param name This entry's new name. + */ + public void setName(String name) { + this.name = normalizeFileName(name, false); + } + + /** + * Set the mode for this entry + * + * @param mode the mode for this entry + */ + public void setMode(int mode) { + this.mode = mode; + } + + /** + * Get this entry's link name. + * + * @return This entry's link name. + */ + public String getLinkName() { + 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. + * + * @return This entry's user id. + * @deprecated use #getLongUserId instead as user ids can be + * bigger than {@link Integer#MAX_VALUE} + */ + @Deprecated + public int getUserId() { + return (int) (userId & 0xffffffff); + } + + /** + * Set this entry's user id. + * + * @param userId This entry's new user id. + */ + public void setUserId(int userId) { + setUserId((long) userId); + } + + /** + * Get this entry's user id. + * + * @return This entry's user id. + * @since 1.9.5 + */ + public long getLongUserId() { + return userId; + } + + /** + * Set this entry's user id. + * + * @param userId This entry's new user id. + * @since 1.9.5 + */ + public void setUserId(long userId) { + this.userId = userId; + } + + /** + * Get this entry's group id. + * + * @return This entry's group id. + * @deprecated use #getLongGroupId instead as group ids can be + * bigger than {@link Integer#MAX_VALUE} + */ + @Deprecated + public int getGroupId() { + return (int) (groupId & 0xffffffff); + } + + /** + * Set this entry's group id. + * + * @param groupId This entry's new group id. + */ + public void setGroupId(int groupId) { + setGroupId((long) groupId); + } + + /** + * Get this entry's group id. + * + * @return This entry's group id. + * @since 1.9.5 + */ + public long getLongGroupId() { + return groupId; + } + + /** + * Set this entry's group id. + * + * @param groupId This entry's new group id. + * @since 1.9.5 + */ + public void setGroupId(long groupId) { + this.groupId = groupId; + } + + /** + * Get this entry's user name. + * + * @return This entry's user name. + */ + public String getUserName() { + return userName.toString(); + } + + /** + * Set this entry's user name. + * + * @param userName This entry's new user name. + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * Get this entry's group name. + * + * @return This entry's group name. + */ + public String getGroupName() { + return groupName.toString(); + } + + /** + * Set this entry's group name. + * + * @param groupName This entry's new group name. + */ + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + /** + * Convenience method to set this entry's group and user ids. + * + * @param userId This entry's new user id. + * @param groupId This entry's new group id. + */ + public void setIds(int userId, int groupId) { + setUserId(userId); + setGroupId(groupId); + } + + /** + * Convenience method to set this entry's group and user names. + * + * @param userName This entry's new user name. + * @param groupName This entry's new group name. + */ + public void setNames(String userName, String groupName) { + setUserName(userName); + setGroupName(groupName); + } + + /** + * Set this entry's modification time. The parameter passed + * to this method is in "Java time". + * + * @param time This entry's new modification time. + */ + public void setModTime(long time) { + modTime = time / MILLIS_PER_SECOND; + } + + /** + * Set this entry's modification time. + * + * @param time This entry's new modification time. + */ + public void setModTime(Date time) { + modTime = time.getTime() / MILLIS_PER_SECOND; + } + + /** + * Set this entry's modification time. + * + * @return time This entry's new modification time. + */ + public Date getModTime() { + return new Date(modTime * MILLIS_PER_SECOND); + } + + /** + * Get this entry's file. + * + * @return This entry's file. + */ + public File getFile() { + return file; + } + + /** + * Get this entry's mode. + * + * @return This entry's mode. + */ + public int getMode() { + return mode; + } + + /** + * Get this entry's file size. + * + * @return This entry's file size. + */ + public long getSize() { + return size; + } + + /** + * Set this entry's file size. + * + * @param size This entry's new file size. + * @throws IllegalArgumentException if the size is < 0. + */ + public void setSize(long size) { + if (size < 0) { + throw new IllegalArgumentException("Size is out of range: " + size); + } + this.size = size; + } + + /** + * Get this entry's major device number. + * + * @return This entry's major device number. + */ + public int getDevMajor() { + return devMajor; + } + + /** + * Set this entry's major device number. + * + * @param devNo This entry's major device number. + * @throws IllegalArgumentException if the devNo is < 0. + */ + public void setDevMajor(int devNo) { + if (devNo < 0) { + throw new IllegalArgumentException("Major device number is out of " + + "range: " + devNo); + } + this.devMajor = devNo; + } + + /** + * Get this entry's minor device number. + * + * @return This entry's minor device number. + */ + public int getDevMinor() { + return devMinor; + } + + /** + * Set this entry's minor device number. + * + * @param devNo This entry's minor device number. + * @throws IllegalArgumentException if the devNo is < 0. + */ + public void setDevMinor(int devNo) { + if (devNo < 0) { + throw new IllegalArgumentException("Minor device number is out of " + + "range: " + devNo); + } + this.devMinor = devNo; + } + + /** + * Indicates in case of a sparse file if an extension sparse header + * follows. + * + * @return true if an extension sparse header follows. + */ + public boolean isExtended() { + return isExtended; + } + + /** + * Get this entry's real file size in case of a sparse file. + * + * @return This entry's real file size. + */ + public long getRealSize() { + return realSize; + } + + /** + * Indicate if this entry is a GNU sparse block. + * + * @return true if this is a sparse extension provided by GNU tar + */ + public boolean isGNUSparse() { + return linkFlag == LF_GNUTYPE_SPARSE; + } + + /** + * Indicate if this entry is a GNU long linkname block + * + * @return true if this is a long name extension provided by GNU tar + */ + public boolean isGNULongLinkEntry() { + return linkFlag == LF_GNUTYPE_LONGLINK + && name.equals(GNU_LONGLINK); + } + + /** + * Indicate if this entry is a GNU long name block + * + * @return true if this is a long name extension provided by GNU tar + */ + public boolean isGNULongNameEntry() { + return linkFlag == LF_GNUTYPE_LONGNAME + && 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; + } + + /** + * Return whether or not this entry represents a directory. + * + * @return True if this entry is a directory. + */ + public boolean isDirectory() { + if (file != null) { + return file.isDirectory(); + } + + if (linkFlag == LF_DIR) { + return true; + } + + if (getName().endsWith("/")) { + return true; + } + + return false; + } + + /** + * Check if this is a "normal file". + * @return <i>true</i> if it 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. + * @return <i>true</i> if it is a symlink + */ + public boolean isSymbolicLink() { + return linkFlag == LF_SYMLINK; + } + + /** + * Check if this is a link entry. + * @return <i>true</i> if it is a link + */ + public boolean isLink() { + return linkFlag == LF_LINK; + } + + /** + * Check if this is a character device entry. + * @return <i>true</i> if it is a character device entry + */ + public boolean isCharacterDevice() { + return linkFlag == LF_CHR; + } + + /** + * @return <i>true</i> if this is a block device entry. + */ + public boolean isBlockDevice() { + return linkFlag == LF_BLK; + } + + /** + * @return <i>true</i> if this is a FIFO (pipe) entry. + */ + public boolean isFIFO() { + return linkFlag == LF_FIFO; + } + + /** + * If this entry represents a file, and the file is a directory, return + * an array of TarEntries for this entry's children. + * + * @return An array of TarEntry's for this entry's children. + */ + public TarEntry[] getDirectoryEntries() { + if (file == null || !file.isDirectory()) { + return new TarEntry[0]; + } + + String[] list = file.list(); + TarEntry[] result = new TarEntry[list.length]; + + for (int i = 0; i < list.length; ++i) { + result[i] = new TarEntry(new File(file, list[i])); + } + + return result; + } + + /** + * 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. + */ + 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 + * @throws IOException if an error occurs while writing the archive + */ + public void writeEntryHeader(byte[] outbuf, ZipEncoding encoding, + boolean starMode) throws IOException { + int offset = 0; + + 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; + + for (int c = 0; c < CHKSUMLEN; ++c) { + outbuf[offset++] = (byte) ' '; + } + + outbuf[offset++] = linkFlag; + offset = TarUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN, + encoding); + offset = TarUtils.formatNameBytes(magic, outbuf, offset, PURE_MAGICLEN); + offset = TarUtils.formatNameBytes(version, outbuf, offset, VERSIONLEN); + offset = TarUtils.formatNameBytes(userName, outbuf, offset, UNAMELEN, + encoding); + offset = TarUtils.formatNameBytes(groupName, outbuf, offset, GNAMELEN, + encoding); + offset = writeEntryHeaderField(devMajor, outbuf, offset, DEVLEN, + starMode); + offset = writeEntryHeaderField(devMinor, outbuf, offset, DEVLEN, + starMode); + + while (offset < outbuf.length) { + outbuf[offset++] = 0; + } + + long chk = TarUtils.computeCheckSum(outbuf); + + TarUtils.formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); + } + + private int writeEntryHeaderField(long value, byte[] outbuf, int offset, + int length, boolean starMode) { + if (!starMode && (value < 0 + || value >= (1L << (3 * (length - 1))))) { + // value doesn't fit into field when written as octal + // number, will be written to PAX header or causes an + // error + return TarUtils.formatLongOctalBytes(0, outbuf, offset, length); + } + return TarUtils.formatLongOctalOrBinaryBytes(value, outbuf, offset, + length); + } + + /** + * Parse an entry's header information from a header buffer. + * + * @param header The tar entry header buffer to get information from. + * @throws IllegalArgumentException if any of the numeric fields have an invalid format + */ + public void parseTarHeader(byte[] header) { + try { + parseTarHeader(header, TarUtils.DEFAULT_ENCODING); + } catch (IOException ex) { + try { + parseTarHeader(header, TarUtils.DEFAULT_ENCODING, true); + } catch (IOException ex2) { + // not really possible + throw new RuntimeException(ex2); + } + } + } + + /** + * Parse an entry's header information from a header buffer. + * + * @param header The tar entry header buffer to get information from. + * @param encoding encoding to use for file names + * @throws IllegalArgumentException if any of the numeric fields + * have an invalid format + * @throws IOException if an error occurs while reading the archive + */ + public void parseTarHeader(byte[] header, ZipEncoding encoding) + throws IOException { + parseTarHeader(header, encoding, false); + } + + private void parseTarHeader(byte[] header, ZipEncoding encoding, + final boolean oldStyle) + throws IOException { + int offset = 0; + + name = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) + : TarUtils.parseName(header, offset, NAMELEN, encoding); + offset += NAMELEN; + mode = (int) TarUtils.parseOctalOrBinary(header, offset, MODELEN); + offset += MODELEN; + userId = (int) TarUtils.parseOctalOrBinary(header, offset, UIDLEN); + offset += UIDLEN; + groupId = (int) TarUtils.parseOctalOrBinary(header, offset, GIDLEN); + offset += GIDLEN; + size = TarUtils.parseOctalOrBinary(header, offset, SIZELEN); + offset += SIZELEN; + modTime = TarUtils.parseOctalOrBinary(header, offset, MODTIMELEN); + offset += MODTIMELEN; + offset += CHKSUMLEN; + linkFlag = header[offset++]; + linkName = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) + : TarUtils.parseName(header, offset, NAMELEN, encoding); + offset += NAMELEN; + magic = TarUtils.parseName(header, offset, PURE_MAGICLEN); + offset += PURE_MAGICLEN; + version = TarUtils.parseName(header, offset, VERSIONLEN); + offset += VERSIONLEN; + userName = oldStyle ? TarUtils.parseName(header, offset, UNAMELEN) + : TarUtils.parseName(header, offset, UNAMELEN, encoding); + offset += UNAMELEN; + groupName = oldStyle ? TarUtils.parseName(header, offset, GNAMELEN) + : TarUtils.parseName(header, offset, GNAMELEN, encoding); + offset += GNAMELEN; + devMajor = (int) TarUtils.parseOctalOrBinary(header, offset, DEVLEN); + 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; + } + } + } + } + + /** + * Strips Windows' drive letter as well as any leading slashes, + * turns path separators into forward slahes. + */ + private static String normalizeFileName(String fileName, + boolean preserveLeadingSlashes) { + String osname = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + + if (osname != null) { + + // Strip off drive letters! + // REVIEW Would a better check be "(File.separator == '\')"? + + if (osname.startsWith("windows")) { + if (fileName.length() > 2) { + char ch1 = fileName.charAt(0); + char ch2 = fileName.charAt(1); + + if (ch2 == ':' + && ((ch1 >= 'a' && ch1 <= 'z') + || (ch1 >= 'A' && ch1 <= 'Z'))) { + fileName = fileName.substring(2); + } + } + } else if (osname.indexOf("netware") > -1) { + int colon = fileName.indexOf(':'); + if (colon != -1) { + fileName = fileName.substring(colon + 1); + } + } + } + + fileName = fileName.replace(File.separatorChar, '/'); + + // No absolute pathnames + // Windows (and Posix?) paths can start with "\\NetworkDrive\", + // so we loop on starting /'s. + while (!preserveLeadingSlashes && fileName.startsWith("/")) { + fileName = fileName.substring(1); + } + 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; + } +} diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarInputStream.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarInputStream.java new file mode 100644 index 00000000..57827a2c --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarInputStream.java @@ -0,0 +1,660 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.tools.zip.ZipEncoding; +import org.apache.tools.zip.ZipEncodingHelper; + +/** + * The TarInputStream reads a UNIX tar archive as an InputStream. + * methods are provided to position at each successive entry in + * the archive, and the read each entry as a normal input stream + * using read(). + * + */ +public class TarInputStream extends FilterInputStream { + private static final int SMALL_BUFFER_SIZE = 256; + private static final int BUFFER_SIZE = 8 * 1024; + private static final int LARGE_BUFFER_SIZE = 32 * 1024; + private static final int BYTE_MASK = 0xFF; + + private final byte[] SKIP_BUF = new byte[BUFFER_SIZE]; + private final byte[] SMALL_BUF = new byte[SMALL_BUFFER_SIZE]; + + // CheckStyle:VisibilityModifier OFF - bc + protected boolean debug; + protected boolean hasHitEOF; + protected long entrySize; + protected long entryOffset; + protected byte[] readBuf; + protected TarBuffer buffer; + protected TarEntry currEntry; + + /** + * This contents of this array is not used at all in this class, + * it is only here to avoid repreated object creation during calls + * to the no-arg read method. + */ + protected byte[] oneBuf; + + // CheckStyle:VisibilityModifier ON + + private final ZipEncoding encoding; + + /** + * Constructor for TarInputStream. + * @param is the input stream to use + */ + public TarInputStream(InputStream is) { + 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. + * @param is the input stream to use + * @param blockSize the block size to use + */ + public TarInputStream(InputStream is, int blockSize) { + 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. + * @param is the input stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + */ + public TarInputStream(InputStream is, int blockSize, int recordSize) { + this(is, blockSize, recordSize, null); + } + + /** + * Constructor for TarInputStream. + * @param is the input stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + * @param encoding name of the encoding to use for file names + */ + public TarInputStream(InputStream is, int blockSize, int recordSize, + String encoding) { + super(is); + this.buffer = new TarBuffer(is, blockSize, recordSize); + this.readBuf = null; + this.oneBuf = new byte[1]; + this.debug = false; + this.hasHitEOF = false; + this.encoding = ZipEncodingHelper.getZipEncoding(encoding); + } + + /** + * Sets the debugging flag. + * + * @param debug True to turn on debugging. + */ + public void setDebug(boolean debug) { + this.debug = debug; + buffer.setDebug(debug); + } + + /** + * Closes this stream. Calls the TarBuffer's close() method. + * @throws IOException on error + */ + @Override + public void close() throws IOException { + buffer.close(); + } + + /** + * Get the record size being used by this stream's TarBuffer. + * + * @return The TarBuffer record size. + */ + public int getRecordSize() { + return buffer.getRecordSize(); + } + + /** + * Get the available data that can be read from the current + * entry in the archive. This does not indicate how much data + * is left in the entire archive, only in the current entry. + * This value is determined from the entry's size header field + * and the amount of data already read from the current entry. + * Integer.MAX_VALUE is returned in case more than Integer.MAX_VALUE + * bytes are left in the current entry in the archive. + * + * @return The number of available bytes for the current entry. + * @throws IOException for signature + */ + @Override + public int available() throws IOException { + if (entrySize - entryOffset > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) (entrySize - entryOffset); + } + + /** + * Skip bytes in the input buffer. This skips bytes in the + * current entry's data, not the entire archive, and will + * stop at the end of the current entry's data if the number + * to skip extends beyond that point. + * + * @param numToSkip The number of bytes to skip. + * @return the number actually skipped + * @throws IOException on error + */ + @Override + public long skip(long numToSkip) throws IOException { + // REVIEW + // This is horribly inefficient, but it ensures that we + // properly skip over bytes via the TarBuffer... + // + long skip = numToSkip; + while (skip > 0) { + int realSkip = (int) (skip > SKIP_BUF.length + ? SKIP_BUF.length : skip); + int numRead = read(SKIP_BUF, 0, realSkip); + if (numRead == -1) { + break; + } + skip -= numRead; + } + return (numToSkip - skip); + } + + /** + * Since we do not support marking just yet, we return false. + * + * @return False. + */ + @Override + public boolean markSupported() { + return false; + } + + /** + * Since we do not support marking just yet, we do nothing. + * + * @param markLimit The limit to mark. + */ + @Override + public void mark(int markLimit) { + } + + /** + * Since we do not support marking just yet, we do nothing. + */ + @Override + public void reset() { + } + + /** + * Get the next entry in this tar archive. This will skip + * over any remaining data in the current entry, if there + * is one, and place the input stream at the header of the + * next entry, and read the header and instantiate a new + * TarEntry from the header bytes and return that entry. + * If there are no more entries in the archive, null will + * be returned to indicate that the end of the archive has + * been reached. + * + * @return The next TarEntry in the archive, or null. + * @throws IOException on error + */ + public TarEntry getNextEntry() throws IOException { + if (hasHitEOF) { + return null; + } + + if (currEntry != null) { + long numToSkip = entrySize - entryOffset; + + if (debug) { + System.err.println("TarInputStream: SKIP currENTRY '" + + currEntry.getName() + "' SZ " + + entrySize + " OFF " + + entryOffset + " skipping " + + numToSkip + " bytes"); + } + + while (numToSkip > 0) { + long skipped = skip(numToSkip); + if (skipped <= 0) { + throw new RuntimeException("failed to skip current tar" + + " entry"); + } + numToSkip -= skipped; + } + + readBuf = null; + } + + byte[] headerBuf = getRecord(); + + if (hasHitEOF) { + currEntry = null; + return null; + } + + 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.isGNULongLinkEntry()) { + byte[] longLinkData = getLongNameData(); + if (longLinkData == null) { + // Bugzilla: 40334 + // Malformed tar file - long link entry name not followed by + // entry + return null; + } + currEntry.setLinkName(encoding.decode(longLinkData)); + } + + if (currEntry.isGNULongNameEntry()) { + byte[] longNameData = getLongNameData(); + if (longNameData == null) { + // Bugzilla: 40334 + // Malformed tar file - long entry name not followed by + // entry + return null; + } + currEntry.setName(encoding.decode(longNameData)); + } + + if (currEntry.isPaxHeader()){ // Process Pax headers + paxHeaders(); + } + + if (currEntry.isGNUSparse()){ // Process sparse files + readGNUSparse(); + } + + // If the size of the next element in the archive has changed + // due to a new size being reported in the posix header + // information, we update entrySize here so that it contains + // the correct value. + entrySize = currEntry.getSize(); + return currEntry; + } + + /** + * Get the next entry in this tar archive as longname data. + * + * @return The next entry in the archive as longname data, or null. + * @throws IOException on error + */ + protected byte[] getLongNameData() throws IOException { + // read in the name + ByteArrayOutputStream longName = new ByteArrayOutputStream(); + int length = 0; + while ((length = read(SMALL_BUF)) >= 0) { + longName.write(SMALL_BUF, 0, length); + } + getNextEntry(); + if (currEntry == null) { + // Bugzilla: 40334 + // Malformed tar file - long entry name not followed by entry + return null; + } + byte[] longNameData = longName.toByteArray(); + // remove trailing null terminator(s) + length = longNameData.length; + while (length > 0 && longNameData[length - 1] == 0) { + --length; + } + if (length != longNameData.length) { + byte[] l = new byte[length]; + System.arraycopy(longNameData, 0, l, 0, length); + longNameData = l; + } + return longNameData; + } + + /** + * 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 + final int restLen = len - read; + byte[] rest = new byte[restLen]; + int got = 0; + while (got < restLen && (ch = i.read()) != -1) { + rest[got++] = (byte) ch; + } + if (got != restLen) { + throw new IOException("Failed to read " + + "Paxheader. Expected " + + restLen + + " bytes, read " + + got); + } + // Drop trailing NL + String value = new String(rest, 0, + restLen - 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(Long.parseLong(val)); + } else if ("gname".equals(key)){ + currEntry.setGroupName(val); + } else if ("uid".equals(key)){ + currEntry.setUserId(Long.parseLong(val)); + } else if ("uname".equals(key)){ + currEntry.setUserName(val); + } else if ("size".equals(key)){ + currEntry.setSize(Long.parseLong(val)); + } else if ("mtime".equals(key)){ + currEntry.setModTime((long) (Double.parseDouble(val) * 1000)); + } else if ("SCHILY.devminor".equals(key)){ + currEntry.setDevMinor(Integer.parseInt(val)); + } else if ("SCHILY.devmajor".equals(key)){ + currEntry.setDevMajor(Integer.parseInt(val)); + } + } + } + + /** + * Adds the sparse chunks from the current entry to the sparse chunks, + * including any additional sparse entries following the current entry. + * + * @throws IOException on error + * + * @todo Sparse files get not yet really processed. + */ + private void readGNUSparse() throws IOException { + /* we do not really process sparse files yet + sparses = new ArrayList(); + sparses.addAll(currEntry.getSparses()); + */ + if (currEntry.isExtended()) { + TarArchiveSparseEntry entry; + do { + byte[] headerBuf = getRecord(); + if (hasHitEOF) { + currEntry = null; + break; + } + entry = new TarArchiveSparseEntry(headerBuf); + /* we do not really process sparse files yet + sparses.addAll(entry.getSparses()); + */ + } while (entry.isExtended()); + } + } + + /** + * Reads a byte from the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @return The byte read, or -1 at EOF. + * @throws IOException on error + */ + @Override + public int read() throws IOException { + int num = read(oneBuf, 0, 1); + return num == -1 ? -1 : (oneBuf[0]) & BYTE_MASK; + } + + /** + * Reads bytes from the current tar archive entry. + * + * This method is aware of the boundaries of the current + * entry in the archive and will deal with them as if they + * were this stream's start and EOF. + * + * @param buf The buffer into which to place bytes read. + * @param offset The offset at which to place bytes read. + * @param numToRead The number of bytes to read. + * @return The number of bytes read, or -1 at EOF. + * @throws IOException on error + */ + @Override + public int read(byte[] buf, int offset, int numToRead) throws IOException { + int totalRead = 0; + + if (entryOffset >= entrySize) { + return -1; + } + + if ((numToRead + entryOffset) > entrySize) { + numToRead = (int) (entrySize - entryOffset); + } + + if (readBuf != null) { + int sz = (numToRead > readBuf.length) ? readBuf.length + : numToRead; + + System.arraycopy(readBuf, 0, buf, offset, sz); + + if (sz >= readBuf.length) { + readBuf = null; + } else { + int newLen = readBuf.length - sz; + byte[] newBuf = new byte[newLen]; + + System.arraycopy(readBuf, sz, newBuf, 0, newLen); + + readBuf = newBuf; + } + + totalRead += sz; + numToRead -= sz; + offset += sz; + } + + while (numToRead > 0) { + byte[] rec = buffer.readRecord(); + + if (rec == null) { + // Unexpected EOF! + throw new IOException("unexpected EOF with " + numToRead + + " bytes unread"); + } + + int sz = numToRead; + int recLen = rec.length; + + if (recLen > sz) { + System.arraycopy(rec, 0, buf, offset, sz); + + readBuf = new byte[recLen - sz]; + + System.arraycopy(rec, sz, readBuf, 0, recLen - sz); + } else { + sz = recLen; + + System.arraycopy(rec, 0, buf, offset, recLen); + } + + totalRead += sz; + numToRead -= sz; + offset += sz; + } + + entryOffset += totalRead; + + return totalRead; + } + + /** + * Copies the contents of the current tar archive entry directly into + * an output stream. + * + * @param out The OutputStream into which to write the entry's data. + * @throws IOException on error + */ + public void copyEntryContents(OutputStream out) throws IOException { + byte[] buf = new byte[LARGE_BUFFER_SIZE]; + + while (true) { + int numRead = read(buf, 0, buf.length); + + if (numRead == -1) { + break; + } + + 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(); + } +} + diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarOutputStream.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarOutputStream.java new file mode 100644 index 00000000..032f7254 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarOutputStream.java @@ -0,0 +1,657 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +package org.apache.tools.tar; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.util.Date; +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. + * Methods are provided to put entries, and then write their contents + * by writing to this stream using write(). + * + */ +public class TarOutputStream extends FilterOutputStream { + /** Fail if a long file name is required in the archive. */ + public static final int LONGFILE_ERROR = 0; + + /** Long paths will be truncated in the archive. */ + public static final int LONGFILE_TRUNCATE = 1; + + /** GNU tar extensions are used to store long file names in the archive. */ + public static final int LONGFILE_GNU = 2; + + /** POSIX/PAX extensions are used to store long file names in the archive. */ + public static final int LONGFILE_POSIX = 3; + + /** Fail if a big number (e.g. size > 8GiB) is required in the archive. */ + public static final int BIGNUMBER_ERROR = 0; + + /** star/GNU tar/BSD tar extensions are used to store big number in the archive. */ + public static final int BIGNUMBER_STAR = 1; + + /** POSIX/PAX extensions are used to store big numbers in the archive. */ + public static final int BIGNUMBER_POSIX = 2; + + // CheckStyle:VisibilityModifier OFF - bc + protected boolean debug; + protected long currSize; + protected String currName; + protected long currBytes; + protected byte[] oneBuf; + protected byte[] recordBuf; + protected int assemLen; + protected byte[] assemBuf; + protected TarBuffer buffer; + protected int longFileMode = LONGFILE_ERROR; + // CheckStyle:VisibilityModifier ON + + private int bigNumberMode = BIGNUMBER_ERROR; + + private boolean closed = false; + + /** Indicates if putNextEntry has been called without closeEntry */ + private boolean haveUnclosedEntry = false; + + /** indicates if this archive is finished */ + private boolean finished = false; + + private final ZipEncoding encoding; + + private boolean addPaxHeadersForNonAsciiNames = false; + private static final ZipEncoding ASCII = + ZipEncodingHelper.getZipEncoding("ASCII"); + + /** + * Constructor for TarInputStream. + * @param os the output stream to use + */ + public TarOutputStream(OutputStream os) { + 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. + * @param os the output stream to use + * @param blockSize the block size to use + */ + public TarOutputStream(OutputStream os, int blockSize) { + 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. + * @param os the output stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + */ + public TarOutputStream(OutputStream os, int blockSize, int recordSize) { + this(os, blockSize, recordSize, null); + } + + /** + * Constructor for TarInputStream. + * @param os the output stream to use + * @param blockSize the block size to use + * @param recordSize the record size to use + * @param encoding name of the encoding to use for file names + */ + public TarOutputStream(OutputStream os, int blockSize, int recordSize, + String encoding) { + super(os); + this.encoding = ZipEncodingHelper.getZipEncoding(encoding); + + this.buffer = new TarBuffer(os, blockSize, recordSize); + this.debug = false; + this.assemLen = 0; + this.assemBuf = new byte[recordSize]; + this.recordBuf = new byte[recordSize]; + this.oneBuf = new byte[1]; + } + + /** + * Set the long file mode. + * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2). + * This specifies the treatment of long file names (names >= TarConstants.NAMELEN). + * Default is LONGFILE_ERROR. + * @param longFileMode the mode to use + */ + public void setLongFileMode(int 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. + * + * @param debugF True to turn on debugging. + */ + public void setDebug(boolean debugF) { + this.debug = debugF; + } + + /** + * Sets the debugging flag in this stream's TarBuffer. + * + * @param debug True to turn on debugging. + */ + public void setBufferDebug(boolean debug) { + buffer.setDebug(debug); + } + + /** + * Ends the TAR archive without closing the underlying OutputStream. + * + * An archive consists of a series of file entries terminated by an + * end-of-archive entry, which consists of two 512 blocks of zero bytes. + * POSIX.1 requires two EOF records, like some other implementations. + * + * @throws IOException on error + */ + public void finish() throws IOException { + if (finished) { + throw new IOException("This archive has already been finished"); + } + + if (haveUnclosedEntry) { + throw new IOException("This archives contains unclosed entries."); + } + writeEOFRecord(); + writeEOFRecord(); + buffer.flushBlock(); + finished = true; + } + + /** + * Ends the TAR archive and closes the underlying OutputStream. + * This means that finish() is called followed by calling the + * TarBuffer's close(). + * @throws IOException on error + */ + @Override + public void close() throws IOException { + if(!finished) { + finish(); + } + + if (!closed) { + buffer.close(); + out.close(); + closed = true; + } + } + + /** + * Get the record size being used by this stream's TarBuffer. + * + * @return The TarBuffer record size. + */ + public int getRecordSize() { + return buffer.getRecordSize(); + } + + /** + * Put an entry on the output stream. This writes the entry's + * header record and positions the output stream for writing + * the contents of the entry. Once this method is called, the + * stream is ready for calls to write() to write the entry's + * contents. Once the contents are written, closeEntry() + * <B>MUST</B> be called to ensure that all buffered data + * is completely written to the output stream. + * + * @param entry The TarEntry to be written to the archive. + * @throws IOException on error + */ + public void putNextEntry(TarEntry entry) throws IOException { + if(finished) { + throw new IOException("Stream has already been finished"); + } + Map<String, String> paxHeaders = new HashMap<String, String>(); + final String entryName = entry.getName(); + boolean paxHeaderContainsPath = handleLongName(entry, entryName, paxHeaders, "path", + TarConstants.LF_GNUTYPE_LONGNAME, "file name"); + + final String linkName = entry.getLinkName(); + boolean paxHeaderContainsLinkPath = linkName != null && linkName.length() > 0 + && handleLongName(entry, linkName, paxHeaders, "linkpath", + TarConstants.LF_GNUTYPE_LONGLINK, "link name"); + + 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 && !paxHeaderContainsLinkPath + && (entry.isLink() || entry.isSymbolicLink()) + && !ASCII.canEncode(linkName)) { + paxHeaders.put("linkpath", linkName); + } + + if (paxHeaders.size() > 0) { + writePaxHeaders(entry, entryName, paxHeaders); + } + + entry.writeEntryHeader(recordBuf, encoding, + bigNumberMode == BIGNUMBER_STAR); + buffer.writeRecord(recordBuf); + + currBytes = 0; + + if (entry.isDirectory()) { + currSize = 0; + } else { + currSize = entry.getSize(); + } + currName = entryName; + haveUnclosedEntry = true; + } + + /** + * Close an entry. This method MUST be called for all file + * entries that contain data. The reason is that we must + * buffer data written to the stream in order to satisfy + * the buffer's record based writes. Thus, there may be + * data fragments still being assembled that must be written + * to the output stream before this entry is closed and the + * next entry written. + * @throws IOException on error + */ + public void closeEntry() throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + if (!haveUnclosedEntry){ + throw new IOException("No current entry to close"); + } + if (assemLen > 0) { + for (int i = assemLen; i < assemBuf.length; ++i) { + assemBuf[i] = 0; + } + + buffer.writeRecord(assemBuf); + + currBytes += assemLen; + assemLen = 0; + } + + if (currBytes < currSize) { + throw new IOException("entry '" + currName + "' closed at '" + + currBytes + + "' before the '" + currSize + + "' bytes specified in the header were written"); + } + haveUnclosedEntry = false; + } + + /** + * Writes a byte to the current tar archive entry. + * + * This method simply calls read( byte[], int, int ). + * + * @param b The byte written. + * @throws IOException on error + */ + @Override + public void write(int b) throws IOException { + oneBuf[0] = (byte) b; + + write(oneBuf, 0, 1); + } + + /** + * Writes bytes to the current tar archive entry. + * + * This method simply calls write( byte[], int, int ). + * + * @param wBuf The buffer to write to the archive. + * @throws IOException on error + */ + @Override + public void write(byte[] wBuf) throws IOException { + write(wBuf, 0, wBuf.length); + } + + /** + * Writes bytes to the current tar archive entry. This method + * is aware of the current entry and will throw an exception if + * you attempt to write bytes past the length specified for the + * current entry. The method is also (painfully) aware of the + * record buffering required by TarBuffer, and manages buffers + * that are not a multiple of recordsize in length, including + * assembling records from small buffers. + * + * @param wBuf The buffer to write to the archive. + * @param wOffset The offset in the buffer from which to get bytes. + * @param numToWrite The number of bytes to write. + * @throws IOException on error + */ + @Override + public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { + if ((currBytes + numToWrite) > currSize) { + throw new IOException("request to write '" + numToWrite + + "' bytes exceeds size in header of '" + + currSize + "' bytes for entry '" + + currName + "'"); + + // + // We have to deal with assembly!!! + // The programmer can be writing little 32 byte chunks for all + // we know, and we must assemble complete records for writing. + // REVIEW Maybe this should be in TarBuffer? Could that help to + // eliminate some of the buffer copying. + // + } + + if (assemLen > 0) { + if ((assemLen + numToWrite) >= recordBuf.length) { + int aLen = recordBuf.length - assemLen; + + System.arraycopy(assemBuf, 0, recordBuf, 0, + assemLen); + System.arraycopy(wBuf, wOffset, recordBuf, + assemLen, aLen); + buffer.writeRecord(recordBuf); + + currBytes += recordBuf.length; + wOffset += aLen; + numToWrite -= aLen; + assemLen = 0; + } else { + System.arraycopy(wBuf, wOffset, assemBuf, assemLen, + numToWrite); + + wOffset += numToWrite; + assemLen += numToWrite; + numToWrite = 0; + } + } + + // + // When we get here we have EITHER: + // o An empty "assemble" buffer. + // o No bytes to write (numToWrite == 0) + // + while (numToWrite > 0) { + if (numToWrite < recordBuf.length) { + System.arraycopy(wBuf, wOffset, assemBuf, assemLen, + numToWrite); + + assemLen += numToWrite; + + break; + } + + buffer.writeRecord(wBuf, wOffset); + + int num = recordBuf.length; + + currBytes += num; + numToWrite -= num; + wOffset += num; + } + } + + /** + * Writes a PAX extended header with the given map as contents. + */ + void writePaxHeaders(TarEntry entry, + 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); + } + while (name.endsWith("/")) { + // TarEntry's constructor would think this is a directory + // and not allow any data to be written + name = name.substring(0, name.length() - 1); + } + TarEntry pex = new TarEntry(name, + TarConstants.LF_PAX_EXTENDED_HEADER_LC); + transferModTime(entry, pex); + + 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(); + StringBuilder result = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char stripped = (char) (name.charAt(i) & 0x7F); + if (stripped != 0) { // would be read as Trailing null + result.append(stripped); + } + } + return result.toString(); + } + + /** + * Write an EOF (end of archive) record to the tar archive. + * An EOF record consists of a record of all zeros. + */ + private void writeEOFRecord() throws IOException { + for (int i = 0; i < recordBuf.length; ++i) { + recordBuf[i] = 0; + } + + buffer.writeRecord(recordBuf); + } + + private void addPaxHeadersForBigNumbers(Map<String, String> paxHeaders, + TarEntry entry) { + addPaxHeaderForBigNumber(paxHeaders, "size", entry.getSize(), + TarConstants.MAXSIZE); + addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(), + TarConstants.MAXID); + addPaxHeaderForBigNumber(paxHeaders, "mtime", + entry.getModTime().getTime() / 1000, + TarConstants.MAXSIZE); + addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(), + 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); + failForBigNumberWithPosixMessage("group id", entry.getLongGroupId(), TarConstants.MAXID); + failForBigNumber("last modification time", + entry.getModTime().getTime() / 1000, + TarConstants.MAXSIZE); + failForBigNumber("user id", entry.getLongUserId(), 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) { + failForBigNumber(field, value, maxValue, ""); + } + + private void failForBigNumberWithPosixMessage(String field, long value, long maxValue) { + failForBigNumber(field, value, maxValue, " Use STAR or POSIX extensions to overcome this limit"); + } + + private void failForBigNumber(String field, long value, long maxValue, String additionalMsg) { + if (value < 0 || value > maxValue) { + throw new RuntimeException(field + " '" + value + + "' is too big ( > " + + maxValue + " )"); + } + } + + /** + * Handles long file or link names according to the longFileMode setting. + * + * <p>I.e. if the given name is too long to be written to a plain + * tar header then + * <ul> + * <li>it creates a pax header who's name is given by the + * paxHeaderName parameter if longFileMode is POSIX</li> + * <li>it creates a GNU longlink entry who's type is given by + * the linkType parameter if longFileMode is GNU</li> + * <li>it throws an exception if longFileMode is ERROR</li> + * <li>it truncates the name if longFileMode is TRUNCATE</li> + * </ul></p> + * + * @param entry entry the name belongs to + * @param name the name to write + * @param paxHeaders current map of pax headers + * @param paxHeaderName name of the pax header to write + * @param linkType type of the GNU entry to write + * @param fieldName the name of the field + * @return whether a pax header has been written. + */ + private boolean handleLongName(TarEntry entry , String name, + Map<String, String> paxHeaders, + String paxHeaderName, byte linkType, String fieldName) + throws IOException { + final ByteBuffer encodedName = encoding.encode(name); + final int len = encodedName.limit() - encodedName.position(); + if (len >= TarConstants.NAMELEN) { + + if (longFileMode == LONGFILE_POSIX) { + paxHeaders.put(paxHeaderName, name); + return true; + } else if (longFileMode == LONGFILE_GNU) { + // create a TarEntry for the LongLink, the contents + // of which are the link's name + TarEntry longLinkEntry = + new TarEntry(TarConstants.GNU_LONGLINK, linkType); + + longLinkEntry.setSize(len + 1); // +1 for NUL + transferModTime(entry, longLinkEntry); + putNextEntry(longLinkEntry); + write(encodedName.array(), encodedName.arrayOffset(), len); + write(0); // NUL terminator + closeEntry(); + } else if (longFileMode != LONGFILE_TRUNCATE) { + throw new RuntimeException(fieldName + " '" + name + + "' is too long ( > " + + TarConstants.NAMELEN + " bytes)"); + } + } + return false; + } + + private void transferModTime(TarEntry from, TarEntry to) { + Date fromModTime = from.getModTime(); + long fromModTimeSeconds = fromModTime.getTime() / 1000; + if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) { + fromModTime = new Date(0); + } + to.setModTime(fromModTime); + } +} diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarUtils.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarUtils.java new file mode 100644 index 00000000..12bd1da3 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/tar/TarUtils.java @@ -0,0 +1,564 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/* + * This package is based on the work done by Timothy Gerard Endres + * (time@ice.com) to whom the Ant project is very grateful for his great code. + */ + +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. + * + */ +// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) +public class TarUtils { + + 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(final String name) { return true; } + + public ByteBuffer encode(final String name) { + final int length = name.length(); + final 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(final byte[] buffer) { + final int length = buffer.length; + final StringBuilder result = new StringBuilder(length); + + for (int i = 0; i < length; ++i) { + final 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 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> + * + * <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 length The maximum number of bytes to parse - must be at least 2 bytes. + * @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(final byte[] buffer, final int offset, final int length) { + long result = 0; + int end = offset + length; + int start = offset; + + if (length < 2){ + throw new IllegalArgumentException("Length "+length+" must be at least 2"); + } + + if (buffer[start] == 0) { + return 0L; + } + + // Skip leading spaces + while (start < end){ + if (buffer[start] == ' '){ + start++; + } else { + break; + } + } + + // Trim all trailing NULs and spaces. + // The ustar and POSIX tar specs require a trailing NUL or + // space but some implementations use the extra digit for big + // sizes/uids/gids ... + byte trailer = buffer[end - 1]; + while (start < end && (trailer == 0 || trailer == ' ')) { + end--; + trailer = buffer[end - 1]; + } + + for ( ;start < end; start++) { + final byte currentByte = buffer[start]; + // CheckStyle:MagicNumber OFF + 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 + } + + return result; + } + + /** + * 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 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 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 long parseOctalOrBinary(final byte[] buffer, final int offset, + final int length) { + + 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); + } + + 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; + } + + private static long parseBinaryBigInteger(final byte[] buffer, + final int offset, + final int length, + final boolean negative) { + final 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(); + } + + /** + * Parse a boolean byte from a buffer. + * Leading spaces and NUL are ignored. + * The buffer may contain trailing spaces or NULs. + * + * @param buffer The buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @return The boolean value of the bytes. + * @throws IllegalArgumentException if an invalid byte is detected. + */ + public static boolean parseBoolean(final byte[] buffer, final int offset) { + return buffer[offset] == 1; + } + + // Helper method to generate the exception message + private static String exceptionMessage(final byte[] buffer, final int offset, + final int length, final int current, final byte currentByte) { + // default charset is good enough for an exception message, + // + // the alternative was to modify parseOctal and + // parseOctalOrBinary to receive the ZipEncoding of the + // archive (deprecating the existing public methods, of + // course) and dealing with the fact that ZipEncoding#decode + // can throw an IOException which parseOctal* doesn't declare + String string = new String(buffer, offset, length); + + 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(final byte[] buffer, final int offset, final int length) { + try { + return parseName(buffer, offset, length, DEFAULT_ENCODING); + } catch (final IOException ex) { + try { + return parseName(buffer, offset, length, FALLBACK_ENCODING); + } catch (final IOException ex2) { + // impossible + throw new RuntimeException(ex2); + } + } + } + + /** + * 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(final 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) { + final byte[] b = new byte[len]; + System.arraycopy(buffer, offset, b, 0, len); + return encoding.decode(b); + } + return ""; + } + + /** + * 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(final String name, final byte[] buf, final int offset, final int length) { + try { + return formatNameBytes(name, buf, offset, length, DEFAULT_ENCODING); + } catch (final IOException ex) { + try { + return formatNameBytes(name, buf, offset, length, + FALLBACK_ENCODING); + } catch (final IOException ex2) { + // impossible + throw new RuntimeException(ex2); + } + } + } + + /** + * 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. + * @param encoding name of the encoding to use for file names + * @return The updated offset, i.e. offset + length + */ + public static int formatNameBytes(final String name, final 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() - b.position(); + System.arraycopy(b.array(), b.arrayOffset(), buf, offset, limit); + + // 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, final byte[] buffer, + final int offset, final int length) { + int remaining = length; + remaining--; + if (value == 0) { + buffer[offset + remaining--] = (byte) '0'; + } else { + long val = value; + for (; remaining >= 0 && val != 0; --remaining) { + // CheckStyle:MagicNumber OFF + buffer[offset + remaining] = (byte) ((byte) '0' + (byte) (val & 7)); + val = val >>> 3; + // CheckStyle:MagicNumber ON + } + if (val != 0){ + throw new IllegalArgumentException + (value+"="+Long.toOctalString(value)+ " will not fit in octal number buffer of length "+length); + } + } + + 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, final 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; + } + + /** + * 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 formatLongOctalBytes(final long value, final byte[] buf, final int offset, final int length) { + + final int idx=length-1; // For space + + formatUnsignedOctalString(value, buf, offset, idx); + buf[offset + idx] = (byte) ' '; // Trailing space + + return offset + length; + } + + /** + * 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, final 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, final 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, final byte[] buf, + final int offset, + final int length, + final boolean negative) { + final 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 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 formatCheckSumOctalBytes(final long value, final byte[] buf, final int offset, final int length) { + + int idx=length-2; // for NUL and space + formatUnsignedOctalString(value, buf, offset, idx); + + buf[offset + idx++] = 0; // Trailing null + buf[offset + idx] = (byte) ' '; // Trailing space + + return offset + length; + } + + /** + * Compute the checksum of a tar entry header. + * + * @param buf The tar entry's header buffer. + * @return The computed checksum. + */ + public static long computeCheckSum(final byte[] buf) { + long sum = 0; + + for (final byte element : buf) { + sum += BYTE_MASK & element; + } + + return sum; + } + +} |