diff --git a/base/src/main/java/proguard/io/DataEntry.java b/base/src/main/java/proguard/io/DataEntry.java
index 420380c28..ed8fbe380 100644
--- a/base/src/main/java/proguard/io/DataEntry.java
+++ b/base/src/main/java/proguard/io/DataEntry.java
@@ -45,6 +45,14 @@ public interface DataEntry
*/
public long getSize();
+ /**
+ * Returns the modification time of this data entry in milliseconds,
+ * since the epoch (1970-01-01T00:00:00Z), or -1 if unknown.
+ */
+ public default long getModificationTime()
+ {
+ return -1;
+ }
/**
* Returns whether the data entry represents a directory.
diff --git a/base/src/main/java/proguard/io/FileDataEntry.java b/base/src/main/java/proguard/io/FileDataEntry.java
index 11b4992ec..78b93a425 100644
--- a/base/src/main/java/proguard/io/FileDataEntry.java
+++ b/base/src/main/java/proguard/io/FileDataEntry.java
@@ -107,6 +107,13 @@ public long getSize()
}
+ @Override
+ public long getModificationTime()
+ {
+ return file.lastModified();
+ }
+
+
@Override
public boolean isDirectory()
{
diff --git a/base/src/main/java/proguard/io/NamedDataEntry.java b/base/src/main/java/proguard/io/NamedDataEntry.java
index 8989b1db7..440dbc179 100644
--- a/base/src/main/java/proguard/io/NamedDataEntry.java
+++ b/base/src/main/java/proguard/io/NamedDataEntry.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.io.InputStream;
+import java.time.Instant;
/**
* This DataEntry
represents a named output entry with a parent.
diff --git a/base/src/main/java/proguard/io/WrappedDataEntry.java b/base/src/main/java/proguard/io/WrappedDataEntry.java
index 7418dd795..465df35a4 100644
--- a/base/src/main/java/proguard/io/WrappedDataEntry.java
+++ b/base/src/main/java/proguard/io/WrappedDataEntry.java
@@ -63,6 +63,13 @@ public long getSize()
}
+ @Override
+ public long getModificationTime()
+ {
+ return wrappedEntry.getModificationTime();
+ }
+
+
@Override
public boolean isDirectory()
{
diff --git a/base/src/main/java/proguard/io/ZipDataEntry.java b/base/src/main/java/proguard/io/ZipDataEntry.java
index 0746ff419..c32c7ef4f 100644
--- a/base/src/main/java/proguard/io/ZipDataEntry.java
+++ b/base/src/main/java/proguard/io/ZipDataEntry.java
@@ -78,6 +78,12 @@ public long getSize()
}
+ @Override
+ public long getModificationTime()
+ {
+ return zipEntry.getLastModifiedTime().toMillis();
+ }
+
@Override
public boolean isDirectory()
{
diff --git a/base/src/main/java/proguard/io/ZipFileDataEntry.java b/base/src/main/java/proguard/io/ZipFileDataEntry.java
index 3ab4960d2..623d523ff 100644
--- a/base/src/main/java/proguard/io/ZipFileDataEntry.java
+++ b/base/src/main/java/proguard/io/ZipFileDataEntry.java
@@ -79,6 +79,13 @@ public long getSize()
}
+ @Override
+ public long getModificationTime()
+ {
+ return zipEntry.getLastModifiedTime().toMillis();
+ }
+
+
@Override
public boolean isDirectory()
{
diff --git a/base/src/main/java/proguard/io/ZipWriter.java b/base/src/main/java/proguard/io/ZipWriter.java
index c2aeaf2ab..ebc7ce73e 100644
--- a/base/src/main/java/proguard/io/ZipWriter.java
+++ b/base/src/main/java/proguard/io/ZipWriter.java
@@ -17,10 +17,14 @@
*/
package proguard.io;
+import java.time.Instant;
+import java.time.LocalDateTime;
+
import proguard.classfile.TypeConstants;
import proguard.util.StringMatcher;
import java.io.*;
+import java.time.ZoneId;
/**
* This {@link DataEntryWriter} sends data entries to the zip files specified by
@@ -53,7 +57,7 @@ public ZipWriter(DataEntryWriter dataEntryWriter)
this(null,
1,
false,
- 0,
+ -1,
dataEntryWriter);
}
@@ -66,7 +70,8 @@ public ZipWriter(DataEntryWriter dataEntryWriter)
* uncompressed entries.
* @param useZip64 Whether to write out the archive in zip64 format.
* @param modificationTime the modification date and time of the zip
- * entries, in DOS format.
+ * entries, in DOS format. If {@code -1}, the modification time
+ * of each {@link DataEntry} will be passed through.
* @param dataEntryWriter the data entry writer that can provide
* output streams for the zip archives.
*/
@@ -98,7 +103,8 @@ public ZipWriter(StringMatcher uncompressedFilter,
* @param extraUncompressedAlignment the desired alignment for the data of
* entries matching extraAlignmentFilter.
* @param modificationTime the modification date and time of the zip
- * entries, in DOS format.
+ * entries, in DOS format. If {@code -1}, the modification time
+ * of each {@link DataEntry} will be passed through.
* @param dataEntryWriter the data entry writer that can provide
* output streams for the zip archives.
*/
@@ -129,7 +135,8 @@ public ZipWriter(StringMatcher uncompressedFilter,
* uncompressed entries.
* @param useZip64 Whether to write out the archive in zip64 format.
* @param modificationTime the modification date and time of the zip
- * entries, in DOS format.
+ * entries, in DOS format. If {@code -1}, the modification time
+ * of each {@link DataEntry} will be passed through.
* @param dataEntryWriter the data entry writer that can provide
* output streams for the zip archives.
*/
@@ -161,7 +168,8 @@ public ZipWriter(StringMatcher uncompressedFilter,
* @param extraUncompressedAlignment the desired alignment for the data of
* entries matching extraAlignmentFilter.
* @param modificationTime the modification date and time of the zip
- * entries, in DOS format.
+ * entries, in DOS format. If {@code -1}, the modification time
+ * of each {@link DataEntry} will be passed through.
* @param dataEntryWriter the data entry writer that can provide
* output streams for the zip archives.
*/
@@ -195,7 +203,8 @@ public ZipWriter(StringMatcher uncompressedFilter,
* @param extraUncompressedAlignment the desired alignment for the data of
* entries matching extraAlignmentFilter.
* @param modificationTime the modification date and time of the zip
- * entries, in DOS format.
+ * entries, in DOS format. If {@code -1}, the modification time
+ * of each {@link DataEntry} will be passed through.
* @param dataEntryWriter the data entry writer that can provide
* output streams for the zip archives.
*/
@@ -240,13 +249,12 @@ public boolean createDirectory(DataEntry dataEntry) throws IOException
OutputStream outputStream =
currentZipOutput.createOutputStream(name,
false,
- modificationTime);
+ getDosModificationTime(dataEntry));
outputStream.close();
return true;
}
-
@Override
public boolean sameOutputStream(DataEntry dataEntry1,
DataEntry dataEntry2)
@@ -290,7 +298,31 @@ public OutputStream createOutputStream(DataEntry dataEntry) throws IOException
currentZipOutput.createOutputStream(name,
compress1 && compress2,
uncompressedAlignment,
- modificationTime);
+ getDosModificationTime(dataEntry));
+ }
+
+
+ /**
+ * Returns the modification time of a {@link DataEntry} in dos format.
+ *
+ * If {@link ZipWriter#modificationTime} is -1, the modification time of the + * {@link DataEntry} will be used, otherwise {@link ZipWriter#modificationTime} + * will be used for all encountered {@link DataEntry}s. + *
+ * If a {@link DataEntry} has no valid modification time, the {@link Instant#now()} + * will be assumed. + */ + private int getDosModificationTime(DataEntry dataEntry) + { + int dosModificationTime = modificationTime; + if (dosModificationTime == -1) { + long dataEntryModificationTime = dataEntry.getModificationTime(); + if (dataEntryModificationTime == -1) { + dataEntryModificationTime = Instant.now().toEpochMilli(); + } + dosModificationTime = (int) javaToDosTime(dataEntryModificationTime); + } + return dosModificationTime; } @@ -378,4 +410,31 @@ private void finish() throws IOException currentZipOutput = null; } } + + /* + * Utility method for conversion of a java time to dos time. + * Copied from + * https://github.com/apache/commons-compress/blob/master/src/main/java/org/apache/commons/compress/archivers/zip/ZipUtil.java + * + * Licenced under Apache Licence v2. + */ + private static final long DOSTIME_BEFORE_1980 = 1 << 21 | 1 << 16; // 0x210000 + + // version with integer overflow fixed - see https://bugs.openjdk.org/browse/JDK-8130914 + private static long javaToDosTime(final long t) + { + final LocalDateTime ldt = + LocalDateTime.ofInstant(Instant.ofEpochMilli(t), ZoneId.systemDefault()); + + if (ldt.getYear() < 1980) { + return DOSTIME_BEFORE_1980; + } + + return (ldt.getYear() - 1980 << 25 + | ldt.getMonthValue() << 21 + | ldt.getDayOfMonth() << 16 + | ldt.getHour() << 11 + | ldt.getMinute() << 5 + | ldt.getSecond() >> 1) & 0xffffffffL; + } } diff --git a/base/src/test/kotlin/proguard/io/ZipWriterTest.kt b/base/src/test/kotlin/proguard/io/ZipWriterTest.kt new file mode 100644 index 000000000..11724b7ab --- /dev/null +++ b/base/src/test/kotlin/proguard/io/ZipWriterTest.kt @@ -0,0 +1,102 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + */ + +package proguard.io + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import java.io.* +import java.nio.file.attribute.FileTime +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + + +class ZipWriterTest : FreeSpec({ + + "Given a zip input file" - { + val time = 1700853925000 + //val time = Instant.now().toEpochMilli() + + // Truncate to seconds at most, as the dos time has a maximum granularity of seconds. + val modificationTime = Instant.ofEpochMilli(time).truncatedTo(ChronoUnit.SECONDS) + val zipArchiveAsByteArray = createZipArchive(modificationTime) + + val dataEntry = + StreamingDataEntry("zipArchive", + ByteArrayInputStream(zipArchiveAsByteArray.toByteArray())) + + "When using a pass-through ZipWriter" - { + val os = ByteArrayOutputStream() + + val writer = ZipWriter( + FixedOutputStreamWriter(os)) + + val dataEntryReader = + JarReader( + DataEntryCopier( + writer)) + + dataEntryReader.read(dataEntry) + + writer.close() + + "Then the resulting modification time should be the same as the original one" { + getModificationTime(ByteArrayInputStream(os.toByteArray()))?.toEpochMilli() shouldBe modificationTime.toEpochMilli() + } + } + } +}) { + companion object { + fun createZipArchive(modificationTime: Instant): ByteArrayOutputStream { + val baos = ByteArrayOutputStream() + baos.use { + val zipOutputStream = ZipOutputStream(baos) + val entry = ZipEntry("test.txt") + entry.setLastModifiedTime(FileTime.fromMillis(modificationTime.toEpochMilli())) + zipOutputStream.putNextEntry(entry) + zipOutputStream.closeEntry() + } + return baos + } + + fun getModificationTime(inputStream: InputStream): Instant? { + var modificationTime: Instant? = null + + val dataEntry = StreamingDataEntry("zipArchive", inputStream) + + val dataEntryReader = + JarReader { + modificationTime = Instant.ofEpochMilli(it.modificationTime) + } + + dataEntryReader.read(dataEntry) + return modificationTime + } + } +} + +class FixedOutputStreamWriter (private val os: OutputStream) : DataEntryWriter { + override fun createDirectory(dataEntry: DataEntry): Boolean { + return true + } + + override fun sameOutputStream(dataEntry1: DataEntry, dataEntry2: DataEntry): Boolean { + return true + } + + override fun createOutputStream(dataEntry: DataEntry): OutputStream { + return os + } + + override fun close() { + os.close() + } + + override fun println(pw: PrintWriter, prefix: String) { + pw.println(prefix + "FixedOutputStreamWriter ()") + } +} diff --git a/examples/src/main/java/proguard/examples/JarSigner.java b/examples/src/main/java/proguard/examples/JarSigner.java new file mode 100644 index 000000000..8f6eb0f1a --- /dev/null +++ b/examples/src/main/java/proguard/examples/JarSigner.java @@ -0,0 +1,131 @@ +package proguard.examples; + +import proguard.io.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; + +/** + * This sample application illustrates how to sign jars with the ProGuardCORE API. + *
+ * Usage:
+ * java proguard.examples.JarSigner -ks