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