8075526: Need a way to read and write ZipEntry timestamp using local date/time without tz conversion
authorsherman
Wed, 22 Jul 2015 21:43:33 +0000
changeset 31819 ba7cde5a611a
parent 31818 9bed5f752a87
child 31820 f8214072b376
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
jdk/src/java.base/share/classes/java/util/zip/ZipEntry.java
jdk/src/java.base/share/classes/java/util/zip/ZipUtils.java
jdk/test/java/util/zip/TestExtraTime.java
jdk/test/java/util/zip/TestLocalTime.java
--- 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!");
+        }
+    }
+}