diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java index b722568f103..6233079e3b3 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; +import java.math.BigDecimal; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.LinkOption; @@ -28,6 +29,7 @@ import java.nio.file.attribute.DosFileAttributes; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFileAttributes; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -37,6 +39,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -158,6 +161,26 @@ * *

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"
+ * };
+ * 
+ *

+ * * @NotThreadSafe */ @@ -189,8 +212,35 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO /** The entry's size. */ private long size; - /** The entry's modification time. */ - private long modTime; + /** + * 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; @@ -314,7 +364,7 @@ public TarArchiveEntry(String name, final boolean preserveAbsolutePath) { this.name = name; this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; this.linkFlag = isDir ? LF_DIR : LF_NORMAL; - this.modTime = System.currentTimeMillis() / MILLIS_PER_SECOND; + this.mTime = FileTime.from(Instant.now()); this.userName = ""; } @@ -437,7 +487,7 @@ public TarArchiveEntry(final File file, final String fileName) { } 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.modTime = file.lastModified() / MILLIS_PER_SECOND; + this.mTime = FileTime.fromMillis(file.lastModified()); } preserveAbsolutePath = false; } @@ -474,20 +524,31 @@ private void readOsSpecificProperties(final Path file, final LinkOption... optio final Set availableAttributeViews = file.getFileSystem().supportedFileAttributeViews(); if (availableAttributeViews.contains("posix")) { final PosixFileAttributes posixFileAttributes = Files.readAttributes(file, PosixFileAttributes.class, options); - setModTime(posixFileAttributes.lastModifiedTime()); + 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); - setModTime(dosFileAttributes.lastModifiedTime()); + setLastModifiedTime(dosFileAttributes.lastModifiedTime()); + setCreationTime(dosFileAttributes.creationTime()); + setLastAccessTime(dosFileAttributes.lastAccessTime()); this.userName = Files.getOwner(file, options).getName(); } else { final BasicFileAttributes basicFileAttributes = Files.readAttributes(file, BasicFileAttributes.class, options); - setModTime(basicFileAttributes.lastModifiedTime()); + setLastModifiedTime(basicFileAttributes.lastModifiedTime()); + setCreationTime(basicFileAttributes.creationTime()); + setLastAccessTime(basicFileAttributes.lastAccessTime()); this.userName = Files.getOwner(file, options).getName(); } } @@ -552,8 +613,25 @@ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding) */ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding, final boolean lenient) throws IOException { + this(Collections.emptyMap(), headerBuf, encoding, lenient); + } + + /** + * Construct 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(headerBuf, encoding, false, lenient); + parseTarHeader(globalPaxHeaders, headerBuf, encoding, false, lenient); } /** @@ -573,6 +651,24 @@ public TarArchiveEntry(final byte[] headerBuf, final ZipEncoding encoding, final setDataOffset(dataOffset); } + /** + * Construct 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); + } + /** * Determine if the two entries are equal. Equality is determined * by the header names being equal. @@ -816,18 +912,20 @@ public void setNames(final String userName, final String groupName) { * 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) { - modTime = time / MILLIS_PER_SECOND; + setLastModifiedTime(FileTime.fromMillis(time)); } /** * Set this entry's modification time. * * @param time This entry's new modification time. + * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final Date time) { - modTime = time.getTime() / MILLIS_PER_SECOND; + setLastModifiedTime(FileTime.fromMillis(time.getTime())); } /** @@ -835,25 +933,115 @@ public void setModTime(final Date time) { * * @param time This entry's new modification time. * @since 1.21 + * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final FileTime time) { - modTime = time.to(TimeUnit.SECONDS); + setLastModifiedTime(time); } /** * Get 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() { - return new Date(modTime * MILLIS_PER_SECOND); + return new Date(mTime.toMillis()); } + /** + * Get 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(); } + /** + * Get this entry's modification time. + * + * @since 1.22 + * @return This entry's modification time. + */ + public FileTime getLastModifiedTime() { + return mTime; + } + + /** + * Set 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"); + } + + /** + * Get this entry's status change time. + * + * @since 1.22 + * @return This entry's status change time. + */ + public FileTime getStatusChangeTime() { + return cTime; + } + + /** + * Set 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; + } + + /** + * Get this entry's last access time. + * + * @since 1.22 + * @return This entry's last access time. + */ + public FileTime getLastAccessTime() { + return aTime; + } + + /** + * Set 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; + } + + /** + * Get this entry's creation time. + * + * @since 1.22 + * @return This entry's computed creation time. + */ + public FileTime getCreationTime() { + return birthTime; + } + + /** + * Set this entry's creation time. + * + * @param time This entry's new creation time. + * @since 1.22 + */ + public void setCreationTime(final FileTime time) { + birthTime = time; + } + /** * Get this entry's checksum status. * @@ -1360,8 +1548,11 @@ private void processPaxHeader(final String key, final String val, final Map globalPaxHeaders, final byte[] header, + final ZipEncoding encoding, final boolean oldStyle, final boolean lenient) + throws IOException { try { - parseTarHeaderUnwrapped(header, encoding, oldStyle, lenient); + parseTarHeaderUnwrapped(globalPaxHeaders, header, encoding, oldStyle, lenient); } catch (IllegalArgumentException ex) { throw new IOException("Corrupted TAR archive.", ex); } } - private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding encoding, - final boolean oldStyle, final boolean lenient) + private void parseTarHeaderUnwrapped(final Map globalPaxHeaders, final byte[] header, + final ZipEncoding encoding, final boolean oldStyle, final boolean lenient) throws IOException { int offset = 0; @@ -1617,7 +1856,7 @@ private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding enco throw new IOException("broken archive, entry with negative size"); } offset += SIZELEN; - modTime = parseOctalOrBinary(header, offset, MODTIMELEN, lenient); + mTime = FileTime.from(parseOctalOrBinary(header, offset, MODTIMELEN, lenient), TimeUnit.SECONDS); offset += MODTIMELEN; checkSumOK = TarUtils.verifyCheckSum(header); offset += CHKSUMLEN; @@ -1644,10 +1883,12 @@ private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding enco offset += 2 * DEVLEN; } - final int type = evaluateType(header); + 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; @@ -1665,9 +1906,14 @@ private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding enco 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: @@ -1675,6 +1921,7 @@ private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding enco 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("/")){ @@ -1687,6 +1934,13 @@ private void parseTarHeaderUnwrapped(final byte[] header, final ZipEncoding enco } } + private static FileTime fileTimeFromOptionalSeconds(long seconds) { + if (seconds <= 0) { + return null; + } + return FileTime.from(seconds, TimeUnit.SECONDS); + } + private long parseOctalOrBinary(final byte[] header, final int offset, final int length, final boolean lenient) { if (lenient) { try { @@ -1745,13 +1999,12 @@ private static String normalizeFileName(String fileName, final boolean preserveA * @param header The tar entry header buffer to evaluate the format for. * @return format type */ - private int evaluateType(final byte[] header) { + 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 (ArchiveUtils.matchAsciiBuffer(MAGIC_XSTAR, header, XSTAR_MAGIC_OFFSET, - XSTAR_MAGIC_LEN)) { + if (isXstar(globalPaxHeaders, header)) { return FORMAT_XSTAR; } return FORMAT_POSIX; @@ -1759,6 +2012,81 @@ private int evaluateType(final byte[] header) { return 0; } + /** + * Check for 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 boolean isInvalidPrefix(final byte[] header) { + // prefix[130] is 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) { + // 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; + } + } else { + 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; + } + void fillGNUSparse0xData(final Map headers) { paxGNUSparse = true; realSize = Integer.parseInt(headers.get("GNU.sparse.size")); diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java index 0fbec404601..b622af6bec5 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java @@ -376,7 +376,7 @@ public TarArchiveEntry getNextTarEntry() throws IOException { } try { - currEntry = new TarArchiveEntry(headerBuf, zipEncoding, lenient); + currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf, zipEncoding, lenient); } catch (final IllegalArgumentException e) { throw new IOException("Error detected parsing the header", e); } diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java index 0f49490e95c..70b0c57dd9c 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java @@ -22,13 +22,17 @@ import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveOutputStream; @@ -230,8 +234,8 @@ public TarArchiveOutputStream(final OutputStream os, final int blockSize, } /** - * 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 >= + * Set the long file mode. This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1), LONGFILE_GNU(2) or + * LONGFILE_POSIX(3). This specifies the treatment of long file names (names >= * TarConstants.NAMELEN). Default is LONGFILE_ERROR. * * @param longFileMode the mode to use @@ -241,9 +245,9 @@ public void setLongFileMode(final int 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. + * Set the big number mode. This can be BIGNUMBER_ERROR(0), BIGNUMBER_STAR(1) or + * BIGNUMBER_POSIX(2). This specifies the treatment of big files (sizes > + * TarConstants.MAXSIZE) and other numeric values too big to fit into a traditional tar header. * Default is BIGNUMBER_ERROR. * * @param bigNumberMode the mode to use @@ -367,7 +371,6 @@ public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException final String entryName = entry.getName(); final boolean paxHeaderContainsPath = handleLongName(entry, entryName, paxHeaders, "path", TarConstants.LF_GNUTYPE_LONGNAME, "file name"); - final String linkName = entry.getLinkName(); final boolean paxHeaderContainsLinkPath = linkName != null && !linkName.isEmpty() && handleLongName(entry, linkName, paxHeaders, "linkpath", @@ -602,12 +605,20 @@ private void addPaxHeadersForBigNumbers(final Map paxHeaders, TarConstants.MAXSIZE); addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(), TarConstants.MAXID); - addPaxHeaderForBigNumber(paxHeaders, "mtime", - entry.getModTime().getTime() / 1000, - TarConstants.MAXSIZE); + addFileTimePaxHeaderForBigNumber(paxHeaders, "mtime", + entry.getLastModifiedTime(), TarConstants.MAXSIZE); + addFileTimePaxHeader(paxHeaders, "atime", entry.getLastAccessTime()); + if (entry.getStatusChangeTime() != null) { + addFileTimePaxHeader(paxHeaders, "ctime", entry.getStatusChangeTime()); + } else { + // ctime is usually set from creation time on platforms where the real ctime is not available + addFileTimePaxHeader(paxHeaders, "ctime", entry.getCreationTime()); + } addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(), TarConstants.MAXID); - // star extensions by J\u00f6rg Schilling + // libarchive extensions + addFileTimePaxHeader(paxHeaders, "LIBARCHIVE.creationtime", entry.getCreationTime()); + // star extensions by Jörg Schilling addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor", entry.getDevMajor(), TarConstants.MAXID); addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor", @@ -624,11 +635,48 @@ private void addPaxHeaderForBigNumber(final Map paxHeaders, } } + private void addFileTimePaxHeaderForBigNumber(final Map paxHeaders, + final String header, final FileTime value, + final long maxValue) { + if (value != null) { + final Instant instant = value.toInstant(); + final long seconds = instant.getEpochSecond(); + final int nanos = instant.getNano(); + if (nanos == 0) { + addPaxHeaderForBigNumber(paxHeaders, header, seconds, maxValue); + } else { + addInstantPaxHeader(paxHeaders, header, seconds, nanos); + } + } + } + + private void addFileTimePaxHeader(final Map paxHeaders, + final String header, final FileTime value) { + if (value != null) { + final Instant instant = value.toInstant(); + final long seconds = instant.getEpochSecond(); + final int nanos = instant.getNano(); + if (nanos == 0) { + paxHeaders.put(header, String.valueOf(seconds)); + } else { + addInstantPaxHeader(paxHeaders, header, seconds, nanos); + } + } + } + + private void addInstantPaxHeader(final Map paxHeaders, + final String header, final long seconds, final int nanos) { + final BigDecimal bdSeconds = BigDecimal.valueOf(seconds); + final BigDecimal bdNanos = BigDecimal.valueOf(nanos).movePointLeft(9).setScale(7, RoundingMode.DOWN); + final BigDecimal timestamp = bdSeconds.add(bdNanos); + paxHeaders.put(header, timestamp.toPlainString()); + } + private void failForBigNumbers(final TarArchiveEntry entry) { failForBigNumber("entry size", entry.getSize(), TarConstants.MAXSIZE); failForBigNumberWithPosixMessage("group id", entry.getLongGroupId(), TarConstants.MAXID); failForBigNumber("last modification time", - entry.getModTime().getTime() / 1000, + entry.getLastModifiedTime().to(TimeUnit.SECONDS), TarConstants.MAXSIZE); failForBigNumber("user id", entry.getLongUserId(), TarConstants.MAXID); failForBigNumber("mode", entry.getMode(), TarConstants.MAXID); @@ -711,11 +759,10 @@ private boolean handleLongName(final TarArchiveEntry entry, final String name, } private void transferModTime(final TarArchiveEntry from, final TarArchiveEntry to) { - Date fromModTime = from.getModTime(); - final long fromModTimeSeconds = fromModTime.getTime() / 1000; + long fromModTimeSeconds = from.getLastModifiedTime().to(TimeUnit.SECONDS); if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) { - fromModTime = new Date(0); + fromModTimeSeconds = 0; } - to.setModTime(fromModTime); + to.setLastModifiedTime(FileTime.from(fromModTimeSeconds, TimeUnit.SECONDS)); } } diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java index ffd2aa58d6a..289046c844d 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java @@ -227,6 +227,14 @@ public interface TarConstants { */ byte LF_OLDNORM = 0; + /** + * Offset inside the header for the "link flag" field. + * + * @since 1.22 + * @see TarArchiveEntry + */ + int LF_OFFSET = 156; + /** * Normal file type. */ @@ -305,6 +313,13 @@ public interface TarConstants { */ byte LF_PAX_GLOBAL_EXTENDED_HEADER = (byte) 'g'; + /** + * Identifies the entry as a multi-volume past volume #0 + * + * @since 1.22 + */ + byte LF_MULTIVOLUME = (byte) 'M'; + /** * The magic tag representing a POSIX tar archive. */ @@ -347,6 +362,14 @@ public interface TarConstants { */ String MAGIC_XSTAR = "tar\0"; + /** + * Offset inside the header for the xtar multivolume data + * + * @since 1.22 + * @see TarArchiveEntry + */ + int XSTAR_MULTIVOLUME_OFFSET = 464; + /** * Offset inside the header for the xstar magic bytes. * @since 1.11 @@ -366,6 +389,22 @@ public interface TarConstants { */ int PREFIXLEN_XSTAR = 131; + /** + * Offset inside the header for the prefix field in xstar archives. + * + * @since 1.22 + * @see TarArchiveEntry + */ + int XSTAR_PREFIX_OFFSET = 345; + + /** + * Offset inside the header for the atime field in xstar archives. + * + * @since 1.22 + * @see TarArchiveEntry + */ + int XSTAR_ATIME_OFFSET = 476; + /** * The length of the access time field in a xstar header buffer. * @@ -373,6 +412,14 @@ public interface TarConstants { */ int ATIMELEN_XSTAR = 12; + /** + * Offset inside the header for the ctime field in xstar archives. + * + * @since 1.22 + * @see TarArchiveEntry + */ + int XSTAR_CTIME_OFFSET = 488; + /** * The length of the created time field in a xstar header buffer. * diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java index 5491c8b0dc3..ec9359fb740 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java @@ -254,7 +254,8 @@ private TarArchiveEntry getNextTarEntry() throws IOException { } try { - currEntry = new TarArchiveEntry(headerBuf.array(), zipEncoding, lenient, archive.position()); + final long position = archive.position(); + currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf.array(), zipEncoding, lenient, position); } catch (final IllegalArgumentException e) { throw new IOException("Error detected parsing the header", e); } diff --git a/src/test/java/org/apache/commons/compress/archivers/tar/FileTimesIT.java b/src/test/java/org/apache/commons/compress/archivers/tar/FileTimesIT.java new file mode 100644 index 00000000000..1ba3023bfcd --- /dev/null +++ b/src/test/java/org/apache/commons/compress/archivers/tar/FileTimesIT.java @@ -0,0 +1,522 @@ +/* + * 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 org.apache.commons.compress.AbstractTestCase; +import org.junit.jupiter.api.Test; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +import static org.junit.Assert.*; + +public class FileTimesIT extends AbstractTestCase { + + // Old BSD tar format + @Test + public void readTimeFromTarOldBsdTar() throws Exception { + final String file = "COMPRESS-612/test-times-oldbsdtar.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-17T01:52:25Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Old UNIX V7 tar format + @Test + public void readTimeFromTarV7() throws Exception { + final String file = "COMPRESS-612/test-times-v7.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Format used by GNU tar of versions prior to 1.12 + // Created using GNU tar + @Test + public void readTimeFromTarOldGnu() throws Exception { + final String file = "COMPRESS-612/test-times-oldgnu.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Format used by GNU tar of versions prior to 1.12 + // Created using GNU tar + @Test + public void readTimeFromTarOldGnuIncremental() throws Exception { + final String file = "COMPRESS-612/test-times-oldgnu-incremental.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T03:17:05Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T03:17:06Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T03:17:05Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // GNU tar format 1989 (violates POSIX) + // Created using s-tar 1.6, which somehow differs from GNU tar's. + @Test + public void readTimeFromTarGnuTar() throws Exception { + final String file = "COMPRESS-612/test-times-gnutar.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-17T01:52:25Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:52:25Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T01:52:25Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // GNU tar format 1989 (violates POSIX) + // Created using GNU tar + @Test + public void readTimeFromTarGnu() throws Exception { + final String file = "COMPRESS-612/test-times-gnu.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // GNU tar format 1989 (violates POSIX) + // Created using GNU tar + @Test + public void readTimeFromTarGnuIncremental() throws Exception { + final String file = "COMPRESS-612/test-times-gnu-incremental.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T03:17:05Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T03:17:10Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T03:17:10Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Standard POSIX.1-1988 tar format + @Test + public void readTimeFromTarUstar() throws Exception { + final String file = "COMPRESS-612/test-times-ustar.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Old star format from 1985 + @Test + public void readTimeFromTarStarFolder() throws Exception { + final String file = "COMPRESS-612/test-times-star-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20Z"), e.getLastModifiedTime()); + assertNull("atime", e.getLastAccessTime()); + assertNull("ctime", e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended standard tar (star 1994) + @Test + public void readTimeFromTarXstar() throws Exception { + final String file = "COMPRESS-612/test-times-xstar.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T04:11:22Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T04:12:48Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T04:12:47Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended standard tar (star 1994) + @Test + public void readTimeFromTarXstarIncremental() throws Exception { + final String file = "COMPRESS-612/test-times-xstar-incremental.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T04:03:29Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T04:03:29Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T04:03:29Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-14T04:11:22Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T04:11:23Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T04:11:22Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended standard tar (star 1994) + @Test + public void readTimeFromTarXstarFolder() throws Exception { + final String file = "COMPRESS-612/test-times-xstar-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:01:34Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:24:44Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // 'xstar' format without tar signature + @Test + public void readTimeFromTarXustar() throws Exception { + final String file = "COMPRESS-612/test-times-xustar.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // 'xstar' format without tar signature + @Test + public void readTimeFromTarXustarIncremental() throws Exception { + final String file = "COMPRESS-612/test-times-xustar-incremental.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test-times.txt", e.getName()); + assertEquals("mtime", toFileTime("2022-03-17T01:52:25.592262900Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:52:25.724278500Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T01:52:25.592262900Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // 'xstar' format without tar signature + @Test + public void readTimeFromTarXustarFolder() throws Exception { + final String file = "COMPRESS-612/test-times-xustar-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:01:19.581236400Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // 'xustar' format - always x-header + @Test + public void readTimeFromTarExustar() throws Exception { + final String file = "COMPRESS-612/test-times-exustar-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:47:00.367783300Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertGlobalHeaders(e); + e = tin.getNextTarEntry(); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertGlobalHeaders(e); + assertNull(tin.getNextTarEntry()); + } + } + + private void assertGlobalHeaders(final TarArchiveEntry e) { + assertEquals(5, e.getExtraPaxHeaders().size()); + assertEquals("SCHILY.archtype", "exustar", e.getExtraPaxHeader("SCHILY.archtype")); + assertEquals("SCHILY.volhdr.dumpdate", "1647478879.579980900", e.getExtraPaxHeader("SCHILY.volhdr.dumpdate")); + assertEquals("SCHILY.release", "star 1.6 (x86_64-unknown-linux-gnu) 2019/04/01", e.getExtraPaxHeader("SCHILY.release")); + assertEquals("SCHILY.volhdr.blocksize", "20", e.getExtraPaxHeader("SCHILY.volhdr.blocksize")); + assertEquals("SCHILY.volhdr.volno", "1", e.getExtraPaxHeader("SCHILY.volhdr.volno")); + } + + // Extended POSIX.1-2001 standard tar + // Created using GNU tar + @Test + public void readTimeFromTarPosix() throws Exception { + final String file = "COMPRESS-612/test-times-posix.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03.599853900Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:31:00.706927200Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:28:59.700505300Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended POSIX.1-2001 standard tar + // Created using s-tar 1.6, which somehow differs from GNU tar's. + @Test + public void readTimeFromTarPax() throws Exception { + final String file = "COMPRESS-612/test-times-pax-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:01:53.369146300Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended POSIX.1-2001 standard tar + x-header + // Created using s-tar 1.6 + @Test + public void readTimeFromTarEpax() throws Exception { + final String file = "COMPRESS-612/test-times-epax-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T01:02:11.910960100Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:24:44.147126600Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-17T00:38:20.536752000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-17T00:38:20.470751500Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended POSIX.1-2001 standard tar + // Created using GNU tar on Linux + @Test + public void readTimeFromTarPosixLinux() throws Exception { + final String file = "COMPRESS-612/test-times-posix-linux.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + final TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03.599853900Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:32:13.837251500Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:31:00.706927200Z"), e.getStatusChangeTime()); + assertNull("birthtime", e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + // Extended POSIX.1-2001 standard tar + // Created using BSD tar on Windows + @Test + public void readTimeFromTarPosixLibArchive() throws Exception { + final String file = "COMPRESS-612/test-times-bsd-folder.tar"; + try (final InputStream in = new BufferedInputStream(Files.newInputStream(getPath(file))); + final TarArchiveInputStream tin = new TarArchiveInputStream(in)) { + TarArchiveEntry e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/", e.getName()); + assertTrue(e.isDirectory()); + assertEquals("mtime", toFileTime("2022-03-16T10:19:43.382883700Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-16T10:21:01.251181000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-16T10:19:24.105111500Z"), e.getStatusChangeTime()); + assertEquals("birthtime", toFileTime("2022-03-16T10:19:24.105111500Z"), e.getCreationTime()); + e = tin.getNextTarEntry(); + assertNotNull(e); + assertTrue(e.getExtraPaxHeaders().isEmpty()); + assertEquals("name", "test/test-times.txt", e.getName()); + assertTrue(e.isFile()); + assertEquals("mtime", toFileTime("2022-03-16T10:21:00.249238500Z"), e.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-16T10:21:01.251181000Z"), e.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:25:03.599853900Z"), e.getStatusChangeTime()); + assertEquals("birthtime", toFileTime("2022-03-14T01:25:03.599853900Z"), e.getCreationTime()); + assertNull(tin.getNextTarEntry()); + } + } + + private FileTime toFileTime(final String text) { + return FileTime.from(Instant.parse(text)); + } +} diff --git a/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveEntryTest.java b/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveEntryTest.java index 89c2b0fd36a..8c1b474afe5 100644 --- a/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveEntryTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveEntryTest.java @@ -34,6 +34,8 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -306,6 +308,160 @@ public void getOrderedSparseHeadersRejectsStructsPointingBeyondOutputEntry() thr te.getOrderedSparseHeaders(); } + @Test + public void shouldParseTimePaxHeadersAndNotCountAsExtraPaxHeaders() { + final TarArchiveEntry entry = createEntryForTimeTests(); + assertEquals("extra header count", 0, entry.getExtraPaxHeaders().size()); + assertNull("size", entry.getExtraPaxHeader("size")); + assertNull("mtime", entry.getExtraPaxHeader("mtime")); + assertNull("atime", entry.getExtraPaxHeader("atime")); + assertNull("ctime", entry.getExtraPaxHeader("ctime")); + assertNull("birthtime", entry.getExtraPaxHeader("LIBARCHIVE.creationtime")); + assertEquals("size", entry.getSize(), 1); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03.599853900Z"), entry.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:31:00.706927200Z"), entry.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:28:59.700505300Z"), entry.getStatusChangeTime()); + assertEquals("birthtime", toFileTime("2022-03-14T01:29:00.723509000Z"), entry.getCreationTime()); + } + + @Test + public void shouldNotWriteTimePaxHeadersByDefault() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) { + final TarArchiveEntry entry = createEntryForTimeTests(); + tos.putArchiveEntry(entry); + tos.write('W'); + tos.closeArchiveEntry(); + } + try (final TarArchiveInputStream tis = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + final TarArchiveEntry entry = tis.getNextTarEntry(); + assertNotNull("couldn't get entry", entry); + + assertEquals("extra header count", 0, entry.getExtraPaxHeaders().size()); + assertNull("mtime", entry.getExtraPaxHeader("mtime")); + assertNull("atime", entry.getExtraPaxHeader("atime")); + assertNull("ctime", entry.getExtraPaxHeader("ctime")); + assertNull("birthtime", entry.getExtraPaxHeader("LIBARCHIVE.creationtime")); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), entry.getLastModifiedTime()); + assertNull("atime", entry.getLastAccessTime()); + assertNull("ctime", entry.getStatusChangeTime()); + assertNull("birthtime", entry.getCreationTime()); + + assertEquals('W', tis.read()); + assertTrue("should be at end of entry", tis.read() < 0); + + assertNull("should be at end of file", tis.getNextTarEntry()); + } + } + + @Test + public void shouldWriteTimesForStarMode() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) { + final TarArchiveEntry entry = createEntryForTimeTests(); + tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR); + tos.putArchiveEntry(entry); + tos.write('W'); + tos.closeArchiveEntry(); + } + try (final TarArchiveInputStream tis = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + final TarArchiveEntry entry = tis.getNextTarEntry(); + assertNotNull("couldn't get entry", entry); + + assertEquals("extra header count", 0, entry.getExtraPaxHeaders().size()); + assertNull("mtime", entry.getExtraPaxHeader("mtime")); + assertNull("atime", entry.getExtraPaxHeader("atime")); + assertNull("ctime", entry.getExtraPaxHeader("ctime")); + assertNull("birthtime", entry.getExtraPaxHeader("LIBARCHIVE.creationtime")); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03Z"), entry.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:31:00Z"), entry.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:28:59Z"), entry.getStatusChangeTime()); + assertNull("birthtime", entry.getCreationTime()); + + assertEquals('W', tis.read()); + assertTrue("should be at end of entry", tis.read() < 0); + + assertNull("should be at end of file", tis.getNextTarEntry()); + } + } + + @Test + public void shouldWriteTimesAsPaxHeadersForPosixMode() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) { + final TarArchiveEntry entry = createEntryForTimeTests(); + tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + tos.putArchiveEntry(entry); + tos.write('W'); + tos.closeArchiveEntry(); + } + try (final TarArchiveInputStream tis = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + final TarArchiveEntry entry = tis.getNextTarEntry(); + assertNotNull("couldn't get entry", entry); + + assertEquals("extra header count", 0, entry.getExtraPaxHeaders().size()); + assertNull("mtime", entry.getExtraPaxHeader("mtime")); + assertNull("atime", entry.getExtraPaxHeader("atime")); + assertNull("ctime", entry.getExtraPaxHeader("ctime")); + assertNull("birthtime", entry.getExtraPaxHeader("LIBARCHIVE.creationtime")); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03.599853900Z"), entry.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:31:00.706927200Z"), entry.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:28:59.700505300Z"), entry.getStatusChangeTime()); + assertEquals("birthtime", toFileTime("2022-03-14T01:29:00.723509000Z"), entry.getCreationTime()); + + assertEquals('W', tis.read()); + assertTrue("should be at end of entry", tis.read() < 0); + + assertNull("should be at end of file", tis.getNextTarEntry()); + } + } + + @Test + public void shouldWriteTimesAsPaxHeadersForPosixModeAndCreationTimeShouldBeUsedAsCtime() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) { + final TarArchiveEntry entry = createEntryForTimeTests(); + entry.setStatusChangeTime(null); + tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + tos.putArchiveEntry(entry); + tos.write('W'); + tos.closeArchiveEntry(); + } + try (final TarArchiveInputStream tis = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()))) { + final TarArchiveEntry entry = tis.getNextTarEntry(); + assertNotNull("couldn't get entry", entry); + + assertEquals("extra header count", 0, entry.getExtraPaxHeaders().size()); + assertNull("mtime", entry.getExtraPaxHeader("mtime")); + assertNull("atime", entry.getExtraPaxHeader("atime")); + assertNull("ctime", entry.getExtraPaxHeader("ctime")); + assertNull("birthtime", entry.getExtraPaxHeader("LIBARCHIVE.creationtime")); + assertEquals("mtime", toFileTime("2022-03-14T01:25:03.599853900Z"), entry.getLastModifiedTime()); + assertEquals("atime", toFileTime("2022-03-14T01:31:00.706927200Z"), entry.getLastAccessTime()); + assertEquals("ctime", toFileTime("2022-03-14T01:29:00.723509000Z"), entry.getStatusChangeTime()); + assertEquals("birthtime", toFileTime("2022-03-14T01:29:00.723509000Z"), entry.getCreationTime()); + + assertEquals('W', tis.read()); + assertTrue("should be at end of entry", tis.read() < 0); + + assertNull("should be at end of file", tis.getNextTarEntry()); + } + } + + private FileTime toFileTime(final String text) { + return FileTime.from(Instant.parse(text)); + } + + private TarArchiveEntry createEntryForTimeTests() { + final TarArchiveEntry entry = new TarArchiveEntry("./times.txt"); + entry.addPaxHeader("size", "1"); + entry.addPaxHeader("mtime", "1647221103.5998539"); + entry.addPaxHeader("atime", "1647221460.7069272"); + entry.addPaxHeader("ctime", "1647221339.7005053"); + entry.addPaxHeader("LIBARCHIVE.creationtime", "1647221340.7235090"); + return entry; + } + private void assertGnuMagic(final TarArchiveEntry t) { assertEquals(MAGIC_GNU + VERSION_GNU_SPACE, readMagic(t)); } diff --git a/src/test/resources/COMPRESS-612/test-times-bsd-folder.tar b/src/test/resources/COMPRESS-612/test-times-bsd-folder.tar new file mode 100644 index 00000000000..fddd24273df Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-bsd-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-epax-folder.tar b/src/test/resources/COMPRESS-612/test-times-epax-folder.tar new file mode 100644 index 00000000000..467a0b13091 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-epax-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-exustar-folder.tar b/src/test/resources/COMPRESS-612/test-times-exustar-folder.tar new file mode 100644 index 00000000000..64305ef4a36 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-exustar-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-gnu-incremental.tar b/src/test/resources/COMPRESS-612/test-times-gnu-incremental.tar new file mode 100644 index 00000000000..a80e78c2ca4 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-gnu-incremental.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-gnu.tar b/src/test/resources/COMPRESS-612/test-times-gnu.tar new file mode 100644 index 00000000000..dd6200a3a94 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-gnu.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-gnutar.tar b/src/test/resources/COMPRESS-612/test-times-gnutar.tar new file mode 100644 index 00000000000..f7ab24fd98e Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-gnutar.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-oldbsdtar.tar b/src/test/resources/COMPRESS-612/test-times-oldbsdtar.tar new file mode 100644 index 00000000000..31802770907 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-oldbsdtar.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-oldgnu-incremental.tar b/src/test/resources/COMPRESS-612/test-times-oldgnu-incremental.tar new file mode 100644 index 00000000000..f7c730c3ced Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-oldgnu-incremental.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-oldgnu.tar b/src/test/resources/COMPRESS-612/test-times-oldgnu.tar new file mode 100644 index 00000000000..4b7e0d43ff9 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-oldgnu.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-pax-folder.tar b/src/test/resources/COMPRESS-612/test-times-pax-folder.tar new file mode 100644 index 00000000000..1b878883d09 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-pax-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-posix-linux.tar b/src/test/resources/COMPRESS-612/test-times-posix-linux.tar new file mode 100644 index 00000000000..95abcd8ffc8 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-posix-linux.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-posix.tar b/src/test/resources/COMPRESS-612/test-times-posix.tar new file mode 100644 index 00000000000..46940815c46 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-posix.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-star-folder.tar b/src/test/resources/COMPRESS-612/test-times-star-folder.tar new file mode 100644 index 00000000000..5ac1d71a730 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-star-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-ustar.tar b/src/test/resources/COMPRESS-612/test-times-ustar.tar new file mode 100644 index 00000000000..0734113e2d8 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-ustar.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-v7.tar b/src/test/resources/COMPRESS-612/test-times-v7.tar new file mode 100644 index 00000000000..07e503ababc Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-v7.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xstar-folder.tar b/src/test/resources/COMPRESS-612/test-times-xstar-folder.tar new file mode 100644 index 00000000000..4ec7cc1e918 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xstar-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xstar-incremental.tar b/src/test/resources/COMPRESS-612/test-times-xstar-incremental.tar new file mode 100644 index 00000000000..1078cff5c08 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xstar-incremental.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xstar.tar b/src/test/resources/COMPRESS-612/test-times-xstar.tar new file mode 100644 index 00000000000..962859ee98d Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xstar.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xustar-folder.tar b/src/test/resources/COMPRESS-612/test-times-xustar-folder.tar new file mode 100644 index 00000000000..710290400c5 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xustar-folder.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xustar-incremental.tar b/src/test/resources/COMPRESS-612/test-times-xustar-incremental.tar new file mode 100644 index 00000000000..d4e3ab99f1f Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xustar-incremental.tar differ diff --git a/src/test/resources/COMPRESS-612/test-times-xustar.tar b/src/test/resources/COMPRESS-612/test-times-xustar.tar new file mode 100644 index 00000000000..43c58e99d90 Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xustar.tar differ