8075526: Need a way to read and write ZipEntry timestamp using local date/time without tz conversion
Summary: to add a pair of set/getTimeLocal()
Reviewed-by: ksrini, rriggs
--- a/jdk/src/java.base/share/classes/java/util/zip/ZipEntry.java Wed Jul 22 11:08:35 2015 +0300
+++ b/jdk/src/java.base/share/classes/java/util/zip/ZipEntry.java Wed Jul 22 21:43:33 2015 +0000
@@ -29,6 +29,9 @@
import java.nio.file.attribute.FileTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
+import java.time.ZoneId;
import static java.util.zip.ZipConstants64.*;
@@ -195,6 +198,85 @@
}
/**
+ * Sets the last modification time of the entry in local date-time.
+ *
+ * <p> If the entry is output to a ZIP file or ZIP file formatted
+ * output stream the last modification time set by this method will
+ * be stored into the {@code date and time fields} of the zip file
+ * entry and encoded in standard {@code MS-DOS date and time format}.
+ * If the date-time set is out of the range of the standard {@code
+ * MS-DOS date and time format}, the time will also be stored into
+ * zip file entry's extended timestamp fields in {@code optional
+ * extra data} in UTC time. The {@link java.time.ZoneId#systemDefault()
+ * system default TimeZone} is used to convert the local date-time
+ * to UTC time.
+ *
+ * <p> {@code LocalDateTime} uses a precision of nanoseconds, whereas
+ * this class uses a precision of milliseconds. The conversion will
+ * truncate any excess precision information as though the amount in
+ * nanoseconds was subject to integer division by one million.
+ *
+ * @param time
+ * The last modification time of the entry in local date-time
+ *
+ * @see #getTimeLocal()
+ * @since 1.9
+ */
+ public void setTimeLocal(LocalDateTime time) {
+ int year = time.getYear() - 1980;
+ if (year < 0) {
+ this.xdostime = DOSTIME_BEFORE_1980;
+ } else {
+ this.xdostime = (year << 25 |
+ time.getMonthValue() << 21 |
+ time.getDayOfMonth() << 16 |
+ time.getHour() << 11 |
+ time.getMinute() << 5 |
+ time.getSecond() >> 1)
+ + ((long)(((time.getSecond() & 0x1) * 1000) +
+ time.getNano() / 1000_000) << 32);
+ }
+ if (xdostime != DOSTIME_BEFORE_1980 && year <= 0x7f) {
+ this.mtime = null;
+ } else {
+ this.mtime = FileTime.from(
+ ZonedDateTime.of(time, ZoneId.systemDefault()).toInstant());
+ }
+ }
+
+ /**
+ * Returns the last modification time of the entry in local date-time.
+ *
+ * <p> If the entry is read from a ZIP file or ZIP file formatted
+ * input stream, this is the last modification time from the zip
+ * file entry's {@code optional extra data} if the extended timestamp
+ * fields are present. Otherwise, the last modification time is read
+ * from entry's standard MS-DOS formatted {@code date and time fields}.
+ *
+ * <p> The {@link java.time.ZoneId#systemDefault() system default TimeZone}
+ * is used to convert the UTC time to local date-time.
+ *
+ * @return The last modification time of the entry in local date-time
+ *
+ * @see #setTimeLocal(LocalDateTime)
+ * @since 1.9
+ */
+ public LocalDateTime getTimeLocal() {
+ if (mtime != null) {
+ return LocalDateTime.ofInstant(mtime.toInstant(), ZoneId.systemDefault());
+ }
+ int ms = (int)(xdostime >> 32);
+ return LocalDateTime.of((int)(((xdostime >> 25) & 0x7f) + 1980),
+ (int)((xdostime >> 21) & 0x0f),
+ (int)((xdostime >> 16) & 0x1f),
+ (int)((xdostime >> 11) & 0x1f),
+ (int)((xdostime >> 5) & 0x3f),
+ (int)((xdostime << 1) & 0x3e) + ms / 1000,
+ (ms % 1000) * 1000_000);
+ }
+
+
+ /**
* Sets the last modification time of the entry.
*
* <p> When output to a ZIP file or ZIP file formatted output stream
@@ -498,15 +580,15 @@
// flag its presence or absence. But if mtime is present
// in LOC it must be present in CEN as well.
if ((flag & 0x1) != 0 && (sz0 + 4) <= sz) {
- mtime = unixTimeToFileTime(get32(extra, off + sz0));
+ mtime = unixTimeToFileTime(get32S(extra, off + sz0));
sz0 += 4;
}
if ((flag & 0x2) != 0 && (sz0 + 4) <= sz) {
- atime = unixTimeToFileTime(get32(extra, off + sz0));
+ atime = unixTimeToFileTime(get32S(extra, off + sz0));
sz0 += 4;
}
if ((flag & 0x4) != 0 && (sz0 + 4) <= sz) {
- ctime = unixTimeToFileTime(get32(extra, off + sz0));
+ ctime = unixTimeToFileTime(get32S(extra, off + sz0));
sz0 += 4;
}
break;
--- a/jdk/src/java.base/share/classes/java/util/zip/ZipUtils.java Wed Jul 22 11:08:35 2015 +0300
+++ b/jdk/src/java.base/share/classes/java/util/zip/ZipUtils.java Wed Jul 22 21:43:33 2015 +0000
@@ -144,4 +144,13 @@
public static final long get64(byte b[], int off) {
return get32(b, off) | (get32(b, off+4) << 32);
}
+
+ /**
+ * Fetches signed 32-bit value from byte array at specified offset.
+ * The bytes are assumed to be in Intel (little-endian) byte order.
+ *
+ */
+ public static final int get32S(byte b[], int off) {
+ return (get16(b, off) | (get16(b, off+2) << 16));
+ }
}
--- a/jdk/test/java/util/zip/TestExtraTime.java Wed Jul 22 11:08:35 2015 +0300
+++ b/jdk/test/java/util/zip/TestExtraTime.java Wed Jul 22 21:43:33 2015 +0000
@@ -23,7 +23,7 @@
/**
* @test
- * @bug 4759491 6303183 7012868 8015666 8023713 8068790 8076641
+ * @bug 4759491 6303183 7012868 8015666 8023713 8068790 8076641 8075526
* @summary Test ZOS and ZIS timestamp in extra field correctly
*/
@@ -54,8 +54,12 @@
for (byte[] extra : new byte[][] { null, new byte[] {1, 2, 3}}) {
test(mtime, null, null, null, extra);
+
// ms-dos 1980 epoch problem
test(FileTime.from(10, TimeUnit.MILLISECONDS), null, null, null, extra);
+ // negative epoch time
+ test(FileTime.from(-100, TimeUnit.DAYS), null, null, null, extra);
+
// non-default tz
test(mtime, null, null, tz, extra);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/zip/TestLocalTime.java Wed Jul 22 21:43:33 2015 +0000
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8075526
+ * @summary Test timestamp via ZipEntry.get/setTimeLocal()
+ */
+
+import java.io.*;
+import java.nio.file.*;
+import java.time.*;
+import java.util.*;
+import java.util.zip.*;
+
+public class TestLocalTime {
+ private static TimeZone tz0 = TimeZone.getDefault();
+
+ public static void main(String[] args) throws Throwable{
+ try {
+ LocalDateTime ldt = LocalDateTime.now();
+ test(getBytes(ldt), ldt); // now
+ ldt = ldt.withYear(1968); test(getBytes(ldt), ldt);
+ ldt = ldt.withYear(1970); test(getBytes(ldt), ldt);
+ ldt = ldt.withYear(1982); test(getBytes(ldt), ldt);
+ ldt = ldt.withYear(2037); test(getBytes(ldt), ldt);
+ ldt = ldt.withYear(2100); test(getBytes(ldt), ldt);
+ ldt = ldt.withYear(2106); test(getBytes(ldt), ldt);
+
+ TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
+ // dos time does not support < 1980, have to use
+ // utc in mtime.
+ testWithTZ(tz, ldt.withYear(1982));
+ testWithTZ(tz, ldt.withYear(2037));
+ testWithTZ(tz, ldt.withYear(2100));
+ testWithTZ(tz, ldt.withYear(2106));
+ } finally {
+ TimeZone.setDefault(tz0);
+ }
+ }
+
+ static byte[] getBytes(LocalDateTime mtime) throws Throwable {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ZipOutputStream zos = new ZipOutputStream(baos);
+ ZipEntry ze = new ZipEntry("TestLocalTime.java");
+ ze.setTimeLocal(mtime);
+ check(ze, mtime);
+
+ zos.putNextEntry(ze);
+ zos.write(new byte[] { 1, 2, 3, 4});
+ zos.close();
+ return baos.toByteArray();
+ }
+
+ static void testWithTZ(TimeZone tz, LocalDateTime ldt) throws Throwable {
+ TimeZone.setDefault(tz);
+ byte[] zbytes = getBytes(ldt);
+ TimeZone.setDefault(tz0);
+ test(zbytes, ldt);
+ }
+
+ static void test(byte[] zbytes, LocalDateTime expected) throws Throwable {
+ System.out.printf("--------------------%nTesting: [%s]%n", expected);
+ // ZipInputStream
+ ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zbytes));
+ ZipEntry ze = zis.getNextEntry();
+ zis.close();
+ check(ze, expected);
+
+ // ZipFile
+ Path zpath = Paths.get(System.getProperty("test.dir", "."),
+ "TestLocalTime.zip");
+ try {
+ Files.copy(new ByteArrayInputStream(zbytes), zpath);
+ ZipFile zf = new ZipFile(zpath.toFile());
+ ze = zf.getEntry("TestLocalTime.java");
+ check(ze, expected);
+ zf.close();
+ } finally {
+ Files.deleteIfExists(zpath);
+ }
+ }
+
+ static void check(ZipEntry ze, LocalDateTime expected) {
+ LocalDateTime ldt = ze.getTimeLocal();
+ if (ldt.atOffset(ZoneOffset.UTC).toEpochSecond() >> 1
+ != expected.atOffset(ZoneOffset.UTC).toEpochSecond() >> 1) {
+ throw new RuntimeException("Timestamp: storing mtime failed!");
+ }
+ }
+}