All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.compress.archivers.tar.TarArchiveEntry Maven / Gradle / Ivy

Go to download

Apache Commons Compress defines an API for working with compression and archive formats. These include bzip2, gzip, pack200, LZMA, XZ, Snappy, traditional Unix Compress, DEFLATE, DEFLATE64, LZ4, Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj.

There is a newer version: 1.27.1
Show newest version
/*
 * 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.commons.compress.archivers.tar;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributes;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.EntryStreamOffsets;
import org.apache.commons.compress.archivers.zip.ZipEncoding;
import org.apache.commons.compress.utils.ArchiveUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.compress.utils.ParsingUtils;
import org.apache.commons.compress.utils.TimeUtils;
import org.apache.commons.io.file.attribute.FileTimes;

/**
 * This class represents an entry in a Tar archive. It consists of the entry's header, as well as the entry's File. Entries can be instantiated in one of three
 * ways, depending on how they are to be used.
 * 

* TarEntries that are created from the header bytes read from an archive are instantiated with the {@link TarArchiveEntry#TarArchiveEntry(byte[])} constructor. * These entries will be used when extracting from or listing the contents of an archive. These entries have their header filled in using the header bytes. They * also set the File to null, since they reference an archive entry not a file. *

*

* TarEntries that are created from Files that are to be written into an archive are instantiated with the {@link TarArchiveEntry#TarArchiveEntry(File)} or * {@link TarArchiveEntry#TarArchiveEntry(Path)} constructor. These entries have their header filled in using the File's information. They also keep a reference * to the File for convenience when writing entries. *

*

* Finally, TarEntries can be constructed from nothing but a name. This allows the programmer to construct the entry by hand, for instance when only an * InputStream is available for writing to the archive, and the header information is constructed from other information. In this case the header fields are set * to defaults and the File is set to null. *

*

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

*
 * struct header {
 *   char name[100];     // TarConstants.NAMELEN    - offset   0
 *   char mode[8];       // TarConstants.MODELEN    - offset 100
 *   char uid[8];        // TarConstants.UIDLEN     - offset 108
 *   char gid[8];        // TarConstants.GIDLEN     - offset 116
 *   char size[12];      // TarConstants.SIZELEN    - offset 124
 *   char mtime[12];     // TarConstants.MODTIMELEN - offset 136
 *   char chksum[8];     // TarConstants.CHKSUMLEN  - offset 148
 *   char linkflag[1];   //                         - offset 156
 *   char linkname[100]; // TarConstants.NAMELEN    - offset 157
 *   // The following fields are only present in new-style POSIX tar archives:
 *   char magic[6];      // TarConstants.MAGICLEN   - offset 257
 *   char version[2];    // TarConstants.VERSIONLEN - offset 263
 *   char uname[32];     // TarConstants.UNAMELEN   - offset 265
 *   char gname[32];     // TarConstants.GNAMELEN   - offset 297
 *   char devmajor[8];   // TarConstants.DEVLEN     - offset 329
 *   char devminor[8];   // TarConstants.DEVLEN     - offset 337
 *   char prefix[155];   // TarConstants.PREFIXLEN  - offset 345
 *   // Used if "name" field is not long enough to hold the path
 *   char pad[12];       // NULs                    - offset 500
 * } 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 * {@link TarUtils#parseOctalOrBinary(byte[], int, int)}. *

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

*
 * struct oldgnu_header {
 *   char unused_pad1[345]; // TarConstants.PAD1LEN_GNU       - offset 0
 *   char atime[12];        // TarConstants.ATIMELEN_GNU      - offset 345
 *   char ctime[12];        // TarConstants.CTIMELEN_GNU      - offset 357
 *   char offset[12];       // TarConstants.OFFSETLEN_GNU     - offset 369
 *   char longnames[4];     // TarConstants.LONGNAMESLEN_GNU  - offset 381
 *   char unused_pad2;      // TarConstants.PAD2LEN_GNU       - offset 385
 *   struct sparse sp[4];   // TarConstants.SPARSELEN_GNU     - offset 386
 *   char isextended;       // TarConstants.ISEXTENDEDLEN_GNU - offset 482
 *   char realsize[12];     // TarConstants.REALSIZELEN_GNU   - offset 483
 *   char unused_pad[17];   // TarConstants.PAD3LEN_GNU       - offset 495
 * };
 * 
*

* Whereas, "struct sparse" is: *

*
 * struct sparse {
 *   char offset[12];   // offset 0
 *   char numbytes[12]; // offset 12
 * };
 * 
*

* The C structure for a xstar (Jörg Schilling star) Tar Entry's header is: *

*
 * struct star_header {
 *   char name[100];     // offset   0
 *   char mode[8];       // offset 100
 *   char uid[8];        // offset 108
 *   char gid[8];        // offset 116
 *   char size[12];      // offset 124
 *   char mtime[12];     // offset 136
 *   char chksum[8];     // offset 148
 *   char typeflag;      // offset 156
 *   char linkname[100]; // offset 157
 *   char magic[6];      // offset 257
 *   char version[2];    // offset 263
 *   char uname[32];     // offset 265
 *   char gname[32];     // offset 297
 *   char devmajor[8];   // offset 329
 *   char devminor[8];   // offset 337
 *   char prefix[131];   // offset 345
 *   char atime[12];     // offset 476
 *   char ctime[12];     // offset 488
 *   char mfill[8];      // offset 500
 *   char xmagic[4];     // offset 508  "tar\0"
 * };
 * 
*

* which is identical to new-style POSIX up to the first 130 bytes of the prefix. *

*

* The C structure for the xstar-specific parts of a xstar Tar Entry's header is: *

*
 * struct xstar_in_header {
 *   char fill[345];         // offset 0     Everything before t_prefix
 *   char prefix[1];         // offset 345   Prefix for t_name
 *   char fill2;             // offset 346
 *   char fill3[8];          // offset 347
 *   char isextended;        // offset 355
 *   struct sparse sp[SIH];  // offset 356   8 x 12
 *   char realsize[12];      // offset 452   Real size for sparse data
 *   char offset[12];        // offset 464   Offset for multivolume data
 *   char atime[12];         // offset 476
 *   char ctime[12];         // offset 488
 *   char mfill[8];          // offset 500
 *   char xmagic[4];         // offset 508   "tar\0"
 * };
 * 
* * @NotThreadSafe */ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamOffsets { private static final TarArchiveEntry[] EMPTY_TAR_ARCHIVE_ENTRY_ARRAY = {}; /** * Value used to indicate unknown mode, user/groupids, device numbers and modTime when parsing a file in lenient mode and the archive contains illegal * fields. * * @since 1.19 */ public static final long UNKNOWN = -1L; /** 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 * * @deprecated Unused. */ @Deprecated public static final int MILLIS_PER_SECOND = 1000; /** * Regular expression pattern for validating values in pax extended header file time fields. These fields contain two numeric values (seconds and sub-second * values) as per this definition: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_05 *

* Since they are parsed into long values, maximum length of each is the same as Long.MAX_VALUE which is 19 digits. *

*/ private static final Pattern PAX_EXTENDED_HEADER_FILE_TIMES_PATTERN = Pattern.compile("-?\\d{1,19}(?:\\.\\d{1,19})?"); private static FileTime fileTimeFromOptionalSeconds(final long seconds) { if (seconds <= 0) { return null; } return TimeUtils.unixTimeToFileTime(seconds); } /** * Strips Windows' drive letter as well as any leading slashes, turns path separators into forward slashes. */ private static String normalizeFileName(String fileName, final boolean preserveAbsolutePath) { if (!preserveAbsolutePath) { final String property = System.getProperty("os.name"); if (property != null) { final String osName = property.toLowerCase(Locale.ROOT); // Strip off drive letters! // REVIEW Would a better check be "(File.separator == '\')"? if (osName.startsWith("windows")) { if (fileName.length() > 2) { final char ch1 = fileName.charAt(0); final char ch2 = fileName.charAt(1); if (ch2 == ':' && (ch1 >= 'a' && ch1 <= 'z' || ch1 >= 'A' && ch1 <= 'Z')) { fileName = fileName.substring(2); } } } else if (osName.contains("netware")) { final int colon = fileName.indexOf(':'); if (colon != -1) { fileName = fileName.substring(colon + 1); } } } } fileName = fileName.replace(File.separatorChar, '/'); // No absolute pathnames // Windows (and Posix?) paths can start with "\\NetworkDrive\", // so we loop on starting /'s. while (!preserveAbsolutePath && fileName.startsWith("/")) { fileName = fileName.substring(1); } return fileName; } private static Instant parseInstantFromDecimalSeconds(final String value) throws IOException { // Validate field values to prevent denial of service attacks with BigDecimal values (see JDK-6560193) if (!TarArchiveEntry.PAX_EXTENDED_HEADER_FILE_TIMES_PATTERN.matcher(value).matches()) { throw new IOException("Corrupted PAX header. Time field value is invalid '" + value + "'"); } final BigDecimal epochSeconds = new BigDecimal(value); final long seconds = epochSeconds.longValue(); final long nanos = epochSeconds.remainder(BigDecimal.ONE).movePointRight(9).longValue(); try { return Instant.ofEpochSecond(seconds, nanos); } catch (DateTimeException | ArithmeticException e) { // DateTimeException: Thrown if the instant exceeds the maximum or minimum instant. // ArithmeticException: Thrown if numeric overflow occurs. throw new IOException("Corrupted PAX header. Time field value is invalid '" + value + "'", e); } } /** The entry's name. */ private String name = ""; /** Whether to allow leading slashes or drive names inside the name */ private final boolean preserveAbsolutePath; /** 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. Corresponds to the POSIX {@code mtime} attribute. */ private FileTime mTime; /** * The entry's status change time. Corresponds to the POSIX {@code ctime} attribute. * * @since 1.22 */ private FileTime cTime; /** * The entry's last access time. Corresponds to the POSIX {@code atime} attribute. * * @since 1.22 */ private FileTime aTime; /** * The entry's creation time. Corresponds to the POSIX {@code birthtime} attribute. * * @since 1.22 */ private FileTime birthTime; /** If the header checksum is reasonably correct. */ private boolean checkSumOK; /** The entry's link flag. */ private byte linkFlag; /** The entry's link name. */ private String linkName = ""; /** The entry's magic tag. */ private String magic = MAGIC_POSIX; /** The version of the format */ private String version = VERSION_POSIX; /** 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; /** The sparse headers in tar */ private List sparseHeaders; /** If an extension sparse header follows. */ private boolean isExtended; /** The entry's real size in case of a sparse file. */ private long realSize; /** Is this entry a GNU sparse entry using one of the PAX formats? */ private boolean paxGNUSparse; /** * is this entry a GNU sparse entry using 1.X PAX formats? the sparse headers of 1.x PAX Format is stored in file data block */ private boolean paxGNU1XSparse; /** Is this entry a star sparse entry using the PAX header? */ private boolean starSparse; /** The entry's file reference */ private final Path file; /** The entry's file linkOptions */ private final LinkOption[] linkOptions; /** Extra, user supplied pax headers */ private final Map extraPaxHeaders = new HashMap<>(); private long dataOffset = EntryStreamOffsets.OFFSET_UNKNOWN; /** * Constructs an empty entry and prepares the header values. */ private TarArchiveEntry(final boolean preserveAbsolutePath) { String user = System.getProperty("user.name", ""); if (user.length() > MAX_NAMELEN) { user = user.substring(0, MAX_NAMELEN); } this.userName = user; this.file = null; this.linkOptions = IOUtils.EMPTY_LINK_OPTIONS; this.preserveAbsolutePath = preserveAbsolutePath; } /** * Constructs 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 TarArchiveEntry(final byte[] headerBuf) { this(false); parseTarHeader(headerBuf); } /** * Constructs 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 * @since 1.4 * @throws IllegalArgumentException if any of the numeric fields have an invalid format * @throws IOException on error */ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding) throws IOException { this(headerBuf, encoding, false); } /** * Constructs 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 * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to * {@link #UNKNOWN}. When set to false such illegal fields cause an exception instead. * @since 1.19 * @throws IllegalArgumentException if any of the numeric fields have an invalid format * @throws IOException on error */ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding, final boolean lenient) throws IOException { this(Collections.emptyMap(), headerBuf, encoding, lenient); } /** * Constructs an entry from an archive's header bytes for random access tar. File is set to null. * * @param headerBuf the header bytes from a tar archive entry. * @param encoding encoding to use for file names. * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to * {@link #UNKNOWN}. When set to false such illegal fields cause an exception instead. * @param dataOffset position of the entry data in the random access file. * @since 1.21 * @throws IllegalArgumentException if any of the numeric fields have an invalid format. * @throws IOException on error. */ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding, final boolean lenient, final long dataOffset) throws IOException { this(headerBuf, encoding, lenient); setDataOffset(dataOffset); } /** * Constructs 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. *

* The entry's name will be the value of the {@code file}'s path with all file separators replaced by forward slashes and leading slashes as well as Windows * drive letters stripped. The name will end in a slash if the {@code file} represents a directory. *

*

* Note: Since 1.21 this internally uses the same code as the TarArchiveEntry constructors with a {@link Path} as parameter. But all thrown exceptions are * ignored. If handling those exceptions is needed consider switching to the path constructors. *

* * @param file The file that the entry represents. */ public TarArchiveEntry(final File file) { this(file, file.getPath()); } /** * Constructs an entry for a file. File is set to file, and the header is constructed from information from the file. *

* The entry's name will be the value of the {@code fileName} argument with all file separators replaced by forward slashes and leading slashes as well as * Windows drive letters stripped. The name will end in a slash if the {@code file} represents a directory. *

*

* Note: Since 1.21 this internally uses the same code as the TarArchiveEntry constructors with a {@link Path} as parameter. But all thrown exceptions are * ignored. If handling those exceptions is needed consider switching to the path constructors. *

* * @param file The file that the entry represents. * @param fileName the name to be used for the entry. */ public TarArchiveEntry(final File file, final String fileName) { final String normalizedName = normalizeFileName(fileName, false); this.file = file.toPath(); this.linkOptions = IOUtils.EMPTY_LINK_OPTIONS; try { readFileMode(this.file, normalizedName); } catch (final IOException e) { // Ignore exceptions from NIO for backwards compatibility // Fallback to get size of file if it's no directory to the old file api if (!file.isDirectory()) { this.size = file.length(); } } this.userName = ""; try { readOsSpecificProperties(this.file); } catch (final IOException e) { // Ignore exceptions from NIO for backwards compatibility // Fallback to get the last modified date of the file from the old file api this.mTime = FileTime.fromMillis(file.lastModified()); } preserveAbsolutePath = false; } /** * Constructs an entry from an archive's header bytes. File is set to null. * * @param globalPaxHeaders the parsed global PAX headers, or null if this is the first one. * @param headerBuf The header bytes from a tar archive entry. * @param encoding encoding to use for file names * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to * {@link #UNKNOWN}. When set to false such illegal fields cause an exception instead. * @since 1.22 * @throws IllegalArgumentException if any of the numeric fields have an invalid format * @throws IOException on error */ public TarArchiveEntry(final Map globalPaxHeaders, final byte[] headerBuf, final ZipEncoding encoding, final boolean lenient) throws IOException { this(false); parseTarHeader(globalPaxHeaders, headerBuf, encoding, false, lenient); } /** * Constructs an entry from an archive's header bytes for random access tar. File is set to null. * * @param globalPaxHeaders the parsed global PAX headers, or null if this is the first one. * @param headerBuf the header bytes from a tar archive entry. * @param encoding encoding to use for file names. * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to * {@link #UNKNOWN}. When set to false such illegal fields cause an exception instead. * @param dataOffset position of the entry data in the random access file. * @since 1.22 * @throws IllegalArgumentException if any of the numeric fields have an invalid format. * @throws IOException on error. */ public TarArchiveEntry(final Map globalPaxHeaders, final byte[] headerBuf, final ZipEncoding encoding, final boolean lenient, final long dataOffset) throws IOException { this(globalPaxHeaders, headerBuf, encoding, lenient); setDataOffset(dataOffset); } /** * Constructs 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. *

* The entry's name will be the value of the {@code file}'s path with all file separators replaced by forward slashes and leading slashes as well as Windows * drive letters stripped. The name will end in a slash if the {@code file} represents a directory. *

* * @param file The file that the entry represents. * @throws IOException if an I/O error occurs * @since 1.21 */ public TarArchiveEntry(final Path file) throws IOException { this(file, file.toString()); } /** * Constructs an entry for a file. File is set to file, and the header is constructed from information from the file. *

* The entry's name will be the value of the {@code fileName} argument with all file separators replaced by forward slashes and leading slashes as well as * Windows drive letters stripped. The name will end in a slash if the {@code file} represents a directory. *

* * @param file The file that the entry represents. * @param fileName the name to be used for the entry. * @param linkOptions options indicating how symbolic links are handled. * @throws IOException if an I/O error occurs * @since 1.21 */ public TarArchiveEntry(final Path file, final String fileName, final LinkOption... linkOptions) throws IOException { final String normalizedName = normalizeFileName(fileName, false); this.file = file; this.linkOptions = linkOptions == null ? IOUtils.EMPTY_LINK_OPTIONS : linkOptions; readFileMode(file, normalizedName, linkOptions); this.userName = ""; readOsSpecificProperties(file); preserveAbsolutePath = false; } /** * Constructs an entry with only a name. This allows the programmer to construct the entry's header "by hand". File is set to null. *

* The entry's name will be the value of the {@code name} argument with all file separators replaced by forward slashes and leading slashes as well as * Windows drive letters stripped. *

* * @param name the entry name */ public TarArchiveEntry(final String name) { this(name, false); } /** * Constructs an entry with only a name. This allows the programmer to construct the entry's header "by hand". File is set to null. *

* The entry's name will be the value of the {@code name} argument with all file separators replaced by forward slashes. Leading slashes and Windows drive * letters are stripped if {@code preserveAbsolutePath} is {@code false}. *

* * @param name the entry name * @param preserveAbsolutePath whether to allow leading slashes or drive letters in the name. * * @since 1.1 */ public TarArchiveEntry(String name, final boolean preserveAbsolutePath) { this(preserveAbsolutePath); name = normalizeFileName(name, preserveAbsolutePath); final boolean isDir = name.endsWith("/"); this.name = name; this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; this.linkFlag = isDir ? LF_DIR : LF_NORMAL; this.mTime = FileTime.from(Instant.now()); this.userName = ""; } /** * Constructs an entry with a name and a link flag. *

* The entry's name will be the value of the {@code name} argument with all file separators replaced by forward slashes and leading slashes as well as * Windows drive letters stripped. *

* * @param name the entry name * @param linkFlag the entry link flag. */ public TarArchiveEntry(final String name, final byte linkFlag) { this(name, linkFlag, false); } /** * Constructs an entry with a name and a link flag. *

* The entry's name will be the value of the {@code name} argument with all file separators replaced by forward slashes. Leading slashes and Windows drive * letters are stripped if {@code preserveAbsolutePath} is {@code false}. *

* * @param name the entry name * @param linkFlag the entry link flag. * @param preserveAbsolutePath whether to allow leading slashes or drive letters in the name. * * @since 1.5 */ public TarArchiveEntry(final String name, final byte linkFlag, final boolean preserveAbsolutePath) { this(name, preserveAbsolutePath); this.linkFlag = linkFlag; if (linkFlag == LF_GNUTYPE_LONGNAME) { magic = MAGIC_GNU; version = VERSION_GNU_SPACE; } } /** * Adds a PAX header to this entry. If the header corresponds to an existing field in the entry, that field will be set; otherwise the header will be added * to the extraPaxHeaders Map * * @param name The full name of the header to set. * @param value value of header. * @since 1.15 */ public void addPaxHeader(final String name, final String value) { try { processPaxHeader(name, value); } catch (final IOException ex) { throw new IllegalArgumentException("Invalid input", ex); } } /** * Clears all extra PAX headers. * * @since 1.15 */ public void clearExtraPaxHeaders() { extraPaxHeaders.clear(); } /** * Determine if the two entries are equal. Equality is determined by the header names being equal. * * @param it Entry to be checked for equality. * @return True if the entries are equal. */ @Override public boolean equals(final Object it) { if (it == null || getClass() != it.getClass()) { return false; } return equals((TarArchiveEntry) it); } /** * 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(final TarArchiveEntry it) { return it != null && getName().equals(it.getName()); } /** * Evaluates 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(final Map globalPaxHeaders, final byte[] header) { if (ArchiveUtils.matchAsciiBuffer(MAGIC_GNU, header, MAGIC_OFFSET, MAGICLEN)) { return FORMAT_OLDGNU; } if (ArchiveUtils.matchAsciiBuffer(MAGIC_POSIX, header, MAGIC_OFFSET, MAGICLEN)) { if (isXstar(globalPaxHeaders, header)) { return FORMAT_XSTAR; } return FORMAT_POSIX; } return 0; } private int fill(final byte value, final int offset, final byte[] outbuf, final int length) { for (int i = 0; i < length; i++) { outbuf[offset + i] = value; } return offset + length; } private int fill(final int value, final int offset, final byte[] outbuf, final int length) { return fill((byte) value, offset, outbuf, length); } void fillGNUSparse0xData(final Map headers) throws IOException { paxGNUSparse = true; realSize = ParsingUtils.parseIntValue(headers.get(TarGnuSparseKeys.SIZE)); if (headers.containsKey(TarGnuSparseKeys.NAME)) { // version 0.1 name = headers.get(TarGnuSparseKeys.NAME); } } void fillGNUSparse1xData(final Map headers) throws IOException { paxGNUSparse = true; paxGNU1XSparse = true; if (headers.containsKey(TarGnuSparseKeys.NAME)) { name = headers.get(TarGnuSparseKeys.NAME); } if (headers.containsKey(TarGnuSparseKeys.REALSIZE)) { realSize = ParsingUtils.parseIntValue(headers.get(TarGnuSparseKeys.REALSIZE)); } } void fillStarSparseData(final Map headers) throws IOException { starSparse = true; if (headers.containsKey("SCHILY.realsize")) { realSize = ParsingUtils.parseLongValue(headers.get("SCHILY.realsize")); } } /** * Gets this entry's creation time. * * @since 1.22 * @return This entry's computed creation time. */ public FileTime getCreationTime() { return birthTime; } /** * {@inheritDoc} * * @since 1.21 */ @Override public long getDataOffset() { return dataOffset; } /** * Gets this entry's major device number. * * @return This entry's major device number. * @since 1.4 */ public int getDevMajor() { return devMajor; } /** * Gets this entry's minor device number. * * @return This entry's minor device number. * @since 1.4 */ public int getDevMinor() { return devMinor; } /** * If this entry represents a file, and the file is a directory, return an array of TarEntries for this entry's children. *

* This method is only useful for entries created from a {@code * File} or {@code Path} but not for entries read from an archive. *

* * @return An array of TarEntry's for this entry's children. */ public TarArchiveEntry[] getDirectoryEntries() { if (file == null || !isDirectory()) { return EMPTY_TAR_ARCHIVE_ENTRY_ARRAY; } final List entries = new ArrayList<>(); try (DirectoryStream dirStream = Files.newDirectoryStream(file)) { for (final Path p : dirStream) { entries.add(new TarArchiveEntry(p)); } } catch (final IOException e) { return EMPTY_TAR_ARCHIVE_ENTRY_ARRAY; } return entries.toArray(EMPTY_TAR_ARCHIVE_ENTRY_ARRAY); } /** * Gets named extra PAX header * * @param name The full name of an extended PAX header to retrieve * @return The value of the header, if any. * @since 1.15 */ public String getExtraPaxHeader(final String name) { return extraPaxHeaders.get(name); } /** * Gets extra PAX Headers * * @return read-only map containing any extra PAX Headers * @since 1.15 */ public Map getExtraPaxHeaders() { return Collections.unmodifiableMap(extraPaxHeaders); } /** * Gets this entry's file. *

* This method is only useful for entries created from a {@code * File} or {@code Path} but not for entries read from an archive. *

* * @return this entry's file or null if the entry was not created from a file. */ public File getFile() { if (file == null) { return null; } return file.toFile(); } /** * Gets 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); } /** * Gets this entry's group name. * * @return This entry's group name. */ public String getGroupName() { return groupName; } /** * Gets this entry's last access time. * * @since 1.22 * @return This entry's last access time. */ public FileTime getLastAccessTime() { return aTime; } /** * Gets this entry's modification time. This is equivalent to {@link TarArchiveEntry#getLastModifiedTime()}, but precision is truncated to milliseconds. * * @return This entry's modification time. * @see TarArchiveEntry#getLastModifiedTime() */ @Override public Date getLastModifiedDate() { return getModTime(); } /** * Gets this entry's modification time. * * @since 1.22 * @return This entry's modification time. */ public FileTime getLastModifiedTime() { return mTime; } /** * Gets this entry's link flag. * * @return this entry's link flag. * @since 1.23 */ public byte getLinkFlag() { return this.linkFlag; } /** * Gets this entry's link name. * * @return This entry's link name. */ public String getLinkName() { return linkName; } /** * Gets this entry's group id. * * @since 1.10 * @return This entry's group id. */ public long getLongGroupId() { return groupId; } /** * Gets this entry's user id. * * @return This entry's user id. * @since 1.10 */ public long getLongUserId() { return userId; } /** * Gets this entry's mode. * * @return This entry's mode. */ public int getMode() { return mode; } /** * Gets this entry's modification time. This is equivalent to {@link TarArchiveEntry#getLastModifiedTime()}, but precision is truncated to milliseconds. * * @return This entry's modification time. * @see TarArchiveEntry#getLastModifiedTime() */ public Date getModTime() { final FileTime fileTime = mTime; return FileTimes.toDate(fileTime); } /** * Gets this entry's name. *

* This method returns the raw name as it is stored inside of the archive. *

* * @return This entry's name. */ @Override public String getName() { return name; } /** * Gets this entry's sparse headers ordered by offset with all empty sparse sections at the start filtered out. * * @return immutable list of this entry's sparse headers, never null * @since 1.21 * @throws IOException if the list of sparse headers contains blocks that overlap */ public List getOrderedSparseHeaders() throws IOException { if (sparseHeaders == null || sparseHeaders.isEmpty()) { return Collections.emptyList(); } final List orderedAndFiltered = sparseHeaders.stream().filter(s -> s.getOffset() > 0 || s.getNumbytes() > 0) .sorted(Comparator.comparingLong(TarArchiveStructSparse::getOffset)).collect(Collectors.toList()); final int numberOfHeaders = orderedAndFiltered.size(); for (int i = 0; i < numberOfHeaders; i++) { final TarArchiveStructSparse str = orderedAndFiltered.get(i); if (i + 1 < numberOfHeaders && str.getOffset() + str.getNumbytes() > orderedAndFiltered.get(i + 1).getOffset()) { throw new IOException("Corrupted TAR archive. Sparse blocks for " + getName() + " overlap each other."); } if (str.getOffset() + str.getNumbytes() < 0) { // integer overflow? throw new IOException("Unreadable TAR archive. Offset and numbytes for sparse block in " + getName() + " too large."); } } if (!orderedAndFiltered.isEmpty()) { final TarArchiveStructSparse last = orderedAndFiltered.get(numberOfHeaders - 1); if (last.getOffset() + last.getNumbytes() > getRealSize()) { throw new IOException("Corrupted TAR archive. Sparse block extends beyond real size of the entry"); } } return orderedAndFiltered; } /** * Gets this entry's file. *

* This method is only useful for entries created from a {@code * File} or {@code Path} but not for entries read from an archive. *

* * @return this entry's file or null if the entry was not created from a file. * @since 1.21 */ public Path getPath() { return file; } /** * Gets this entry's real file size in case of a sparse file. *

* This is the size a file would take on disk if the entry was expanded. *

*

* If the file is not a sparse file, return size instead of realSize. *

* * @return This entry's real file size, if the file is not a sparse file, return size instead of realSize. */ public long getRealSize() { if (!isSparse()) { return getSize(); } return realSize; } /** * Gets this entry's file size. *

* This is the size the entry's data uses inside the archive. Usually this is the same as {@link #getRealSize}, but it doesn't take the "holes" into account * when the entry represents a sparse file. *

* * @return This entry's file size. */ @Override public long getSize() { return size; } /** * Gets this entry's sparse headers * * @return This entry's sparse headers * @since 1.20 */ public List getSparseHeaders() { return sparseHeaders; } /** * Gets this entry's status change time. * * @since 1.22 * @return This entry's status change time. */ public FileTime getStatusChangeTime() { return cTime; } /** * Gets 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); } /** * Gets this entry's user name. * * @return This entry's user name. */ public String getUserName() { return userName; } /** * Hash codes are based on entry names. * * @return the entry hash code */ @Override public int hashCode() { return getName().hashCode(); } /** * Tests whether this is a block device entry. * * @since 1.2 * @return whether this is a block device */ public boolean isBlockDevice() { return linkFlag == LF_BLK; } /** * Tests whether this is a character device entry. * * @since 1.2 * @return whether this is a character device */ public boolean isCharacterDevice() { return linkFlag == LF_CHR; } /** * Tests whether this entry's checksum status. * * @return if the header checksum is reasonably correct * @see TarUtils#verifyCheckSum(byte[]) * @since 1.5 */ public boolean isCheckSumOK() { return checkSumOK; } /** * Tests whether the given entry is a descendant of this entry. Descendancy is determined by the name of the descendant starting with this entry's name. * * @param desc Entry to be checked as a descendent of this. * @return True if entry is a descendant of this. */ public boolean isDescendent(final TarArchiveEntry desc) { return desc.getName().startsWith(getName()); } /** * Tests whether or not this entry represents a directory. * * @return True if this entry is a directory. */ @Override public boolean isDirectory() { if (file != null) { return Files.isDirectory(file, linkOptions); } if (linkFlag == LF_DIR) { return true; } return !isPaxHeader() && !isGlobalPaxHeader() && getName().endsWith("/"); } /** * Tests whether in case of an oldgnu sparse file if an extension sparse header follows. * * @return true if an extension oldgnu sparse header follows. */ public boolean isExtended() { return isExtended; } /** * Tests whether this is a FIFO (pipe) entry. * * @since 1.2 * @return whether this is a FIFO entry */ public boolean isFIFO() { return linkFlag == LF_FIFO; } /** * Tests whether this is a "normal file" * * @since 1.2 * @return whether this is a "normal file" */ public boolean isFile() { if (file != null) { return Files.isRegularFile(file, linkOptions); } if (linkFlag == LF_OLDNORM || linkFlag == LF_NORMAL) { return true; } return linkFlag != LF_DIR && !getName().endsWith("/"); } /** * Tests whether this is a Pax header. * * @return {@code true} if this is a Pax header. * * @since 1.1 */ public boolean isGlobalPaxHeader() { return linkFlag == LF_PAX_GLOBAL_EXTENDED_HEADER; } /** * Tests whether 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; } /** * Tests whether 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; } /** * Tests whether this entry is a GNU sparse block. * * @return true if this is a sparse extension provided by GNU tar */ public boolean isGNUSparse() { return isOldGNUSparse() || isPaxGNUSparse(); } private boolean isInvalidPrefix(final byte[] header) { // prefix[130] is guaranteed to be '\0' with XSTAR/XUSTAR if (header[XSTAR_PREFIX_OFFSET + 130] != 0) { // except when typeflag is 'M' if (header[LF_OFFSET] != LF_MULTIVOLUME) { return true; } // We come only here if we try to read in a GNU/xstar/xustar multivolume archive starting past volume #0 // As of 1.22, commons-compress does not support multivolume tar archives. // If/when it does, this should work as intended. if ((header[XSTAR_MULTIVOLUME_OFFSET] & 0x80) == 0 && header[XSTAR_MULTIVOLUME_OFFSET + 11] != ' ') { return true; } } return false; } private boolean isInvalidXtarTime(final byte[] buffer, final int offset, final int length) { // If atime[0]...atime[10] or ctime[0]...ctime[10] is not a POSIX octal number it cannot be 'xstar'. if ((buffer[offset] & 0x80) == 0) { final int lastIndex = length - 1; for (int i = 0; i < lastIndex; i++) { final byte b = buffer[offset + i]; if (b < '0' || b > '7') { return true; } } // Check for both POSIX compliant end of number characters if not using base 256 final byte b = buffer[offset + lastIndex]; if (b != ' ' && b != 0) { return true; } } return false; } /** * Tests whether this is a link entry. * * @since 1.2 * @return whether this is a link entry */ public boolean isLink() { return linkFlag == LF_LINK; } /** * Tests whether this entry is a GNU or star sparse block using the oldgnu format. * * @return true if this is a sparse extension provided by GNU tar or star * @since 1.11 */ public boolean isOldGNUSparse() { return linkFlag == LF_GNUTYPE_SPARSE; } /** * Tests whether this entry is a sparse file with 1.X PAX Format or not * * @return True if this entry is a sparse file with 1.X PAX Format * @since 1.20 */ public boolean isPaxGNU1XSparse() { return paxGNU1XSparse; } /** * Tests whether this entry is a GNU sparse block using one of the PAX formats. * * @return true if this is a sparse extension provided by GNU tar * @since 1.11 */ public boolean isPaxGNUSparse() { return paxGNUSparse; } /** * Tests whether this is a Pax header. * * @return {@code true} if this is a Pax header. * * @since 1.1 */ public boolean isPaxHeader() { return linkFlag == LF_PAX_EXTENDED_HEADER_LC || linkFlag == LF_PAX_EXTENDED_HEADER_UC; } /** * Tests whether this is a sparse entry. * * @return whether this is a sparse entry * @since 1.11 */ public boolean isSparse() { return isGNUSparse() || isStarSparse(); } /** * Tests whether this entry is a star sparse block using PAX headers. * * @return true if this is a sparse extension provided by star * @since 1.11 */ public boolean isStarSparse() { return starSparse; } /** * {@inheritDoc} * * @since 1.21 */ @Override public boolean isStreamContiguous() { return true; } /** * Tests whether this is a symbolic link entry. * * @since 1.2 * @return whether this is a symbolic link */ public boolean isSymbolicLink() { return linkFlag == LF_SYMLINK; } /** * Tests whether the given header is in XSTAR / XUSTAR format. * * Use the same logic found in star version 1.6 in {@code header.c}, function {@code isxmagic(TCB *ptb)}. */ private boolean isXstar(final Map globalPaxHeaders, final byte[] header) { // Check if this is XSTAR if (ArchiveUtils.matchAsciiBuffer(MAGIC_XSTAR, header, XSTAR_MAGIC_OFFSET, XSTAR_MAGIC_LEN)) { return true; } /* * If SCHILY.archtype is present in the global PAX header, we can use it to identify the type of archive. * * Possible values for XSTAR: - xustar: 'xstar' format without "tar" signature at header offset 508. - exustar: 'xustar' format variant that always * includes x-headers and g-headers. */ final String archType = globalPaxHeaders.get("SCHILY.archtype"); if (archType != null) { return "xustar".equals(archType) || "exustar".equals(archType); } // Check if this is XUSTAR if (isInvalidPrefix(header)) { return false; } if (isInvalidXtarTime(header, XSTAR_ATIME_OFFSET, ATIMELEN_XSTAR)) { return false; } if (isInvalidXtarTime(header, XSTAR_CTIME_OFFSET, CTIMELEN_XSTAR)) { return false; } return true; } private long parseOctalOrBinary(final byte[] header, final int offset, final int length, final boolean lenient) { if (lenient) { try { return TarUtils.parseOctalOrBinary(header, offset, length); } catch (final IllegalArgumentException ex) { // NOSONAR return UNKNOWN; } } return TarUtils.parseOctalOrBinary(header, offset, length); } /** * Parses 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(final byte[] header) { try { parseTarHeader(header, TarUtils.DEFAULT_ENCODING); } catch (final IOException ex) { // NOSONAR try { parseTarHeader(header, TarUtils.DEFAULT_ENCODING, true, false); } catch (final IOException ex2) { // not really possible throw new UncheckedIOException(ex2); // NOSONAR } } } /** * 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 * @since 1.4 * @throws IllegalArgumentException if any of the numeric fields have an invalid format * @throws IOException on error */ public void parseTarHeader(final byte[] header, final ZipEncoding encoding) throws IOException { parseTarHeader(header, encoding, false, false); } private void parseTarHeader(final byte[] header, final ZipEncoding encoding, final boolean oldStyle, final boolean lenient) throws IOException { parseTarHeader(Collections.emptyMap(), header, encoding, oldStyle, lenient); } private void parseTarHeader(final Map globalPaxHeaders, final byte[] header, final ZipEncoding encoding, final boolean oldStyle, final boolean lenient) throws IOException { try { parseTarHeaderUnwrapped(globalPaxHeaders, header, encoding, oldStyle, lenient); } catch (final IllegalArgumentException ex) { throw new IOException("Corrupted TAR archive.", ex); } } private void parseTarHeaderUnwrapped(final Map globalPaxHeaders, final byte[] header, final ZipEncoding encoding, final boolean oldStyle, final boolean lenient) throws IOException { int offset = 0; name = oldStyle ? TarUtils.parseName(header, offset, NAMELEN) : TarUtils.parseName(header, offset, NAMELEN, encoding); offset += NAMELEN; mode = (int) parseOctalOrBinary(header, offset, MODELEN, lenient); offset += MODELEN; userId = (int) parseOctalOrBinary(header, offset, UIDLEN, lenient); offset += UIDLEN; groupId = (int) parseOctalOrBinary(header, offset, GIDLEN, lenient); offset += GIDLEN; size = TarUtils.parseOctalOrBinary(header, offset, SIZELEN); if (size < 0) { throw new IOException("broken archive, entry with negative size"); } offset += SIZELEN; mTime = TimeUtils.unixTimeToFileTime(parseOctalOrBinary(header, offset, MODTIMELEN, lenient)); offset += MODTIMELEN; checkSumOK = TarUtils.verifyCheckSum(header); 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, MAGICLEN); offset += 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; if (linkFlag == LF_CHR || linkFlag == LF_BLK) { devMajor = (int) parseOctalOrBinary(header, offset, DEVLEN, lenient); offset += DEVLEN; devMinor = (int) parseOctalOrBinary(header, offset, DEVLEN, lenient); offset += DEVLEN; } else { offset += 2 * DEVLEN; } final int type = evaluateType(globalPaxHeaders, header); switch (type) { case FORMAT_OLDGNU: { aTime = fileTimeFromOptionalSeconds(parseOctalOrBinary(header, offset, ATIMELEN_GNU, lenient)); offset += ATIMELEN_GNU; cTime = fileTimeFromOptionalSeconds(parseOctalOrBinary(header, offset, CTIMELEN_GNU, lenient)); offset += CTIMELEN_GNU; offset += OFFSETLEN_GNU; offset += LONGNAMESLEN_GNU; offset += PAD2LEN_GNU; sparseHeaders = new ArrayList<>(TarUtils.readSparseStructs(header, offset, SPARSE_HEADERS_IN_OLDGNU_HEADER)); offset += SPARSELEN_GNU; isExtended = TarUtils.parseBoolean(header, offset); offset += ISEXTENDEDLEN_GNU; realSize = TarUtils.parseOctal(header, offset, REALSIZELEN_GNU); offset += REALSIZELEN_GNU; // NOSONAR - assignment as documentation break; } case FORMAT_XSTAR: { final String xstarPrefix = oldStyle ? TarUtils.parseName(header, offset, PREFIXLEN_XSTAR) : TarUtils.parseName(header, offset, PREFIXLEN_XSTAR, encoding); offset += PREFIXLEN_XSTAR; if (!xstarPrefix.isEmpty()) { name = xstarPrefix + "/" + name; } aTime = fileTimeFromOptionalSeconds(parseOctalOrBinary(header, offset, ATIMELEN_XSTAR, lenient)); offset += ATIMELEN_XSTAR; cTime = fileTimeFromOptionalSeconds(parseOctalOrBinary(header, offset, CTIMELEN_XSTAR, lenient)); offset += CTIMELEN_XSTAR; // NOSONAR - assignment as documentation break; } case FORMAT_POSIX: default: { final String prefix = oldStyle ? TarUtils.parseName(header, offset, PREFIXLEN) : TarUtils.parseName(header, offset, PREFIXLEN, encoding); offset += PREFIXLEN; // NOSONAR - assignment as documentation // SunOS tar -E does not add / to directory names, so fix // up to be consistent if (isDirectory() && !name.endsWith("/")) { name += "/"; } if (!prefix.isEmpty()) { name = prefix + "/" + name; } } } } /** * Processes one pax header, using the entries extraPaxHeaders map as source for extra headers used when handling entries for sparse files. * * @param key * @param val * @since 1.15 */ private void processPaxHeader(final String key, final String val) throws IOException { processPaxHeader(key, val, extraPaxHeaders); } /** * Processes one pax header, using the supplied map as source for extra headers to be used when handling entries for sparse files * * @param key the header name. * @param val the header value. * @param headers map of headers used for dealing with sparse file. * @throws NumberFormatException if encountered errors when parsing the numbers * @since 1.15 */ private void processPaxHeader(final String key, final String val, final Map headers) throws IOException { /* * The following headers are defined for Pax. charset: cannot use these without changing TarArchiveEntry fields mtime atime ctime * LIBARCHIVE.creationtime comment gid, gname linkpath size uid,uname SCHILY.devminor, SCHILY.devmajor: don't have setters/getters for those * * GNU sparse files use additional members, we use GNU.sparse.size to detect the 0.0 and 0.1 versions and GNU.sparse.realsize for 1.0. * * star files use additional members of which we use SCHILY.filetype in order to detect star sparse files. * * If called from addExtraPaxHeader, these additional headers must be already present . */ switch (key) { case "path": setName(val); break; case "linkpath": setLinkName(val); break; case "gid": setGroupId(ParsingUtils.parseLongValue(val)); break; case "gname": setGroupName(val); break; case "uid": setUserId(ParsingUtils.parseLongValue(val)); break; case "uname": setUserName(val); break; case "size": final long size = ParsingUtils.parseLongValue(val); if (size < 0) { throw new IOException("Corrupted TAR archive. Entry size is negative"); } setSize(size); break; case "mtime": setLastModifiedTime(FileTime.from(parseInstantFromDecimalSeconds(val))); break; case "atime": setLastAccessTime(FileTime.from(parseInstantFromDecimalSeconds(val))); break; case "ctime": setStatusChangeTime(FileTime.from(parseInstantFromDecimalSeconds(val))); break; case "LIBARCHIVE.creationtime": setCreationTime(FileTime.from(parseInstantFromDecimalSeconds(val))); break; case "SCHILY.devminor": final int devMinor = ParsingUtils.parseIntValue(val); if (devMinor < 0) { throw new IOException("Corrupted TAR archive. Dev-Minor is negative"); } setDevMinor(devMinor); break; case "SCHILY.devmajor": final int devMajor = ParsingUtils.parseIntValue(val); if (devMajor < 0) { throw new IOException("Corrupted TAR archive. Dev-Major is negative"); } setDevMajor(devMajor); break; case TarGnuSparseKeys.SIZE: fillGNUSparse0xData(headers); break; case TarGnuSparseKeys.REALSIZE: fillGNUSparse1xData(headers); break; case "SCHILY.filetype": if ("sparse".equals(val)) { fillStarSparseData(headers); } break; default: extraPaxHeaders.put(key, val); } } private void readFileMode(final Path file, final String normalizedName, final LinkOption... options) throws IOException { if (Files.isDirectory(file, options)) { this.mode = DEFAULT_DIR_MODE; this.linkFlag = LF_DIR; final int nameLength = normalizedName.length(); if (nameLength == 0 || normalizedName.charAt(nameLength - 1) != '/') { this.name = normalizedName + "/"; } else { this.name = normalizedName; } } else { this.mode = DEFAULT_FILE_MODE; this.linkFlag = LF_NORMAL; this.name = normalizedName; this.size = Files.size(file); } } private void readOsSpecificProperties(final Path file, final LinkOption... options) throws IOException { final Set availableAttributeViews = file.getFileSystem().supportedFileAttributeViews(); if (availableAttributeViews.contains("posix")) { final PosixFileAttributes posixFileAttributes = Files.readAttributes(file, PosixFileAttributes.class, options); setLastModifiedTime(posixFileAttributes.lastModifiedTime()); setCreationTime(posixFileAttributes.creationTime()); setLastAccessTime(posixFileAttributes.lastAccessTime()); this.userName = posixFileAttributes.owner().getName(); this.groupName = posixFileAttributes.group().getName(); if (availableAttributeViews.contains("unix")) { this.userId = ((Number) Files.getAttribute(file, "unix:uid", options)).longValue(); this.groupId = ((Number) Files.getAttribute(file, "unix:gid", options)).longValue(); try { setStatusChangeTime((FileTime) Files.getAttribute(file, "unix:ctime", options)); } catch (final IllegalArgumentException ex) { // NOSONAR // ctime is not supported } } } else { if (availableAttributeViews.contains("dos")) { final DosFileAttributes dosFileAttributes = Files.readAttributes(file, DosFileAttributes.class, options); setLastModifiedTime(dosFileAttributes.lastModifiedTime()); setCreationTime(dosFileAttributes.creationTime()); setLastAccessTime(dosFileAttributes.lastAccessTime()); } else { final BasicFileAttributes basicFileAttributes = Files.readAttributes(file, BasicFileAttributes.class, options); setLastModifiedTime(basicFileAttributes.lastModifiedTime()); setCreationTime(basicFileAttributes.creationTime()); setLastAccessTime(basicFileAttributes.lastAccessTime()); } this.userName = Files.getOwner(file, options).getName(); } } /** * Sets this entry's creation time. * * @param time This entry's new creation time. * @since 1.22 */ public void setCreationTime(final FileTime time) { birthTime = time; } /** * Sets the offset of the data for the tar entry. * * @param dataOffset the position of the data in the tar. * @since 1.21 */ public void setDataOffset(final long dataOffset) { if (dataOffset < 0) { throw new IllegalArgumentException("The offset can not be smaller than 0"); } this.dataOffset = dataOffset; } /** * Sets this entry's major device number. * * @param devNo This entry's major device number. * @throws IllegalArgumentException if the devNo is < 0. * @since 1.4 */ public void setDevMajor(final int devNo) { if (devNo < 0) { throw new IllegalArgumentException("Major device number is out of " + "range: " + devNo); } this.devMajor = devNo; } /** * Sets this entry's minor device number. * * @param devNo This entry's minor device number. * @throws IllegalArgumentException if the devNo is < 0. * @since 1.4 */ public void setDevMinor(final int devNo) { if (devNo < 0) { throw new IllegalArgumentException("Minor device number is out of " + "range: " + devNo); } this.devMinor = devNo; } /** * Sets this entry's group id. * * @param groupId This entry's new group id. */ public void setGroupId(final int groupId) { setGroupId((long) groupId); } /** * Sets this entry's group id. * * @since 1.10 * @param groupId This entry's new group id. */ public void setGroupId(final long groupId) { this.groupId = groupId; } /** * Sets this entry's group name. * * @param groupName This entry's new group name. */ public void setGroupName(final 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(final int userId, final int groupId) { setUserId(userId); setGroupId(groupId); } /** * Sets this entry's last access time. * * @param time This entry's new last access time. * @since 1.22 */ public void setLastAccessTime(final FileTime time) { aTime = time; } /** * Sets this entry's modification time. * * @param time This entry's new modification time. * @since 1.22 */ public void setLastModifiedTime(final FileTime time) { mTime = Objects.requireNonNull(time, "Time must not be null"); } /** * Sets this entry's link name. * * @param link the link name to use. * * @since 1.1 */ public void setLinkName(final String link) { this.linkName = link; } /** * Sets the mode for this entry * * @param mode the mode for this entry */ public void setMode(final int mode) { this.mode = mode; } /** * Sets this entry's modification time. * * @param time This entry's new modification time. * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final Date time) { setLastModifiedTime(FileTimes.toFileTime(time)); } /** * Sets this entry's modification time. * * @param time This entry's new modification time. * @since 1.21 * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final FileTime time) { setLastModifiedTime(time); } /** * Sets this entry's modification time. The parameter passed to this method is in "Java time". * * @param time This entry's new modification time. * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final long time) { setLastModifiedTime(FileTime.fromMillis(time)); } /** * Sets this entry's name. * * @param name This entry's new name. */ public void setName(final String name) { this.name = normalizeFileName(name, this.preserveAbsolutePath); } /** * Convenience method to set this entry's group and user names. * * @param userName This entry's new user name. * @param groupName This entry's new group name. */ public void setNames(final String userName, final String groupName) { setUserName(userName); setGroupName(groupName); } /** * Sets this entry's file size. * * @param size This entry's new file size. * @throws IllegalArgumentException if the size is < 0. */ public void setSize(final long size) { if (size < 0) { throw new IllegalArgumentException("Size is out of range: " + size); } this.size = size; } /** * Sets this entry's sparse headers * * @param sparseHeaders The new sparse headers * @since 1.20 */ public void setSparseHeaders(final List sparseHeaders) { this.sparseHeaders = sparseHeaders; } /** * Sets this entry's status change time. * * @param time This entry's new status change time. * @since 1.22 */ public void setStatusChangeTime(final FileTime time) { cTime = time; } /** * Sets this entry's user id. * * @param userId This entry's new user id. */ public void setUserId(final int userId) { setUserId((long) userId); } /** * Sets this entry's user id. * * @param userId This entry's new user id. * @since 1.10 */ public void setUserId(final long userId) { this.userId = userId; } /** * Sets this entry's user name. * * @param userName This entry's new user name. */ public void setUserName(final String userName) { this.userName = userName; } /** * Update the entry using a map of pax headers. * * @param headers * @since 1.15 */ void updateEntryFromPaxHeaders(final Map headers) throws IOException { for (final Map.Entry ent : headers.entrySet()) { processPaxHeader(ent.getKey(), ent.getValue(), headers); } } /** * Writes an entry's header information to a header buffer. *

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

* * @param outbuf The tar entry header buffer to fill in. */ public void writeEntryHeader(final byte[] outbuf) { try { writeEntryHeader(outbuf, TarUtils.DEFAULT_ENCODING, false); } catch (final IOException ex) { // NOSONAR try { writeEntryHeader(outbuf, TarUtils.FALLBACK_ENCODING, false); } catch (final IOException ex2) { // impossible throw new UncheckedIOException(ex2); // NOSONAR } } } /** * Writes 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 * @since 1.4 * @throws IOException on error */ public void writeEntryHeader(final byte[] outbuf, final ZipEncoding encoding, final 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(TimeUtils.toUnixTime(mTime), outbuf, offset, MODTIMELEN, starMode); final int csOffset = offset; offset = fill((byte) ' ', offset, outbuf, CHKSUMLEN); outbuf[offset++] = linkFlag; offset = TarUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN, encoding); offset = TarUtils.formatNameBytes(magic, outbuf, offset, 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); if (starMode) { // skip prefix offset = fill(0, offset, outbuf, PREFIXLEN_XSTAR); offset = writeEntryHeaderOptionalTimeField(aTime, offset, outbuf, ATIMELEN_XSTAR); offset = writeEntryHeaderOptionalTimeField(cTime, offset, outbuf, CTIMELEN_XSTAR); // 8-byte fill offset = fill(0, offset, outbuf, 8); // Do not write MAGIC_XSTAR because it causes issues with some TAR tools // This makes it effectively XUSTAR, which guarantees compatibility with USTAR offset = fill(0, offset, outbuf, XSTAR_MAGIC_LEN); } offset = fill(0, offset, outbuf, outbuf.length - offset); // NOSONAR - assignment as documentation final long chk = TarUtils.computeCheckSum(outbuf); TarUtils.formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); } private int writeEntryHeaderField(final long value, final byte[] outbuf, final int offset, final int length, final 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); } private int writeEntryHeaderOptionalTimeField(final FileTime time, int offset, final byte[] outbuf, final int fieldLength) { if (time != null) { offset = writeEntryHeaderField(TimeUtils.toUnixTime(time), outbuf, offset, fieldLength, true); } else { offset = fill(0, offset, outbuf, fieldLength); } return offset; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy