/* * Copyright (c) 2021, 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 * @requires vm.bits == 64 * @bug 8276766 * @summary Test jar --date source date of entries and that jars are * reproducible * @modules jdk.jartool * @run testng/othervm ReproducibleJar */ import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.testng.annotations.DataProvider; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.attribute.FileTime; import java.util.Date; import java.util.TimeZone; import java.util.spi.ToolProvider; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.Instant; import java.util.concurrent.TimeUnit; public class ReproducibleJar { private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar") .orElseThrow(() -> new RuntimeException("jar tool not found") ); // ZipEntry's mod date has 2 seconds precision: give extra time to // allow for e.g. rounding/truncation and networked/samba drives. private static final long PRECISION = 10000L; private static final TimeZone TZ = TimeZone.getDefault(); private static final boolean DST = TZ.inDaylightTime(new Date()); private static final String UNIX_2038_ROLLOVER_TIME = "2038-01-19T03:14:07Z"; private static final Instant UNIX_2038_ROLLOVER = Instant.parse(UNIX_2038_ROLLOVER_TIME); private static final File DIR_OUTER = new File("outer"); private static final File DIR_INNER = new File(DIR_OUTER, "inner"); private static final File FILE_INNER = new File(DIR_INNER, "foo.txt"); private static final File JAR_FILE_SOURCE_DATE1 = new File("JarEntryTimeSourceDate1.jar"); private static final File JAR_FILE_SOURCE_DATE2 = new File("JarEntryTimeSourceDate2.jar"); // Valid --date values for jar @DataProvider private Object[][] validSourceDates() { return new Object[][]{ {"1980-01-01T00:00:02+00:00"}, {"1986-06-24T01:02:03+00:00"}, {"2022-03-15T00:00:00+00:00"}, {"2022-03-15T00:00:00+06:00"}, {"2021-12-25T09:30:00-08:00[America/Los_Angeles]"}, {"2021-12-31T23:59:59Z"}, {"2024-06-08T14:24Z"}, {"2026-09-24T16:26-05:00"}, {"2038-11-26T06:06:06+00:00"}, {"2098-02-18T00:00:00-08:00"}, {"2099-12-31T23:59:59+00:00"} }; } // Invalid --date values for jar @DataProvider private Object[][] invalidSourceDates() { return new Object[][]{ {"1976-06-24T01:02:03+00:00"}, {"1980-01-01T00:00:01+00:00"}, {"2100-01-01T00:00:00+00:00"}, {"2138-02-18T00:00:00-11:00"}, {"2006-04-06T12:38:00"}, {"2012-08-24T16"} }; } @BeforeMethod public void runBefore() throws IOException { runAfter(); createOuterInnerDirs(); } @AfterMethod public void runAfter() { cleanup(DIR_INNER); cleanup(DIR_OUTER); JAR_FILE_SOURCE_DATE1.delete(); JAR_FILE_SOURCE_DATE2.delete(); TimeZone.setDefault(TZ); } /** * Test jar tool with various valid --date */ @Test(dataProvider = "validSourceDates") public void testValidSourceDate(String sourceDate) { if (isInTransition()) return; // Test --date source date Assert.assertEquals(JAR_TOOL.run(System.out, System.err, "--create", "--file", JAR_FILE_SOURCE_DATE1.getName(), "--date", sourceDate, DIR_OUTER.getName()), 0); Assert.assertTrue(JAR_FILE_SOURCE_DATE1.exists()); // Extract JAR_FILE_SOURCE_DATE1 and check last modified values Assert.assertEquals(JAR_TOOL.run(System.out, System.err, "--extract", "--file", JAR_FILE_SOURCE_DATE1.getName()), 0); Assert.assertTrue(DIR_OUTER.exists()); Assert.assertTrue(DIR_INNER.exists()); Assert.assertTrue(FILE_INNER.exists()); LocalDateTime expectedLdt = ZonedDateTime.parse(sourceDate, DateTimeFormatter.ISO_DATE_TIME) .withZoneSameInstant(ZoneOffset.UTC) .toLocalDateTime(); System.out.format("Checking jar entries local date time for --date %s, is %s%n", sourceDate, expectedLdt); long sourceDateEpochMillis = TimeUnit.MILLISECONDS.convert( expectedLdt.toEpochSecond(ZoneId.systemDefault().getRules() .getOffset(expectedLdt)), TimeUnit.SECONDS); checkFileTime(DIR_OUTER.lastModified(), sourceDateEpochMillis); checkFileTime(DIR_INNER.lastModified(), sourceDateEpochMillis); checkFileTime(FILE_INNER.lastModified(), sourceDateEpochMillis); } /** * Test jar tool with various invalid --date */ @Test(dataProvider = "invalidSourceDates") public void testInvalidSourceDate(String sourceDate) { // Negative Tests --date out of range or wrong format source date Assert.assertNotEquals(JAR_TOOL.run(System.out, System.err, "--create", "--file", JAR_FILE_SOURCE_DATE1.getName(), "--date", sourceDate, DIR_OUTER.getName()), 0); } /** * Test jar produces deterministic reproducible output */ @Test(dataProvider = "validSourceDates") public void testJarsReproducible(String sourceDate) throws IOException { // Test jars are reproducible across timezones TimeZone tzAsia = TimeZone.getTimeZone("Asia/Shanghai"); TimeZone tzLA = TimeZone.getTimeZone("America/Los_Angeles"); TimeZone.setDefault(tzAsia); Assert.assertEquals(JAR_TOOL.run(System.out, System.err, "--create", "--file", JAR_FILE_SOURCE_DATE1.getName(), "--date", sourceDate, DIR_OUTER.getName()), 0); Assert.assertTrue(JAR_FILE_SOURCE_DATE1.exists()); try { // Sleep 5 seconds to ensure jar timestamps might be different if they could be Thread.sleep(5000); } catch (InterruptedException ex) { } TimeZone.setDefault(tzLA); Assert.assertEquals(JAR_TOOL.run(System.out, System.err, "--create", "--file", JAR_FILE_SOURCE_DATE2.getName(), "--date", sourceDate, DIR_OUTER.getName()), 0); Assert.assertTrue(JAR_FILE_SOURCE_DATE2.exists()); // Check jars are identical Assert.assertEquals(Files.readAllBytes(JAR_FILE_SOURCE_DATE1.toPath()), Files.readAllBytes(JAR_FILE_SOURCE_DATE2.toPath())); } /** * Create the standard directory structure used by the test: * outer/ * inner/ * foo.txt */ static void createOuterInnerDirs() throws IOException { Assert.assertTrue(DIR_OUTER.mkdir()); Assert.assertTrue(DIR_INNER.mkdir()); try (PrintWriter pw = new PrintWriter(FILE_INNER)) { pw.println("hello, world"); } Assert.assertTrue(DIR_OUTER.exists()); Assert.assertTrue(DIR_INNER.exists()); Assert.assertTrue(FILE_INNER.exists()); } /** * Check the extracted and original millis since Epoch file times are * within the zip precision time period. */ static void checkFileTime(long now, long original) { if (isTimeSettingChanged()) { return; } if (Math.abs(now - original) > PRECISION) { // If original time is after UNIX 2038 32bit rollover // and the now time is exactly the rollover time, then assume // running on a file system that only supports to 2038 (e.g.XFS) and pass test if (FileTime.fromMillis(original).toInstant().isAfter(UNIX_2038_ROLLOVER) && FileTime.fromMillis(now).toInstant().equals(UNIX_2038_ROLLOVER)) { System.out.println("Checking file time after Unix 2038 rollover," + " and extracted file time is " + UNIX_2038_ROLLOVER_TIME + ", " + " Assuming restricted file system, pass file time check."); } else { throw new AssertionError("checkFileTime failed," + " extracted to " + FileTime.fromMillis(now) + ", expected to be close to " + FileTime.fromMillis(original)); } } } /** * Has the timezone or DST changed during the test? */ private static boolean isTimeSettingChanged() { TimeZone currentTZ = TimeZone.getDefault(); boolean currentDST = currentTZ.inDaylightTime(new Date()); if (!currentTZ.equals(TZ) || currentDST != DST) { System.out.println("Timezone or DST has changed during " + "ReproducibleJar testcase execution. Test skipped"); return true; } else { return false; } } /** * Is the Zone currently within the transition change period? */ private static boolean isInTransition() { var inTransition = false; var date = new Date(); var defZone = ZoneId.systemDefault(); if (defZone.getRules().getTransition( date.toInstant().atZone(defZone).toLocalDateTime()) != null) { System.out.println("ReproducibleJar testcase being run during Zone offset transition. Test skipped."); inTransition = true; } return inTransition; } /** * Remove the directory and its contents */ static void cleanup(File dir) { File[] x = dir.listFiles(); if (x != null) { for (File f : x) { f.delete(); } } dir.delete(); } }