You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2022/03/18 18:29:19 UTC
[commons-compress] branch master updated: [COMPRESS-612] improve TAR support for file times (#254)
This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new b7f0cbb [COMPRESS-612] improve TAR support for file times (#254)
b7f0cbb is described below
commit b7f0cbb63ac7a622dd985e2923193c9a59858f42
Author: Andre Brait <an...@gmail.com>
AuthorDate: Fri Mar 18 19:27:29 2022 +0100
[COMPRESS-612] improve TAR support for file times (#254)
* COMPRESS-612: improve TAR support for file times
R/W atime and ctime support for XSTAR/XUSTAR/POSIX
R/W high precision (100ns increments) time support for POSIX
Read support for atime and ctime for OLDGNU/GNU
Use FileTime instead of Date to allow for higher precision
* COMPRESS-612: split ctime and birthtime
* COMPRESS-612: address review notes, more tests
* COMPRESS-612: Test older formats, add comments
* COMPRESS-612: Fix GNU tar tests
* COMPRESS-612: Improve documentation
---
.../compress/archivers/tar/TarArchiveEntry.java | 388 +++++++++++++--
.../archivers/tar/TarArchiveInputStream.java | 2 +-
.../archivers/tar/TarArchiveOutputStream.java | 79 +++-
.../compress/archivers/tar/TarConstants.java | 47 ++
.../commons/compress/archivers/tar/TarFile.java | 3 +-
.../compress/archivers/tar/FileTimesIT.java | 522 +++++++++++++++++++++
.../archivers/tar/TarArchiveEntryTest.java | 156 ++++++
.../COMPRESS-612/test-times-bsd-folder.tar | Bin 0 -> 4608 bytes
.../COMPRESS-612/test-times-epax-folder.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-exustar-folder.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-gnu-incremental.tar | Bin 0 -> 10240 bytes
src/test/resources/COMPRESS-612/test-times-gnu.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-gnutar.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-oldbsdtar.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-oldgnu-incremental.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-oldgnu.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-pax-folder.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-posix-linux.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-posix.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-star-folder.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-ustar.tar | Bin 0 -> 10240 bytes
src/test/resources/COMPRESS-612/test-times-v7.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-xstar-folder.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-xstar-incremental.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-xstar.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-xustar-folder.tar | Bin 0 -> 10240 bytes
.../COMPRESS-612/test-times-xustar-incremental.tar | Bin 0 -> 10240 bytes
.../resources/COMPRESS-612/test-times-xustar.tar | Bin 0 -> 10240 bytes
28 files changed, 1149 insertions(+), 48 deletions(-)
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 b722568..6233079 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 @@ package org.apache.commons.compress.archivers.tar;
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.BasicFileAttributes;
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.Iterator;
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 @@ import org.apache.commons.compress.utils.IOUtils;
* </pre>
* <p>which is identical to new-style POSIX up to the first 130 bytes of the prefix.</p>
*
+ * <p>
+ * The C structure for the xstar-specific parts of a xstar Tar Entry's header is:
+ * <pre>
+ * 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"
+ * };
+ * </pre>
+ * </p>
+ *
* @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 class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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 class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
} 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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
final Set<String> 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 class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
*/
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<String, String> 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);
}
/**
@@ -574,6 +652,24 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
}
/**
+ * 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<String, String> 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 class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
* 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,26 +933,116 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
*
* @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.
*
* @return if the header checksum is reasonably correct
@@ -1360,8 +1548,11 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
throws IOException {
/*
* The following headers are defined for Pax.
- * atime, ctime, charset: cannot use these without changing TarArchiveEntry fields
+ * charset: cannot use these without changing TarArchiveEntry fields
* mtime
+ * atime
+ * ctime
+ * LIBARCHIVE.creationtime
* comment
* gid, gname
* linkpath
@@ -1405,7 +1596,16 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
setSize(size);
break;
case "mtime":
- setModTime((long) (Double.parseDouble(val) * 1000));
+ setLastModifiedTime(FileTime.from(parseInstantFromDecimalSeconds(val)));
+ break;
+ case "atime":
+ setLastAccessTime(FileTime.from(parseInstantFromDecimalSeconds(val)));
+ break;
+ case "ctime":
+ setStatusChangeTime(FileTime.from(parseInstantFromDecimalSeconds(val)));
+ break;
+ case "LIBARCHIVE.creationtime":
+ setCreationTime(FileTime.from(parseInstantFromDecimalSeconds(val)));
break;
case "SCHILY.devminor":
final int devMinor = Integer.parseInt(val);
@@ -1437,7 +1637,12 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
}
}
-
+ private static Instant parseInstantFromDecimalSeconds(final String value) {
+ final BigDecimal epochSeconds = new BigDecimal(value);
+ final long seconds = epochSeconds.longValue();
+ final long nanos = epochSeconds.remainder(BigDecimal.ONE).movePointRight(9).longValue();
+ return Instant.ofEpochSecond(seconds, nanos);
+ }
/**
* If this entry represents a file, and the file is a directory, return
@@ -1509,14 +1714,12 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
offset = writeEntryHeaderField(groupId, outbuf, offset, GIDLEN,
starMode);
offset = writeEntryHeaderField(size, outbuf, offset, SIZELEN, starMode);
- offset = writeEntryHeaderField(modTime, outbuf, offset, MODTIMELEN,
- starMode);
+ offset = writeEntryHeaderField(mTime.to(TimeUnit.SECONDS), outbuf, offset,
+ MODTIMELEN, starMode);
final int csOffset = offset;
- for (int c = 0; c < CHKSUMLEN; ++c) {
- outbuf[offset++] = (byte) ' ';
- }
+ offset = fill((byte) ' ', offset, outbuf, CHKSUMLEN);
outbuf[offset++] = linkFlag;
offset = TarUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN,
@@ -1532,15 +1735,45 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
offset = writeEntryHeaderField(devMinor, outbuf, offset, DEVLEN,
starMode);
- while (offset < outbuf.length) {
- outbuf[offset++] = 0;
+ if (starMode) {
+ // skip prefix
+ offset = fill(0, offset, outbuf, PREFIXLEN_XSTAR);
+ offset = writeEntryHeaderOptionalTimeField(aTime, offset, outbuf, ATIMELEN_XSTAR);
+ offset = writeEntryHeaderOptionalTimeField(cTime, offset, outbuf, CTIMELEN_XSTAR);
+ // 8-byte fill
+ offset = fill(0, offset, outbuf, 8);
+ // Do not write MAGIC_XSTAR because it causes issues with some TAR tools
+ // This makes it effectively XUSTAR, which guarantees compatibility with USTAR
+ offset = fill(0, offset, outbuf, XSTAR_MAGIC_LEN);
}
+ offset = fill(0, offset, outbuf, outbuf.length - offset); // NOSONAR - assignment as documentation
+
final long chk = TarUtils.computeCheckSum(outbuf);
TarUtils.formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN);
}
+ private int writeEntryHeaderOptionalTimeField(FileTime time, int offset, byte[] outbuf, int fieldLength) {
+ if (time != null) {
+ offset = writeEntryHeaderField(time.to(TimeUnit.SECONDS), outbuf, offset, fieldLength, true);
+ } else {
+ offset = fill(0, offset, outbuf, fieldLength);
+ }
+ return offset;
+ }
+
+ private int fill(final int value, final int offset, final byte[] outbuf, final int length) {
+ return fill((byte) value, offset, outbuf, length);
+ }
+
+ private int fill(final byte value, final int offset, final byte[] outbuf, final int length) {
+ for (int i = 0; i < length; i++) {
+ outbuf[offset + i] = value;
+ }
+ return offset + length;
+ }
+
private int writeEntryHeaderField(final long value, final byte[] outbuf, final int offset,
final int length, final boolean starMode) {
if (!starMode && (value < 0
@@ -1591,15 +1824,21 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
private void parseTarHeader(final byte[] header, final ZipEncoding encoding,
final boolean oldStyle, final boolean lenient)
throws IOException {
+ parseTarHeader(Collections.emptyMap(), header, encoding, oldStyle, lenient);
+ }
+
+ private void parseTarHeader(final Map<String, String> 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<String, String> globalPaxHeaders, final byte[] header,
+ final ZipEncoding encoding, final boolean oldStyle, final boolean lenient)
throws IOException {
int offset = 0;
@@ -1617,7 +1856,7 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
}
}
+ 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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
* @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<String, String> 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 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO
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<String, String> 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<String, String> 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 0fbec40..b622af6 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 class TarArchiveInputStream extends ArchiveInputStream {
}
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 0f49490..70b0c57 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.File;
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 class TarArchiveOutputStream extends ArchiveOutputStream {
}
/**
- * 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 class TarArchiveOutputStream extends ArchiveOutputStream {
}
/**
- * 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 class TarArchiveOutputStream extends ArchiveOutputStream {
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 @@ public class TarArchiveOutputStream extends ArchiveOutputStream {
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 @@ public class TarArchiveOutputStream extends ArchiveOutputStream {
}
}
+ private void addFileTimePaxHeaderForBigNumber(final Map<String, String> 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<String, String> 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<String, String> 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 @@ public class TarArchiveOutputStream extends ArchiveOutputStream {
}
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 ffd2aa5..289046c 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
@@ -228,6 +228,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.
*/
byte LF_NORMAL = (byte) '0';
@@ -306,6 +314,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.
*/
String MAGIC_POSIX = "ustar\0";
@@ -348,6 +363,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
*/
@@ -367,6 +390,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.
*
* @since 1.11
@@ -374,6 +413,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.
*
* @since 1.11
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 5491c8b..ec9359f 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 @@ public class TarFile implements Closeable {
}
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 0000000..1ba3023
--- /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 89c2b0f..8c1b474 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.ByteArrayOutputStream;
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 class TarArchiveEntryTest implements TarConstants {
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 0000000..fddd242
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 0000000..467a0b1
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 0000000..64305ef
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 0000000..a80e78c
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 0000000..dd6200a
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 0000000..f7ab24f
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 0000000..3180277
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 0000000..f7c730c
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 0000000..4b7e0d4
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 0000000..1b87888
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 0000000..95abcd8
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 0000000..4694081
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 0000000..5ac1d71
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 0000000..0734113
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 0000000..07e503a
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 0000000..4ec7cc1
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 0000000..1078cff
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 0000000..962859e
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 0000000..7102904
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 0000000..d4e3ab9
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 0000000..43c58e9
Binary files /dev/null and b/src/test/resources/COMPRESS-612/test-times-xustar.tar differ