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 &gt;=
+     * 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 &gt;=
      * 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 &gt;
-     * 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 &gt;
+     * 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