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 650ad055 COMPRESS-614: Use FileTime in SevenZArchiveEntry (#256) 650ad055 is described below commit 650ad0555e3702177c7403544ae8850f4ebbc9b1 Author: Andre Brait <andrebr...@gmail.com> AuthorDate: Thu Dec 8 02:10:16 2022 +0100 COMPRESS-614: Use FileTime in SevenZArchiveEntry (#256) * COMPRESS-612: Add deprecation notice * COMPRESS-614: Move NTFS <-> FileTime to ZipUtil * COMPRESS-614: Use FileTime in SevenZArchiveEntry * COMPRESS-614: Make Sonar happy * COMPRESS-614: Reuse time operations in TimeUtils * COMPRESS-614: Fix NTFS time conversion * COMPRESS-614: Remove unused imports from ZipUtil * COMPRESS-614: Revert some documentation changes * COMPRESS-614: Add tests for TimeUtils * COMPRESS-614: Refine tests * COMPRESS-614: Add missing license text * COMPRESS-614: Add missing since tags * COMPRESS-614: Workaround for Java 17 on *NIX OSes Java 17 supports nanosecond-precision FileTimes on Linux and macOS NTFS dates support only 100ns units, so truncate dates when comparing * COMPRESS-614: Add missing test cases * Update Javadoc since tags * Update Javadoc tag * Update Javadoc tags * Update TimeUtils.java Co-authored-by: Gary Gregory <garydgreg...@users.noreply.github.com> --- .../archivers/sevenz/SevenZArchiveEntry.java | 160 +++++++++++++----- .../archivers/sevenz/SevenZOutputFile.java | 30 +++- .../compress/archivers/tar/TarArchiveEntry.java | 11 +- .../commons/compress/archivers/zip/X000A_NTFS.java | 105 ++++++++++-- .../apache/commons/compress/utils/TimeUtils.java | 141 ++++++++++++++++ .../commons/compress/archivers/SevenZTestCase.java | 26 ++- .../archivers/sevenz/SevenZArchiveEntryTest.java | 28 ++++ .../compress/archivers/sevenz/SevenZFileTest.java | 72 +++++++- .../archivers/sevenz/SevenZOutputFileTest.java | 36 +++- .../compress/archivers/zip/X000A_NTFSTest.java | 42 +++++ .../commons/compress/utils/TimeUtilsTest.java | 181 +++++++++++++++++++++ src/test/resources/times.7z | Bin 0 -> 3934 bytes 12 files changed, 737 insertions(+), 95 deletions(-) diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntry.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntry.java index 74d4276e..b303085f 100644 --- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntry.java +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntry.java @@ -18,15 +18,15 @@ package org.apache.commons.compress.archivers.sevenz; import java.util.Arrays; -import java.util.Calendar; +import java.nio.file.attribute.FileTime; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.Objects; -import java.util.TimeZone; import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.utils.TimeUtils; /** * An entry in a 7z archive. @@ -42,9 +42,9 @@ public class SevenZArchiveEntry implements ArchiveEntry { private boolean hasCreationDate; private boolean hasLastModifiedDate; private boolean hasAccessDate; - private long creationDate; - private long lastModifiedDate; - private long accessDate; + private FileTime creationDate; + private FileTime lastModifiedDate; + private FileTime accessDate; private boolean hasWindowsAttributes; private int windowsAttributes; private boolean hasCrc; @@ -148,13 +148,26 @@ public class SevenZArchiveEntry implements ArchiveEntry { /** * Gets the creation date. - * @throws UnsupportedOperationException if the entry hasn't got a - * creation date. - * @return the creation date + * This is equivalent to {@link SevenZArchiveEntry#getCreationTime()}, but precision is truncated to milliseconds. + * + * @throws UnsupportedOperationException if the entry hasn't got a creation date. + * @return the new creation date + * @see SevenZArchiveEntry#getCreationTime() */ public Date getCreationDate() { + return TimeUtils.fileTimeToDate(getCreationTime()); + } + + /** + * Gets the creation time. + * + * @throws UnsupportedOperationException if the entry hasn't got a creation time. + * @return the creation time + * @since 1.23 + */ + public FileTime getCreationTime() { if (hasCreationDate) { - return ntfsTimeToJavaTime(creationDate); + return creationDate; } throw new UnsupportedOperationException( "The entry doesn't have this timestamp"); @@ -166,17 +179,29 @@ public class SevenZArchiveEntry implements ArchiveEntry { * @param ntfsCreationDate the creation date */ public void setCreationDate(final long ntfsCreationDate) { - this.creationDate = ntfsCreationDate; + this.creationDate = TimeUtils.ntfsTimeToFileTime(ntfsCreationDate); } /** - * Sets the creation date, - * @param creationDate the creation date + * Sets the creation date. + * + * @param creationDate the new creation date + * @see SevenZArchiveEntry#setCreationTime(FileTime) */ public void setCreationDate(final Date creationDate) { - hasCreationDate = creationDate != null; + setCreationTime(TimeUtils.dateToFileTime(creationDate)); + } + + /** + * Sets the creation time. + * + * @param time the new creation time + * @since 1.23 + */ + public void setCreationTime(final FileTime time) { + hasCreationDate = time != null; if (hasCreationDate) { - this.creationDate = javaTimeToNtfsTime(creationDate); + this.creationDate = time; } } @@ -199,14 +224,27 @@ public class SevenZArchiveEntry implements ArchiveEntry { /** * Gets the last modified date. - * @throws UnsupportedOperationException if the entry hasn't got a - * last modified date. + * This is equivalent to {@link SevenZArchiveEntry#getLastModifiedTime()}, but precision is truncated to milliseconds. + * + * @throws UnsupportedOperationException if the entry hasn't got a last modified date. * @return the last modified date + * @see SevenZArchiveEntry#getLastModifiedTime() */ @Override public Date getLastModifiedDate() { + return TimeUtils.fileTimeToDate(getLastModifiedTime()); + } + + /** + * Gets the last modified time. + * + * @throws UnsupportedOperationException if the entry hasn't got a last modified time. + * @return the last modified time + * @since 1.23 + */ + public FileTime getLastModifiedTime() { if (hasLastModifiedDate) { - return ntfsTimeToJavaTime(lastModifiedDate); + return lastModifiedDate; } throw new UnsupportedOperationException( "The entry doesn't have this timestamp"); @@ -218,17 +256,29 @@ public class SevenZArchiveEntry implements ArchiveEntry { * @param ntfsLastModifiedDate the last modified date */ public void setLastModifiedDate(final long ntfsLastModifiedDate) { - this.lastModifiedDate = ntfsLastModifiedDate; + this.lastModifiedDate = TimeUtils.ntfsTimeToFileTime(ntfsLastModifiedDate); } /** - * Sets the last modified date, - * @param lastModifiedDate the last modified date + * Sets the last modified date. + * + * @param lastModifiedDate the new last modified date + * @see SevenZArchiveEntry#setLastModifiedTime(FileTime) */ public void setLastModifiedDate(final Date lastModifiedDate) { - hasLastModifiedDate = lastModifiedDate != null; + setLastModifiedTime(TimeUtils.dateToFileTime(lastModifiedDate)); + } + + /** + * Sets the last modified time. + * + * @param time the new last modified time + * @since 1.23 + */ + public void setLastModifiedTime(final FileTime time) { + hasLastModifiedDate = time != null; if (hasLastModifiedDate) { - this.lastModifiedDate = javaTimeToNtfsTime(lastModifiedDate); + this.lastModifiedDate = time; } } @@ -250,13 +300,26 @@ public class SevenZArchiveEntry implements ArchiveEntry { /** * Gets the access date. - * @throws UnsupportedOperationException if the entry hasn't got a - * access date. + * This is equivalent to {@link SevenZArchiveEntry#getAccessTime()}, but precision is truncated to milliseconds. + * + * @throws UnsupportedOperationException if the entry hasn't got an access date. * @return the access date + * @see SevenZArchiveEntry#getAccessTime() */ public Date getAccessDate() { + return TimeUtils.fileTimeToDate(getAccessTime()); + } + + /** + * Gets the access time. + * + * @throws UnsupportedOperationException if the entry hasn't got an access time. + * @return the access time + * @since 1.23 + */ + public FileTime getAccessTime() { if (hasAccessDate) { - return ntfsTimeToJavaTime(accessDate); + return accessDate; } throw new UnsupportedOperationException( "The entry doesn't have this timestamp"); @@ -268,17 +331,29 @@ public class SevenZArchiveEntry implements ArchiveEntry { * @param ntfsAccessDate the access date */ public void setAccessDate(final long ntfsAccessDate) { - this.accessDate = ntfsAccessDate; + this.accessDate = TimeUtils.ntfsTimeToFileTime(ntfsAccessDate); } /** - * Sets the access date, - * @param accessDate the access date + * Sets the access date. + * + * @param accessDate the new access date + * @see SevenZArchiveEntry#setAccessTime(FileTime) */ public void setAccessDate(final Date accessDate) { - hasAccessDate = accessDate != null; + setAccessTime(TimeUtils.dateToFileTime(accessDate)); + } + + /** + * Sets the access time. + * + * @param time the new access time + * @since 1.23 + */ + public void setAccessTime(final FileTime time) { + hasAccessDate = time != null; if (hasAccessDate) { - this.accessDate = javaTimeToNtfsTime(accessDate); + this.accessDate = time; } } @@ -528,9 +603,9 @@ public class SevenZArchiveEntry implements ArchiveEntry { hasCreationDate == other.hasCreationDate && hasLastModifiedDate == other.hasLastModifiedDate && hasAccessDate == other.hasAccessDate && - creationDate == other.creationDate && - lastModifiedDate == other.lastModifiedDate && - accessDate == other.accessDate && + Objects.equals(creationDate, other.creationDate) && + Objects.equals(lastModifiedDate, other.lastModifiedDate) && + Objects.equals(accessDate, other.accessDate) && hasWindowsAttributes == other.hasWindowsAttributes && windowsAttributes == other.windowsAttributes && hasCrc == other.hasCrc && @@ -546,27 +621,24 @@ public class SevenZArchiveEntry implements ArchiveEntry { * to Java time. * @param ntfsTime the NTFS time in 100 nanosecond units * @return the Java time + * @deprecated Use {@link TimeUtils#ntfsTimeToDate(long)} instead. + * @see TimeUtils#ntfsTimeToDate(long) */ + @Deprecated public static Date ntfsTimeToJavaTime(final long ntfsTime) { - final Calendar ntfsEpoch = Calendar.getInstance(); - ntfsEpoch.setTimeZone(TimeZone.getTimeZone("GMT+0")); - ntfsEpoch.set(1601, 0, 1, 0, 0, 0); - ntfsEpoch.set(Calendar.MILLISECOND, 0); - final long realTime = ntfsEpoch.getTimeInMillis() + (ntfsTime / (10*1000)); - return new Date(realTime); + return TimeUtils.ntfsTimeToDate(ntfsTime); } /** * Converts Java time to NTFS time. * @param date the Java time * @return the NTFS time + * @deprecated Use {@link TimeUtils#dateToNtfsTime(Date)} instead. + * @see TimeUtils#dateToNtfsTime(Date) */ + @Deprecated public static long javaTimeToNtfsTime(final Date date) { - final Calendar ntfsEpoch = Calendar.getInstance(); - ntfsEpoch.setTimeZone(TimeZone.getTimeZone("GMT+0")); - ntfsEpoch.set(1601, 0, 1, 0, 0, 0); - ntfsEpoch.set(Calendar.MILLISECOND, 0); - return ((date.getTime() - ntfsEpoch.getTimeInMillis())* 1000 * 10); + return TimeUtils.dateToNtfsTime(date); } private boolean equalSevenZMethods(final Iterable<? extends SevenZMethodConfiguration> c1, diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java index ba6de451..903e0df1 100644 --- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java @@ -36,6 +36,7 @@ import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; @@ -50,6 +51,7 @@ import java.util.zip.CRC32; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.utils.CountingOutputStream; +import org.apache.commons.compress.utils.TimeUtils; /** * Writes a 7z file. @@ -159,7 +161,11 @@ public class SevenZOutputFile implements Closeable { final SevenZArchiveEntry entry = new SevenZArchiveEntry(); entry.setDirectory(inputFile.isDirectory()); entry.setName(entryName); - entry.setLastModifiedDate(new Date(inputFile.lastModified())); + try { + fillDates(inputFile.toPath(), entry); + } catch (IOException e) { // NOSONAR + entry.setLastModifiedDate(new Date(inputFile.lastModified())); + } return entry; } @@ -179,10 +185,18 @@ public class SevenZOutputFile implements Closeable { final SevenZArchiveEntry entry = new SevenZArchiveEntry(); entry.setDirectory(Files.isDirectory(inputPath, options)); entry.setName(entryName); - entry.setLastModifiedDate(new Date(Files.getLastModifiedTime(inputPath, options).toMillis())); + fillDates(inputPath, entry, options); return entry; } + private void fillDates(final Path inputPath, final SevenZArchiveEntry entry, + final LinkOption... options) throws IOException { + BasicFileAttributes attributes = Files.readAttributes(inputPath, BasicFileAttributes.class, options); + entry.setLastModifiedTime(attributes.lastModifiedTime()); + entry.setCreationTime(attributes.creationTime()); + entry.setAccessTime(attributes.lastAccessTime()); + } + /** * Records an archive entry to add. * @@ -651,8 +665,8 @@ public class SevenZOutputFile implements Closeable { out.write(0); for (final SevenZArchiveEntry entry : files) { if (entry.getHasCreationDate()) { - out.writeLong(Long.reverseBytes( - SevenZArchiveEntry.javaTimeToNtfsTime(entry.getCreationDate()))); + final long ntfsTime = TimeUtils.fileTimeToNtfsTime(entry.getCreationTime()); + out.writeLong(Long.reverseBytes(ntfsTime)); } } out.flush(); @@ -687,8 +701,8 @@ public class SevenZOutputFile implements Closeable { out.write(0); for (final SevenZArchiveEntry entry : files) { if (entry.getHasAccessDate()) { - out.writeLong(Long.reverseBytes( - SevenZArchiveEntry.javaTimeToNtfsTime(entry.getAccessDate()))); + final long ntfsTime = TimeUtils.fileTimeToNtfsTime(entry.getAccessTime()); + out.writeLong(Long.reverseBytes(ntfsTime)); } } out.flush(); @@ -723,8 +737,8 @@ public class SevenZOutputFile implements Closeable { out.write(0); for (final SevenZArchiveEntry entry : files) { if (entry.getHasLastModifiedDate()) { - out.writeLong(Long.reverseBytes( - SevenZArchiveEntry.javaTimeToNtfsTime(entry.getLastModifiedDate()))); + final long ntfsTime = TimeUtils.fileTimeToNtfsTime(entry.getLastModifiedTime()); + out.writeLong(Long.reverseBytes(ntfsTime)); } } out.flush(); 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 a8a60c67..689c211b 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 @@ -49,6 +49,7 @@ import org.apache.commons.compress.archivers.EntryStreamOffsets; import org.apache.commons.compress.archivers.zip.ZipEncoding; import org.apache.commons.compress.utils.ArchiveUtils; import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.TimeUtils; /** * This class represents an entry in a Tar archive. It consists @@ -210,7 +211,11 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO /** Default permissions bits for files */ public static final int DEFAULT_FILE_MODE = 0100644; - /** Convert millis to seconds */ + /** + * Convert millis to seconds + * @deprecated Unused. + */ + @Deprecated public static final int MILLIS_PER_SECOND = 1000; private static FileTime fileTimeFromOptionalSeconds(long seconds) { @@ -992,7 +997,7 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO * @see TarArchiveEntry#getLastModifiedTime() */ public Date getModTime() { - return new Date(mTime.toMillis()); + return TimeUtils.fileTimeToDate(mTime); } /** @@ -1906,7 +1911,7 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants, EntryStreamO * @see TarArchiveEntry#setLastModifiedTime(FileTime) */ public void setModTime(final Date time) { - setLastModifiedTime(FileTime.fromMillis(time.getTime())); + setLastModifiedTime(TimeUtils.dateToFileTime(time)); } /** diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java b/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java index 2f8645d4..542b2d91 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java @@ -17,6 +17,9 @@ */ package org.apache.commons.compress.archivers.zip; +import org.apache.commons.compress.utils.TimeUtils; + +import java.nio.file.attribute.FileTime; import java.util.Date; import java.util.Objects; import java.util.zip.ZipException; @@ -244,6 +247,39 @@ public class X000A_NTFS implements ZipExtraField { return zipToDate(createTime); } + /** + * Gets the modify time as as a {@link FileTime} + * of this zip entry, or null if no such timestamp exists in the zip entry. + * + * @return modify time as a {@link FileTime} or null. + * @since 1.23 + */ + public FileTime getModifyFileTime() { + return zipToFileTime(modifyTime); + } + + /** + * Gets the access time as a {@link FileTime} + * of this zip entry, or null if no such timestamp exists in the zip entry. + * + * @return access time as a {@link FileTime} or null. + * @since 1.23 + */ + public FileTime getAccessFileTime() { + return zipToFileTime(accessTime); + } + + /** + * Gets the create time as a {@link FileTime} + * of this zip entry, or null if no such timestamp exists in the zip entry. + * + * @return create time as a {@link FileTime} or null. + * @since 1.23 + */ + public FileTime getCreateFileTime() { + return zipToFileTime(createTime); + } + /** * Sets the File last modification time of this zip entry using a * ZipEightByteInteger object. @@ -304,6 +340,36 @@ public class X000A_NTFS implements ZipExtraField { */ public void setCreateJavaTime(final Date d) { setCreateTime(dateToZip(d)); } + /** + * Sets the modify time. + * + * @param time modify time as a {@link FileTime} + * @since 1.23 + */ + public void setModifyFileTime(final FileTime time) { + setModifyTime(fileTimeToZip(time)); + } + + /** + * Sets the access time. + * + * @param time access time as a {@link FileTime} + * @since 1.23 + */ + public void setAccessFileTime(final FileTime time) { + setAccessTime(fileTimeToZip(time)); + } + + /** + * Sets the create time. + * + * @param time create time as a {@link FileTime} + * @since 1.23 + */ + public void setCreateFileTime(final FileTime time) { + setCreateTime(fileTimeToZip(time)); + } + /** * Returns a String representation of this class useful for * debugging purposes. @@ -315,9 +381,9 @@ public class X000A_NTFS implements ZipExtraField { public String toString() { final StringBuilder buf = new StringBuilder(); buf.append("0x000A Zip Extra Field:") - .append(" Modify:[").append(getModifyJavaTime()).append("] ") - .append(" Access:[").append(getAccessJavaTime()).append("] ") - .append(" Create:[").append(getCreateJavaTime()).append("] "); + .append(" Modify:[").append(getModifyFileTime()).append("] ") + .append(" Access:[").append(getAccessFileTime()).append("] ") + .append(" Create:[").append(getCreateFileTime()).append("] "); return buf.toString(); } @@ -374,22 +440,31 @@ public class X000A_NTFS implements ZipExtraField { } } - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724290%28v=vs.85%29.aspx - // A file time is a 64-bit value that represents the number of - // 100-nanosecond intervals that have elapsed since 12:00 - // A.M. January 1, 1601 Coordinated Universal Time (UTC). - // this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals - private static final long EPOCH_OFFSET = -116444736000000000L; - private static ZipEightByteInteger dateToZip(final Date d) { - if (d == null) { return null; } - return new ZipEightByteInteger((d.getTime() * 10000L) - EPOCH_OFFSET); + if (d == null) { + return null; + } + return new ZipEightByteInteger(TimeUtils.dateToNtfsTime(d)); + } + + private static ZipEightByteInteger fileTimeToZip(final FileTime time) { + if (time == null) { + return null; + } + return new ZipEightByteInteger(TimeUtils.fileTimeToNtfsTime(time)); } private static Date zipToDate(final ZipEightByteInteger z) { - if (z == null || ZipEightByteInteger.ZERO.equals(z)) { return null; } - final long l = (z.getLongValue() + EPOCH_OFFSET) / 10000L; - return new Date(l); + if (z == null || ZipEightByteInteger.ZERO.equals(z)) { + return null; + } + return TimeUtils.ntfsTimeToDate(z.getLongValue()); } + private static FileTime zipToFileTime(final ZipEightByteInteger z) { + if (z == null || ZipEightByteInteger.ZERO.equals(z)) { + return null; + } + return TimeUtils.ntfsTimeToFileTime(z.getLongValue()); + } } diff --git a/src/main/java/org/apache/commons/compress/utils/TimeUtils.java b/src/main/java/org/apache/commons/compress/utils/TimeUtils.java new file mode 100644 index 00000000..e4bc119c --- /dev/null +++ b/src/main/java/org/apache/commons/compress/utils/TimeUtils.java @@ -0,0 +1,141 @@ +/* + * 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.utils; + +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * Utility class for handling time-related types and conversions. + * + * @since 1.23 + */ +public final class TimeUtils { + + /** Private constructor to prevent instantiation of this utility class. */ + private TimeUtils(){ + } + + /** + * <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms724290%28v=vs.85%29.aspx">Windows File Times</a> + * <p> + * A file time is a 64-bit value that represents the number of + * 100-nanosecond intervals that have elapsed since 12:00 + * A.M. January 1, 1601 Coordinated Universal Time (UTC). + * This is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals. + * </p> + */ + public static final long WINDOWS_EPOCH_OFFSET = -116444736000000000L; + + /** The amount of 100-nanosecond intervals in one second. */ + public static final long HUNDRED_NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1) / 100; + + /** The amount of 100-nanosecond intervals in one millisecond. */ + public static final long HUNDRED_NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1) / 100; + + /** + * Converts NTFS time (100 nanosecond units since 1 January 1601) to Java time. + * + * @param ntfsTime the NTFS time in 100 nanosecond units + * @return the Date + */ + public static Date ntfsTimeToDate(final long ntfsTime) { + final long javaHundredNanos = Math.addExact(ntfsTime, WINDOWS_EPOCH_OFFSET); + final long javaMillis = Math.floorDiv(javaHundredNanos, HUNDRED_NANOS_PER_MILLISECOND); + return new Date(javaMillis); + } + + /** + * Converts a {@link Date} to NTFS time. + * + * @param date the Date + * @return the NTFS time + */ + public static long dateToNtfsTime(final Date date) { + final long javaHundredNanos = date.getTime() * HUNDRED_NANOS_PER_MILLISECOND; + return Math.subtractExact(javaHundredNanos, WINDOWS_EPOCH_OFFSET); + } + + /** + * Converts a {@link FileTime} to NTFS time (100-nanosecond units since 1 January 1601). + * + * @param time the FileTime + * @return the NTFS time in 100-nanosecond units + * + * @see TimeUtils#WINDOWS_EPOCH_OFFSET + * @see TimeUtils#ntfsTimeToFileTime(long) + */ + public static long fileTimeToNtfsTime(final FileTime time) { + final Instant instant = time.toInstant(); + final long javaHundredNanos = (instant.getEpochSecond() * HUNDRED_NANOS_PER_SECOND) + (instant.getNano() / 100); + return Math.subtractExact(javaHundredNanos, WINDOWS_EPOCH_OFFSET); + } + + /** + * Converts NTFS time (100-nanosecond units since 1 January 1601) to a FileTime. + * + * @param ntfsTime the NTFS time in 100-nanosecond units + * @return the FileTime + * + * @see TimeUtils#WINDOWS_EPOCH_OFFSET + * @see TimeUtils#fileTimeToNtfsTime(FileTime) + */ + public static FileTime ntfsTimeToFileTime(final long ntfsTime) { + final long javaHundredsNanos = Math.addExact(ntfsTime, WINDOWS_EPOCH_OFFSET); + final long javaSeconds = Math.floorDiv(javaHundredsNanos, HUNDRED_NANOS_PER_SECOND); + final long javaNanos = Math.floorMod(javaHundredsNanos, HUNDRED_NANOS_PER_SECOND) * 100; + return FileTime.from(Instant.ofEpochSecond(javaSeconds, javaNanos)); + } + + /** + * Truncates a FileTime to 100-nanosecond precision. + * + * @param fileTime the FileTime to be truncated + * @return the truncated FileTime + */ + public static FileTime truncateToHundredNanos(final FileTime fileTime) { + final Instant instant = fileTime.toInstant(); + return FileTime.from(Instant.ofEpochSecond(instant.getEpochSecond(), (instant.getNano() / 100) * 100)); + } + + /** + * Converts {@link Date} to a {@link FileTime}. + * If the provided Date is {@code null}, the returned FileTime is also {@code null}. + * + * @param date the date to be converted. + * @return a {@link FileTime} which corresponds to the supplied date, or {@code null} if the date is {@code null}. + * @see TimeUtils#fileTimeToDate(FileTime) + */ + public static FileTime dateToFileTime(final Date date) { + return date != null ? FileTime.fromMillis(date.getTime()) : null; + } + + /** + * Converts {@link FileTime} to a {@link Date}. + * If the provided FileTime is {@code null}, the returned Date is also {@code null}. + * + * @param time the file time to be converted. + * @return a {@link Date} which corresponds to the supplied time, or {@code null} if the time is {@code null}. + * @see TimeUtils#dateToFileTime(Date) + */ + public static Date fileTimeToDate(final FileTime time) { + return time != null ? new Date(time.toMillis()) : null; + } +} diff --git a/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java b/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java index 1f6535de..704addc8 100644 --- a/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java +++ b/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java @@ -17,12 +17,11 @@ */ package org.apache.commons.compress.archivers; -import static org.junit.Assert.assertEquals; - import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; @@ -32,10 +31,13 @@ import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; import org.apache.commons.compress.archivers.sevenz.SevenZFile; import org.apache.commons.compress.archivers.sevenz.SevenZMethod; import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; +import org.apache.commons.compress.utils.TimeUtils; import org.junit.Assume; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.Assert.*; + public class SevenZTestCase extends AbstractTestCase { private File output; @@ -84,14 +86,22 @@ public class SevenZTestCase extends AbstractTestCase { SevenZArchiveEntry entry; entry = archive.getNextEntry(); - assert (entry != null); - assertEquals(entry.getName(), file1.getName()); + assertNotNull(entry); + assertEquals(file1.getName(),entry.getName()); + BasicFileAttributes attributes = Files.readAttributes(file1.toPath(), BasicFileAttributes.class); + assertEquals(TimeUtils.truncateToHundredNanos(attributes.lastModifiedTime()), entry.getLastModifiedTime()); + assertEquals(TimeUtils.truncateToHundredNanos(attributes.creationTime()), entry.getCreationTime()); + assertNotNull(entry.getAccessTime()); entry = archive.getNextEntry(); - assert (entry != null); - assertEquals(entry.getName(), file2.getName()); - - assert (archive.getNextEntry() == null); + assertNotNull(entry); + assertEquals(file2.getName(), entry.getName()); + attributes = Files.readAttributes(file2.toPath(), BasicFileAttributes.class); + assertEquals(TimeUtils.truncateToHundredNanos(attributes.lastModifiedTime()), entry.getLastModifiedTime()); + assertEquals(TimeUtils.truncateToHundredNanos(attributes.creationTime()), entry.getCreationTime()); + assertNotNull(entry.getAccessTime()); + + assertNull(archive.getNextEntry()); } } diff --git a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntryTest.java b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntryTest.java index 77c9b3c1..ce8feb61 100644 --- a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntryTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZArchiveEntryTest.java @@ -96,4 +96,32 @@ public class SevenZArchiveEntryTest { assertThrows(UnsupportedOperationException.class, () -> new SevenZArchiveEntry().getLastModifiedDate()); } + @Test + public void shouldThrowIfAccessDateIsSetToNull() { + assertThrows(UnsupportedOperationException.class, () -> { + SevenZArchiveEntry entry = new SevenZArchiveEntry(); + entry.setAccessDate(null); + entry.getAccessDate(); + }); + } + + @Test + public void shouldThrowIfCreationDateIsSetToNull() { + assertThrows(UnsupportedOperationException.class, () -> { + SevenZArchiveEntry entry = new SevenZArchiveEntry(); + entry.setCreationDate(null); + entry.getCreationDate(); + }); + } + + @Test + public void shouldThrowIfLastModifiedDateIsSetToNull() { + assertThrows(UnsupportedOperationException.class, () -> { + SevenZArchiveEntry entry = new SevenZArchiveEntry(); + entry.setLastModifiedDate(null); + entry.getLastModifiedDate(); + }); + } + + } diff --git a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java index 21fe7947..1c605bbb 100644 --- a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java @@ -35,15 +35,12 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; +import java.time.Instant; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; import javax.crypto.Cipher; @@ -124,7 +121,11 @@ public class SevenZFileTest extends AbstractTestCase { @Test public void testAllEmptyFilesArchive() throws Exception { try (SevenZFile archive = new SevenZFile(getFile("7z-empty-mhc-off.7z"))) { - assertNotNull(archive.getNextEntry()); + SevenZArchiveEntry e = archive.getNextEntry(); + assertNotNull(e); + assertEquals("empty", e.getName()); + assertDates(e, "2013-05-14T17:50:19Z", null, null); + assertNull(archive.getNextEntry()); } } @@ -775,6 +776,32 @@ public class SevenZFileTest extends AbstractTestCase { } } + @Test + public void readTimesFromFile() throws IOException { + try (SevenZFile sevenZFile = new SevenZFile(getFile("times.7z"))) { + SevenZArchiveEntry entry = sevenZFile.getNextEntry(); + assertNotNull(entry); + assertEquals("test", entry.getName()); + assertTrue(entry.isDirectory()); + assertDates(entry, "2022-03-21T14:50:46.2099751Z", "2022-03-21T14:50:46.2099751Z", "2022-03-16T10:19:24.1051115Z"); + + entry = sevenZFile.getNextEntry(); + assertNotNull(entry); + assertEquals("test/test-times.txt", entry.getName()); + assertFalse(entry.isDirectory()); + assertDates(entry, "2022-03-18T10:00:15Z", "2022-03-18T10:14:37.8130002Z", "2022-03-18T10:14:37.8110032Z"); + + entry = sevenZFile.getNextEntry(); + assertNotNull(entry); + assertEquals("test/test-times2.txt", entry.getName()); + assertFalse(entry.isDirectory()); + assertDates(entry, "2022-03-18T10:00:19Z", "2022-03-18T10:14:37.8170038Z", "2022-03-18T10:14:37.8140004Z"); + + entry = sevenZFile.getNextEntry(); + assertNull(entry); + } + } + private void test7zUnarchive(final File f, final SevenZMethod m, final byte[] password) throws Exception { try (SevenZFile sevenZFile = new SevenZFile(f, password)) { @@ -797,9 +824,11 @@ public class SevenZFileTest extends AbstractTestCase { private void test7zUnarchive(final SevenZFile sevenZFile, final SevenZMethod m) throws Exception { SevenZArchiveEntry entry = sevenZFile.getNextEntry(); assertEquals("test1.xml", entry.getName()); + assertDates(entry, "2007-11-14T10:19:02Z", null, null); assertEquals(m, entry.getContentMethods().iterator().next().getMethod()); entry = sevenZFile.getNextEntry(); assertEquals("test2.xml", entry.getName()); + assertDates(entry, "2007-11-14T10:19:02Z", null, null); assertEquals(m, entry.getContentMethods().iterator().next().getMethod()); final byte[] contents = new byte[(int) entry.getSize()]; int off = 0; @@ -816,6 +845,7 @@ public class SevenZFileTest extends AbstractTestCase { try (SevenZFile sevenZFile = new SevenZFile(getFile(filename))) { final SevenZArchiveEntry entry = sevenZFile.getNextEntry(); assertEquals("Hello world.txt", entry.getName()); + assertDates(entry, "2013-05-07T19:40:48Z", null, null); final byte[] contents = new byte[(int) entry.getSize()]; int off = 0; while ((off < contents.length)) { @@ -831,4 +861,28 @@ public class SevenZFileTest extends AbstractTestCase { private static boolean isStrongCryptoAvailable() throws NoSuchAlgorithmException { return Cipher.getMaxAllowedKeyLength("AES/ECB/PKCS5Padding") >= 256; } + + private void assertDates(SevenZArchiveEntry entry, String modified, String access, String creation) { + assertDate(entry, modified, SevenZArchiveEntry::getHasLastModifiedDate, + SevenZArchiveEntry::getLastModifiedTime, SevenZArchiveEntry::getLastModifiedDate); + assertDate(entry, access, SevenZArchiveEntry::getHasAccessDate, + SevenZArchiveEntry::getAccessTime, SevenZArchiveEntry::getAccessDate); + assertDate(entry, creation, SevenZArchiveEntry::getHasCreationDate, + SevenZArchiveEntry::getCreationTime, SevenZArchiveEntry::getCreationDate); + } + + private void assertDate(SevenZArchiveEntry entry, String value, Function<SevenZArchiveEntry, Boolean> hasValue, + Function<SevenZArchiveEntry, FileTime> timeFunction, Function<SevenZArchiveEntry, Date> dateFunction) { + if (value != null) { + assertTrue(hasValue.apply(entry)); + final Instant parsedInstant = Instant.parse(value); + final FileTime parsedFileTime = FileTime.from(parsedInstant); + assertEquals(parsedFileTime, timeFunction.apply(entry)); + assertEquals(Date.from(parsedInstant), dateFunction.apply(entry)); + } else { + assertFalse(hasValue.apply(entry)); + assertThrows(UnsupportedOperationException.class, () -> timeFunction.apply(entry)); + assertThrows(UnsupportedOperationException.class, () -> dateFunction.apply(entry)); + } + } } diff --git a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java index 4dcd2d85..908951fd 100644 --- a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java @@ -23,13 +23,18 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import org.apache.commons.compress.utils.TimeUtils; +import org.junit.jupiter.api.Test; + import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Arrays; +import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -38,7 +43,6 @@ import java.util.Iterator; import org.apache.commons.compress.AbstractTestCase; import org.apache.commons.compress.utils.ByteUtils; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; -import org.junit.jupiter.api.Test; import org.tukaani.xz.LZMA2Options; public class SevenZOutputFileTest extends AbstractTestCase { @@ -67,7 +71,8 @@ public class SevenZOutputFileTest extends AbstractTestCase { public void testDirectoriesAndEmptyFiles() throws Exception { output = new File(dir, "empties.7z"); - final Date accessDate = new Date(); + final FileTime accessTime = getHundredNanosFileTime(); + final Date accessDate = new Date(accessTime.toMillis()); final Calendar cal = Calendar.getInstance(); cal.add(Calendar.HOUR, -1); final Date creationDate = cal.getTime(); @@ -80,7 +85,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { entry = new SevenZArchiveEntry(); entry.setName("foo/bar"); entry.setCreationDate(creationDate); - entry.setAccessDate(accessDate); + entry.setAccessTime(accessTime); outArchive.putArchiveEntry(entry); outArchive.write(ByteUtils.EMPTY_BYTE_ARRAY); outArchive.closeArchiveEntry(); @@ -88,7 +93,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { entry = new SevenZArchiveEntry(); entry.setName("foo/bar/boo0"); entry.setCreationDate(creationDate); - entry.setAccessDate(accessDate); + entry.setAccessTime(accessTime); outArchive.putArchiveEntry(entry); outArchive.write(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY)); outArchive.closeArchiveEntry(); @@ -96,7 +101,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { entry = new SevenZArchiveEntry(); entry.setName("foo/bar/boo1"); entry.setCreationDate(creationDate); - entry.setAccessDate(accessDate); + entry.setAccessTime(accessTime); outArchive.putArchiveEntry(entry); outArchive.write(new ByteArrayInputStream(new byte[] {'a'})); outArchive.closeArchiveEntry(); @@ -104,7 +109,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { entry = new SevenZArchiveEntry(); entry.setName("foo/bar/boo10000"); entry.setCreationDate(creationDate); - entry.setAccessDate(accessDate); + entry.setAccessTime(accessTime); outArchive.putArchiveEntry(entry); outArchive.write(new ByteArrayInputStream(new byte[10000])); outArchive.closeArchiveEntry(); @@ -112,7 +117,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { entry = new SevenZArchiveEntry(); entry.setName("foo/bar/test.txt"); entry.setCreationDate(creationDate); - entry.setAccessDate(accessDate); + entry.setAccessTime(accessTime); outArchive.putArchiveEntry(entry); outArchive.write(Paths.get("src/test/resources/test.txt")); outArchive.closeArchiveEntry(); @@ -159,6 +164,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { assertFalse(entry.isAntiItem()); assertEquals(0, entry.getSize()); assertFalse(entry.getHasLastModifiedDate()); + assertEquals(accessTime, entry.getAccessTime()); assertEquals(accessDate, entry.getAccessDate()); assertEquals(creationDate, entry.getCreationDate()); @@ -169,6 +175,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { assertFalse(entry.isAntiItem()); assertEquals(0, entry.getSize()); assertFalse(entry.getHasLastModifiedDate()); + assertEquals(accessTime, entry.getAccessTime()); assertEquals(accessDate, entry.getAccessDate()); assertEquals(creationDate, entry.getCreationDate()); @@ -179,6 +186,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { assertFalse(entry.isAntiItem()); assertEquals(1, entry.getSize()); assertFalse(entry.getHasLastModifiedDate()); + assertEquals(accessTime, entry.getAccessTime()); assertEquals(accessDate, entry.getAccessDate()); assertEquals(creationDate, entry.getCreationDate()); @@ -189,6 +197,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { assertFalse(entry.isAntiItem()); assertEquals(10000, entry.getSize()); assertFalse(entry.getHasLastModifiedDate()); + assertEquals(accessTime, entry.getAccessTime()); assertEquals(accessDate, entry.getAccessDate()); assertEquals(creationDate, entry.getCreationDate()); @@ -199,6 +208,7 @@ public class SevenZOutputFileTest extends AbstractTestCase { assertFalse(entry.isAntiItem()); assertEquals(Files.size(Paths.get("src/test/resources/test.txt")), entry.getSize()); assertFalse(entry.getHasLastModifiedDate()); + assertEquals(accessTime, entry.getAccessTime()); assertEquals(accessDate, entry.getAccessDate()); assertEquals(creationDate, entry.getCreationDate()); @@ -237,6 +247,16 @@ public class SevenZOutputFileTest extends AbstractTestCase { } + private FileTime getHundredNanosFileTime() { + final Instant now = Instant.now(); + // In some platforms, Java's Instant has a precision of milliseconds. + // Add some nanos at the end to test 100ns intervals. + final FileTime fileTime = FileTime.from(Instant.ofEpochSecond(now.getEpochSecond(), now.getNano() + 999900)); + // However, in some platforms, Java's Instant has a precision of nanoseconds. + // Truncate the resulting FileTime to 100ns intervals. + return TimeUtils.truncateToHundredNanos(fileTime); + } + @Test public void testDirectoriesOnly() throws Exception { output = new File(dir, "dirs.7z"); diff --git a/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java b/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java index 6521f9bb..55ee1220 100644 --- a/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java @@ -20,6 +20,8 @@ package org.apache.commons.compress.archivers.zip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Date; import org.junit.jupiter.api.Test; @@ -41,4 +43,44 @@ public class X000A_NTFSTest { assertEquals(new Date(-11644473601000L), xf2.getAccessJavaTime()); assertNull(xf2.getCreateJavaTime()); } + + @Test + public void simpleRountripWithHighPrecisionDatesWithSmallValues() throws Exception { + final X000A_NTFS xf = new X000A_NTFS(); + // The last 2 digits should not be written due to the 100ns precision + xf.setModifyFileTime(FileTime.from(Instant.ofEpochSecond(0, 1234))); + // one second past midnight + xf.setAccessFileTime(FileTime.from(Instant.ofEpochSecond(-11644473601L))); + xf.setCreateFileTime(null); + final byte[] b = xf.getLocalFileDataData(); + + final X000A_NTFS xf2 = new X000A_NTFS(); + xf2.parseFromLocalFileData(b, 0, b.length); + assertEquals(FileTime.from(Instant.ofEpochSecond(0, 1200)), xf2.getModifyFileTime()); + assertEquals(new Date(0), xf2.getModifyJavaTime()); + assertEquals(FileTime.from(Instant.ofEpochSecond(-11644473601L)), xf2.getAccessFileTime()); + assertEquals(new Date(-11644473601000L), xf2.getAccessJavaTime()); + assertNull(xf2.getCreateFileTime()); + assertNull(xf2.getCreateJavaTime()); + } + + @Test + public void simpleRountripWithHighPrecisionDatesWithBigValues() throws Exception { + final X000A_NTFS xf = new X000A_NTFS(); + xf.setModifyFileTime(FileTime.from(Instant.ofEpochSecond(123456789101L, 123456700))); + // one second past midnight + xf.setAccessFileTime(FileTime.from(Instant.ofEpochSecond(-11644473601L))); + // 765432100ns past midnight + xf.setCreateFileTime(FileTime.from(Instant.ofEpochSecond(-11644473600L, 765432100))); + final byte[] b = xf.getLocalFileDataData(); + + final X000A_NTFS xf2 = new X000A_NTFS(); + xf2.parseFromLocalFileData(b, 0, b.length); + assertEquals(FileTime.from(Instant.ofEpochSecond(123456789101L, 123456700)), xf2.getModifyFileTime()); + assertEquals(new Date(123456789101123L), xf2.getModifyJavaTime()); + assertEquals(FileTime.from(Instant.ofEpochSecond(-11644473601L)), xf2.getAccessFileTime()); + assertEquals(new Date(-11644473601000L), xf2.getAccessJavaTime()); + assertEquals(FileTime.from(Instant.ofEpochSecond(-11644473600L, 765432100)), xf2.getCreateFileTime()); + assertEquals(new Date(-11644473599235L).toInstant(), xf2.getCreateJavaTime().toInstant()); + } } diff --git a/src/test/java/org/apache/commons/compress/utils/TimeUtilsTest.java b/src/test/java/org/apache/commons/compress/utils/TimeUtilsTest.java new file mode 100644 index 00000000..455fefbc --- /dev/null +++ b/src/test/java/org/apache/commons/compress/utils/TimeUtilsTest.java @@ -0,0 +1,181 @@ +/* + * 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.utils; + +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Stream; + +import static org.apache.commons.compress.utils.TimeUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TimeUtilsTest { + + public static Stream<Arguments> dateToNtfsProvider() { + return Stream.of( + Arguments.of("1601-01-01T00:00:00.000Z", 0), + Arguments.of("1601-01-01T00:00:00.000Z", 1), + Arguments.of("1600-12-31T23:59:59.999Z", -1), + Arguments.of("1601-01-01T00:00:00.001Z", HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1601-01-01T00:00:00.001Z", HUNDRED_NANOS_PER_MILLISECOND + 1), + Arguments.of("1601-01-01T00:00:00.000Z", HUNDRED_NANOS_PER_MILLISECOND - 1), + Arguments.of("1600-12-31T23:59:59.999Z", -HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1600-12-31T23:59:59.999Z", -HUNDRED_NANOS_PER_MILLISECOND + 1), + Arguments.of("1600-12-31T23:59:59.998Z", -HUNDRED_NANOS_PER_MILLISECOND - 1), + Arguments.of("1970-01-01T00:00:00.000Z", -WINDOWS_EPOCH_OFFSET), + Arguments.of("1970-01-01T00:00:00.000Z", -WINDOWS_EPOCH_OFFSET + 1), + Arguments.of("1970-01-01T00:00:00.001Z", -WINDOWS_EPOCH_OFFSET + HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1969-12-31T23:59:59.999Z", -WINDOWS_EPOCH_OFFSET - 1), + Arguments.of("1969-12-31T23:59:59.999Z", -WINDOWS_EPOCH_OFFSET - HUNDRED_NANOS_PER_MILLISECOND) + ); + } + + public static Stream<Arguments> fileTimeToNtfsProvider() { + return Stream.of( + Arguments.of("1601-01-01T00:00:00.0000000Z", 0), + Arguments.of("1601-01-01T00:00:00.0000001Z", 1), + Arguments.of("1600-12-31T23:59:59.9999999Z", -1), + Arguments.of("1601-01-01T00:00:00.0010000Z", HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1601-01-01T00:00:00.0010001Z", HUNDRED_NANOS_PER_MILLISECOND + 1), + Arguments.of("1601-01-01T00:00:00.0009999Z", HUNDRED_NANOS_PER_MILLISECOND - 1), + Arguments.of("1600-12-31T23:59:59.9990000Z", -HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1600-12-31T23:59:59.9990001Z", -HUNDRED_NANOS_PER_MILLISECOND + 1), + Arguments.of("1600-12-31T23:59:59.9989999Z", -HUNDRED_NANOS_PER_MILLISECOND - 1), + Arguments.of("1970-01-01T00:00:00.0000000Z", -WINDOWS_EPOCH_OFFSET), + Arguments.of("1970-01-01T00:00:00.0000001Z", -WINDOWS_EPOCH_OFFSET + 1), + Arguments.of("1970-01-01T00:00:00.0010000Z", -WINDOWS_EPOCH_OFFSET + HUNDRED_NANOS_PER_MILLISECOND), + Arguments.of("1969-12-31T23:59:59.9999999Z", -WINDOWS_EPOCH_OFFSET - 1), + Arguments.of("1969-12-31T23:59:59.9990000Z", -WINDOWS_EPOCH_OFFSET - HUNDRED_NANOS_PER_MILLISECOND) + ); + } + + @ParameterizedTest + @MethodSource("dateToNtfsProvider") + public void shouldConvertNtfsTimeToDate(final String instant, final long ntfsTime) { + final Date converted = ntfsTimeToDate(ntfsTime); + assertEquals(Instant.parse(instant), converted.toInstant()); + // ensuring the deprecated method still works + assertEquals(converted, SevenZArchiveEntry.ntfsTimeToJavaTime(ntfsTime)); + } + + @ParameterizedTest + @MethodSource("dateToNtfsProvider") + public void shouldConvertDateToNtfsTime(final String instant, final long ntfsTime) { + final long ntfsMillis = Math.floorDiv(ntfsTime, HUNDRED_NANOS_PER_MILLISECOND) * HUNDRED_NANOS_PER_MILLISECOND; + final Date parsed = Date.from(Instant.parse(instant)); + final long converted = dateToNtfsTime(parsed); + assertEquals(ntfsMillis, converted); + // ensuring the deprecated method still works + assertEquals(converted, SevenZArchiveEntry.javaTimeToNtfsTime(parsed)); + } + + @ParameterizedTest + @MethodSource("fileTimeToNtfsProvider") + public void shouldConvertFileTimeToNtfsTime(final String instant, final long ntfsTime) { + final FileTime parsed = FileTime.from(Instant.parse(instant)); + assertEquals(ntfsTime, fileTimeToNtfsTime(parsed)); + } + + @ParameterizedTest + @MethodSource("fileTimeToNtfsProvider") + public void shouldConvertNtfsTimeToFileTime(final String instant, final long ntfsTime) { + final FileTime parsed = FileTime.from(Instant.parse(instant)); + assertEquals(parsed, ntfsTimeToFileTime(ntfsTime)); + } + + @Test + public void shouldConvertNullDateToNullFileTime() { + assertNull(dateToFileTime(null)); + } + + @Test + public void shouldConvertNullFileTimeToNullDate() { + assertNull(fileTimeToDate(null)); + } + + @ParameterizedTest + @MethodSource("dateToNtfsProvider") + public void shouldConvertDateToFileTime(final String instant, final long ignored) { + final Instant parsedInstant = Instant.parse(instant); + final FileTime parsedFileTime = FileTime.from(parsedInstant); + final Date parsedDate = Date.from(parsedInstant); + assertEquals(parsedFileTime, dateToFileTime(parsedDate)); + } + + @ParameterizedTest + @MethodSource("fileTimeToNtfsProvider") + public void shouldConvertFileTimeToDate(final String instant, final long ignored) { + final Instant parsedInstant = Instant.parse(instant); + final FileTime parsedFileTime = FileTime.from(parsedInstant); + final Date parsedDate = Date.from(parsedInstant); + assertEquals(parsedDate, fileTimeToDate(parsedFileTime)); + } + + public static Stream<Arguments> truncateFileTimeProvider() { + return Stream.of( + Arguments.of( + "2022-05-10T18:25:33.123456789Z", + "2022-05-10T18:25:33.1234567Z" + ), + Arguments.of( + "1970-01-01T00:00:00.000000001Z", + "1970-01-01T00:00:00.0000000Z" + ), + Arguments.of( + "1970-01-01T00:00:00.000000010Z", + "1970-01-01T00:00:00.0000000Z" + ), + Arguments.of( + "1970-01-01T00:00:00.000000199Z", + "1970-01-01T00:00:00.0000001Z" + ), + Arguments.of( + "1969-12-31T23:59:59.999999999Z", + "1969-12-31T23:59:59.9999999Z" + ), + Arguments.of( + "1969-12-31T23:59:59.000000001Z", + "1969-12-31T23:59:59.0000000Z" + ), + Arguments.of( + "1969-12-31T23:59:59.000000010Z", + "1969-12-31T23:59:59.0000000Z" + ), + Arguments.of( + "1969-12-31T23:59:59.000000199Z", + "1969-12-31T23:59:59.0000001Z" + ) + ); + } + + @ParameterizedTest + @MethodSource("truncateFileTimeProvider") + public void shouldTruncateFileTimeToHundredNanos(final String original, final String truncated) { + final FileTime originalTime = FileTime.from(Instant.parse(original)); + final FileTime truncatedTime = FileTime.from(Instant.parse(truncated)); + assertEquals(truncatedTime, TimeUtils.truncateToHundredNanos(originalTime)); + } +} diff --git a/src/test/resources/times.7z b/src/test/resources/times.7z new file mode 100644 index 00000000..5e2910ce Binary files /dev/null and b/src/test/resources/times.7z differ