--- old/src/java.base/share/classes/java/util/zip/ZipEntry.java 2015-07-01 15:17:16.000000000 -0700 +++ new/src/java.base/share/classes/java/util/zip/ZipEntry.java 2015-07-01 15:17:16.000000000 -0700 @@ -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. + * + *

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. + * + *

{@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. + * + *

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}. + * + *

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. * *

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; --- old/src/java.base/share/classes/java/util/zip/ZipUtils.java 2015-07-01 15:17:18.000000000 -0700 +++ new/src/java.base/share/classes/java/util/zip/ZipUtils.java 2015-07-01 15:17:17.000000000 -0700 @@ -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)); + } } --- old/test/java/util/zip/TestExtraTime.java 2015-07-01 15:17:19.000000000 -0700 +++ new/test/java/util/zip/TestExtraTime.java 2015-07-01 15:17:19.000000000 -0700 @@ -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 2015-06-18 09:30:42.886371318 -0700 +++ new/test/java/util/zip/TestLocalTime.java 2015-07-01 15:17:20.000000000 -0700 @@ -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!"); + } + } +}