Skip to content
Permalink
Browse files
[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
  • Loading branch information
andrebrait committed Mar 18, 2022
1 parent fa0690d commit b7f0cbb63ac7a622dd985e2923193c9a59858f42
Show file tree
Hide file tree
Showing 28 changed files with 1,149 additions and 48 deletions.

Large diffs are not rendered by default.

@@ -376,7 +376,7 @@ public TarArchiveEntry getNextTarEntry() throws IOException {
}

try {
currEntry = new TarArchiveEntry(headerBuf, zipEncoding, lenient);
currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf, zipEncoding, lenient);
} catch (final IllegalArgumentException e) {
throw new IOException("Error detected parsing the header", e);
}
@@ -22,13 +22,17 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
@@ -230,8 +234,8 @@ public TarArchiveOutputStream(final OutputStream os, final int blockSize,
}

/**
* Set the long file mode. This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or
* LONGFILE_GNU(2). This specifies the treatment of long file names (names >=
* Set the long file mode. This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1), LONGFILE_GNU(2) or
* LONGFILE_POSIX(3). This specifies the treatment of long file names (names >=
* TarConstants.NAMELEN). Default is LONGFILE_ERROR.
*
* @param longFileMode the mode to use
@@ -241,9 +245,9 @@ public void setLongFileMode(final int longFileMode) {
}

/**
* Set the big number mode. This can be BIGNUMBER_ERROR(0), BIGNUMBER_POSIX(1) or
* BIGNUMBER_STAR(2). This specifies the treatment of big files (sizes >
* TarConstants.MAXSIZE) and other numeric values to big to fit into a traditional tar header.
* Set the big number mode. This can be BIGNUMBER_ERROR(0), BIGNUMBER_STAR(1) or
* BIGNUMBER_POSIX(2). This specifies the treatment of big files (sizes >
* TarConstants.MAXSIZE) and other numeric values too big to fit into a traditional tar header.
* Default is BIGNUMBER_ERROR.
*
* @param bigNumberMode the mode to use
@@ -367,7 +371,6 @@ public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException
final String entryName = entry.getName();
final boolean paxHeaderContainsPath = handleLongName(entry, entryName, paxHeaders, "path",
TarConstants.LF_GNUTYPE_LONGNAME, "file name");

final String linkName = entry.getLinkName();
final boolean paxHeaderContainsLinkPath = linkName != null && !linkName.isEmpty()
&& handleLongName(entry, linkName, paxHeaders, "linkpath",
@@ -602,12 +605,20 @@ private void addPaxHeadersForBigNumbers(final Map<String, String> paxHeaders,
TarConstants.MAXSIZE);
addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(),
TarConstants.MAXID);
addPaxHeaderForBigNumber(paxHeaders, "mtime",
entry.getModTime().getTime() / 1000,
TarConstants.MAXSIZE);
addFileTimePaxHeaderForBigNumber(paxHeaders, "mtime",
entry.getLastModifiedTime(), TarConstants.MAXSIZE);
addFileTimePaxHeader(paxHeaders, "atime", entry.getLastAccessTime());
if (entry.getStatusChangeTime() != null) {
addFileTimePaxHeader(paxHeaders, "ctime", entry.getStatusChangeTime());
} else {
// ctime is usually set from creation time on platforms where the real ctime is not available
addFileTimePaxHeader(paxHeaders, "ctime", entry.getCreationTime());
}
addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(),
TarConstants.MAXID);
// star extensions by J\u00f6rg Schilling
// libarchive extensions
addFileTimePaxHeader(paxHeaders, "LIBARCHIVE.creationtime", entry.getCreationTime());
// star extensions by Jörg Schilling
addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor",
entry.getDevMajor(), TarConstants.MAXID);
addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor",
@@ -624,11 +635,48 @@ private void addPaxHeaderForBigNumber(final Map<String, String> paxHeaders,
}
}

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 @@ private boolean handleLongName(final TarArchiveEntry entry, final String name,
}

private void transferModTime(final TarArchiveEntry from, final TarArchiveEntry to) {
Date fromModTime = from.getModTime();
final long fromModTimeSeconds = fromModTime.getTime() / 1000;
long fromModTimeSeconds = from.getLastModifiedTime().to(TimeUnit.SECONDS);
if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) {
fromModTime = new Date(0);
fromModTimeSeconds = 0;
}
to.setModTime(fromModTime);
to.setLastModifiedTime(FileTime.from(fromModTimeSeconds, TimeUnit.SECONDS));
}
}
@@ -227,6 +227,14 @@ public interface TarConstants {
*/
byte LF_OLDNORM = 0;

/**
* Offset inside the header for the "link flag" field.
*
* @since 1.22
* @see TarArchiveEntry
*/
int LF_OFFSET = 156;

/**
* Normal file type.
*/
@@ -305,6 +313,13 @@ public interface TarConstants {
*/
byte LF_PAX_GLOBAL_EXTENDED_HEADER = (byte) 'g';

/**
* Identifies the entry as a multi-volume past volume #0
*
* @since 1.22
*/
byte LF_MULTIVOLUME = (byte) 'M';

/**
* The magic tag representing a POSIX tar archive.
*/
@@ -347,6 +362,14 @@ public interface TarConstants {
*/
String MAGIC_XSTAR = "tar\0";

/**
* Offset inside the header for the xtar multivolume data
*
* @since 1.22
* @see TarArchiveEntry
*/
int XSTAR_MULTIVOLUME_OFFSET = 464;

/**
* Offset inside the header for the xstar magic bytes.
* @since 1.11
@@ -366,13 +389,37 @@ 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
*/
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.
*
@@ -254,7 +254,8 @@ private TarArchiveEntry getNextTarEntry() throws IOException {
}

try {
currEntry = new TarArchiveEntry(headerBuf.array(), zipEncoding, lenient, archive.position());
final long position = archive.position();
currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf.array(), zipEncoding, lenient, position);
} catch (final IllegalArgumentException e) {
throw new IOException("Error detected parsing the header", e);
}

0 comments on commit b7f0cbb

Please sign in to comment.