8276766: Enable jar and jmod to produce deterministic timestamped content

Reviewed-by: ihse, lancea, alanb, jgneff
This commit is contained in:
Andrew Leonard 2021-12-11 15:27:08 +00:00
parent 6eb6ec05fd
commit db68a0ce1c
8 changed files with 480 additions and 22 deletions
src
jdk.jartool/share/classes/sun/tools/jar
jdk.jlink/share/classes/jdk/tools/jmod
test/jdk/tools

@ -34,12 +34,21 @@ import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import jdk.internal.module.ModulePath;
import jdk.internal.module.ModuleResolution;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/**
* Parser for GNU Style Options.
*/
class GNUStyleOptions {
// Valid --date range
static final ZonedDateTime DATE_MIN = ZonedDateTime.parse("1980-01-01T00:00:02Z");
static final ZonedDateTime DATE_MAX = ZonedDateTime.parse("2099-12-31T23:59:59Z");
static class BadArgs extends Exception {
static final long serialVersionUID = 0L;
@ -188,6 +197,20 @@ class GNUStyleOptions {
jartool.flag0 = true;
}
},
new Option(true, OptionType.CREATE_UPDATE_INDEX, "--date") {
void process(Main jartool, String opt, String arg) throws BadArgs {
try {
ZonedDateTime date = ZonedDateTime.parse(arg, DateTimeFormatter.ISO_ZONED_DATE_TIME)
.withZoneSameInstant(ZoneOffset.UTC);
if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
throw new BadArgs("error.date.out.of.range", arg);
}
jartool.date = date.toLocalDateTime();
} catch (DateTimeParseException x) {
throw new BadArgs("error.date.notvalid", arg);
}
}
},
// Hidden options
new Option(false, OptionType.OTHER, "-P") {

@ -60,6 +60,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import java.util.concurrent.TimeUnit;
import jdk.internal.module.Checks;
import jdk.internal.module.ModuleHashes;
import jdk.internal.module.ModuleHashesBuilder;
@ -68,6 +69,8 @@ import jdk.internal.module.ModuleInfoExtender;
import jdk.internal.module.ModuleResolution;
import jdk.internal.module.ModuleTarget;
import jdk.internal.util.jar.JarIndex;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.jar.JarFile.MANIFEST_NAME;
@ -174,6 +177,9 @@ public class Main {
static final int VERSIONS_DIR_LENGTH = VERSIONS_DIR.length();
private static ResourceBundle rsrc;
/* Date option for entry timestamps resolved to UTC Local time */
LocalDateTime date;
/**
* If true, maintain compatibility with JDK releases prior to 6.0 by
* timestamping extracted files with the time at which they are extracted.
@ -862,12 +868,12 @@ public class Main {
output(getMsg("out.added.manifest"));
}
ZipEntry e = new ZipEntry(MANIFEST_DIR);
e.setTime(System.currentTimeMillis());
setZipEntryTime(e);
e.setSize(0);
e.setCrc(0);
zos.putNextEntry(e);
e = new ZipEntry(MANIFEST_NAME);
e.setTime(System.currentTimeMillis());
setZipEntryTime(e);
if (flag0) {
crc32Manifest(e, manifest);
}
@ -967,7 +973,7 @@ public class Main {
// do our own compression
ZipEntry e2 = new ZipEntry(name);
e2.setMethod(e.getMethod());
e2.setTime(e.getTime());
setZipEntryTime(e2, e.getTime());
e2.setComment(e.getComment());
e2.setExtra(e.getExtra());
if (e.getMethod() == ZipEntry.STORED) {
@ -1033,7 +1039,7 @@ public class Main {
throws IOException
{
ZipEntry e = new ZipEntry(INDEX_NAME);
e.setTime(System.currentTimeMillis());
setZipEntryTime(e);
if (flag0) {
CRC32OutputStream os = new CRC32OutputStream();
index.write(os);
@ -1055,9 +1061,9 @@ public class Main {
ZipEntry e = new ZipEntry(name);
FileTime lastModified = mie.getLastModifiedTime();
if (lastModified != null) {
e.setLastModifiedTime(lastModified);
setZipEntryTime(e, lastModified.toMillis());
} else {
e.setLastModifiedTime(FileTime.fromMillis(System.currentTimeMillis()));
setZipEntryTime(e);
}
if (flag0) {
crc32ModuleInfo(e, bytes);
@ -1083,7 +1089,7 @@ public class Main {
addMultiRelease(m);
}
ZipEntry e = new ZipEntry(MANIFEST_NAME);
e.setTime(System.currentTimeMillis());
setZipEntryTime(e);
if (flag0) {
crc32Manifest(e, m);
}
@ -1204,7 +1210,7 @@ public class Main {
out.print(formatMsg("out.adding", name));
}
ZipEntry e = new ZipEntry(name);
e.setTime(file.lastModified());
setZipEntryTime(e, file.lastModified());
if (size == 0) {
e.setMethod(ZipEntry.STORED);
e.setSize(0);
@ -2318,4 +2324,18 @@ public class Main {
static Comparator<ZipEntry> ENTRY_COMPARATOR =
Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);
// Set the ZipEntry dostime using date if specified otherwise the current time
private void setZipEntryTime(ZipEntry e) {
setZipEntryTime(e, System.currentTimeMillis());
}
// Set the ZipEntry dostime using the date if specified
// otherwise the original time
private void setZipEntryTime(ZipEntry e, long origTime) {
if (date != null) {
e.setTimeLocal(date);
} else {
e.setTime(origTime);
}
}
}

@ -1,5 +1,5 @@
#
# Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 1999, 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
@ -82,6 +82,10 @@ error.release.value.toosmall=\
release {0} not valid, must be >= 9
error.release.unexpected.versioned.entry=\
unexpected versioned entry {0} for release {1}
error.date.notvalid=\
date {0} is not a valid ISO-8601 extended offset date-time with optional time-zone
error.date.out.of.range=\
date {0} is not within the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
error.validator.jarfile.exception=\
can not validate {0}: {1}
error.validator.jarfile.invalid=\
@ -290,6 +294,10 @@ main.help.opt.create.update.index=\
\ Operation modifiers valid only in create, update, and generate-index mode:\n
main.help.opt.create.update.index.no-compress=\
\ -0, --no-compress Store only; use no ZIP compression
main.help.opt.create.update.index.date=\
\ --date=TIMESTAMP The timestamp in ISO-8601 extended offset date-time with\n\
\ optional time-zone format, to use for the timestamps of\n\
\ entries, e.g. "2022-02-12T12:30:00-05:00"
main.help.opt.other=\
\ Other options:\n
main.help.opt.other.help=\

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 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
@ -41,6 +41,7 @@ import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import jdk.internal.jmod.JmodFile;
import java.time.LocalDateTime;
import static jdk.internal.jmod.JmodFile.*;
@ -54,15 +55,17 @@ class JmodOutputStream extends OutputStream implements AutoCloseable {
* This method creates (or overrides, if exists) the JMOD file,
* returning the the output stream to write to the JMOD file.
*/
static JmodOutputStream newOutputStream(Path file) throws IOException {
static JmodOutputStream newOutputStream(Path file, LocalDateTime date) throws IOException {
OutputStream out = Files.newOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(out);
return new JmodOutputStream(bos);
return new JmodOutputStream(bos, date);
}
private final ZipOutputStream zos;
private JmodOutputStream(OutputStream out) {
private final LocalDateTime date;
private JmodOutputStream(OutputStream out, LocalDateTime date) {
this.zos = new ZipOutputStream(out);
this.date = date;
try {
JmodFile.writeMagicNumber(out);
} catch (IOException e) {
@ -104,7 +107,11 @@ class JmodOutputStream extends OutputStream implements AutoCloseable {
// sun.tools.jar.Main.update()
ZipEntry e2 = new ZipEntry(e1.getName());
e2.setMethod(e1.getMethod());
e2.setTime(e1.getTime());
if (date != null) {
e2.setTimeLocal(date);
} else {
e2.setTime(e1.getTime());
}
e2.setComment(e1.getComment());
e2.setExtra(e1.getExtra());
if (e1.getMethod() == ZipEntry.STORED) {
@ -124,7 +131,11 @@ class JmodOutputStream extends OutputStream implements AutoCloseable {
String name = Paths.get(prefix, path).toString()
.replace(File.separatorChar, '/');
entries.get(section).add(path);
return new ZipEntry(name);
ZipEntry zipEntry = new ZipEntry(name);
if (date != null) {
zipEntry.setTimeLocal(date);
}
return zipEntry;
}
public boolean contains(Section section, String path) {

@ -62,6 +62,11 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import jdk.internal.jmod.JmodFile;
import jdk.internal.jmod.JmodFile.Section;
@ -160,8 +165,13 @@ public class JmodTask {
boolean dryrun;
List<PathMatcher> excludes;
Path extractDir;
LocalDateTime date;
}
// Valid --date range
static final ZonedDateTime DATE_MIN = ZonedDateTime.parse("1980-01-01T00:00:02Z");
static final ZonedDateTime DATE_MAX = ZonedDateTime.parse("2099-12-31T23:59:59Z");
public int run(String[] args) {
try {
@ -427,7 +437,7 @@ public class JmodTask {
Path target = options.jmodFile;
Path tempTarget = jmodTempFilePath(target);
try {
try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) {
try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget, options.date)) {
jmod.write(jos);
}
Files.move(tempTarget, target);
@ -984,7 +994,11 @@ public class JmodTask {
if (e.getName().equals(MODULE_INFO)) {
// what about module-info.class in versioned entries?
ZipEntry ze = new ZipEntry(e.getName());
ze.setTime(System.currentTimeMillis());
if (options.date != null) {
ze.setTimeLocal(options.date);
} else {
ze.setTime(System.currentTimeMillis());
}
jos.putNextEntry(ze);
recordHashes(in, jos, moduleHashes);
jos.closeEntry();
@ -1012,7 +1026,7 @@ public class JmodTask {
{
try (JmodFile jf = new JmodFile(target);
JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget))
JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget, options.date))
{
jf.stream().forEach(e -> {
try (InputStream in = jf.getInputStream(e.section(), e.name())) {
@ -1147,6 +1161,26 @@ public class JmodTask {
@Override public String valuePattern() { return "module-version"; }
}
static class DateConverter implements ValueConverter<LocalDateTime> {
@Override
public LocalDateTime convert(String value) {
try {
ZonedDateTime date = ZonedDateTime.parse(value, DateTimeFormatter.ISO_ZONED_DATE_TIME)
.withZoneSameInstant(ZoneOffset.UTC);
if (date.isBefore(DATE_MIN) || date.isAfter(DATE_MAX)) {
throw new CommandException("err.date.out.of.range", value);
}
return date.toLocalDateTime();
} catch (DateTimeParseException x) {
throw new CommandException("err.invalid.date", value, x.getMessage());
}
}
@Override public Class<LocalDateTime> valueType() { return LocalDateTime.class; }
@Override public String valuePattern() { return "date"; }
}
static class WarnIfResolvedReasonConverter
implements ValueConverter<ModuleResolution>
{
@ -1382,6 +1416,11 @@ public class JmodTask {
OptionSpec<Void> version
= parser.accepts("version", getMessage("main.opt.version"));
OptionSpec<LocalDateTime> date
= parser.accepts("date", getMessage("main.opt.date"))
.withRequiredArg()
.withValuesConvertedBy(new DateConverter());
NonOptionArgumentSpec<String> nonOptions
= parser.nonOptions();
@ -1425,6 +1464,8 @@ public class JmodTask {
options.manPages = getLastElement(opts.valuesOf(manPages));
if (opts.has(legalNotices))
options.legalNotices = getLastElement(opts.valuesOf(legalNotices));
if (opts.has(date))
options.date = opts.valueOf(date);
if (opts.has(modulePath)) {
Path[] dirs = getLastElement(opts.valuesOf(modulePath)).toArray(new Path[0]);
options.moduleFinder = ModulePath.of(Runtime.version(), true, dirs);

@ -1,5 +1,5 @@
#
# Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2015, 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
@ -74,6 +74,9 @@ main.opt.hash-modules=Compute and record hashes to tie a packaged module\
main.opt.do-not-resolve-by-default=Exclude from the default root set of modules
main.opt.warn-if-resolved=Hint for a tool to issue a warning if the module \
is resolved. One of deprecated, deprecated-for-removal, or incubating
main.opt.date=Date and time for the timestamps of entries, specified in ISO-8601\
\ extended offset date-time with optional time-zone format, e.g.\
\ "2022-02-12T12:30:00-05:00"
main.opt.cmdfile=Read options from the specified file
@ -106,6 +109,8 @@ err.module.descriptor.not.found=Module descriptor not found
err.missing.export.or.open.packages=Packages that are exported or open in {0} are not present: {1}
err.module.resolution.fail=Resolution failed: {0}
err.no.moduleToHash=No hashes recorded: no module matching {0} found to record hashes
err.invalid.date=--date {0} is not a valid ISO-8601 extended offset date-time with optional time-zone format: {1}
err.date.out.of.range=--date {0} is out of the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z
warn.invalid.arg=Invalid classname or pathname not exist: {0}
warn.no.module.hashes=No hashes recorded: no module specified for hashing depends on {0}
warn.ignore.entry=ignoring entry {0}, in section {1}

@ -0,0 +1,290 @@
/*
* 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
* @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 <timestamps>
*/
@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 <timestamps>
*/
@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();
}
}

@ -23,7 +23,7 @@
/*
* @test
* @bug 8142968 8166568 8166286 8170618 8168149 8240910 8276764
* @bug 8142968 8166568 8166286 8170618 8168149 8240910 8276764 8276766
* @summary Basic test for jmod
* @library /test/lib
* @modules jdk.compiler
@ -183,13 +183,15 @@ public class JmodTest {
@Test
public void testList() throws IOException {
String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
Path jmod = MODS_DIR.resolve("foo.jmod");
FileUtils.deleteFileIfExistsWithRetry(jmod);
jmod("create",
"--class-path", cp,
MODS_DIR.resolve("foo.jmod").toString())
jmod.toString())
.assertSuccess();
jmod("list",
MODS_DIR.resolve("foo.jmod").toString())
jmod.toString())
.assertSuccess()
.resultChecker(r -> {
// asserts dependent on the exact contents of foo
@ -211,6 +213,64 @@ public class JmodTest {
});
}
@Test
public void testSourceDateReproducible() throws IOException {
String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
Path jmod1 = MODS_DIR.resolve("foo1.jmod");
Path jmod2 = MODS_DIR.resolve("foo2.jmod");
Path jmod3 = MODS_DIR.resolve("foo3.jmod");
FileUtils.deleteFileIfExistsWithRetry(jmod1);
FileUtils.deleteFileIfExistsWithRetry(jmod2);
FileUtils.deleteFileIfExistsWithRetry(jmod3);
// Use source date of 15/03/2022
String sourceDate = "2022-03-15T00:00:00+00:00";
jmod("create",
"--class-path", cp,
"--date", sourceDate,
jmod1.toString())
.assertSuccess();
try {
// Sleep 5 seconds to ensure zip timestamps might be different if they could be
Thread.sleep(5000);
} catch(InterruptedException ex) {}
jmod("create",
"--class-path", cp,
"--date", sourceDate,
jmod2.toString())
.assertSuccess();
// Compare file byte content to see if they are identical
assertSameContent(jmod1, jmod2);
// Use a date before 1980 and assert failure error
sourceDate = "1976-03-15T00:00:00+00:00";
jmod("create",
"--class-path", cp,
"--date", sourceDate,
jmod3.toString())
.assertFailure()
.resultChecker(r -> {
assertContains(r.output, "is out of the valid range");
});
// Use a date after 2099 and assert failure error
sourceDate = "2100-03-15T00:00:00+00:00";
jmod("create",
"--class-path", cp,
"--date", sourceDate,
jmod3.toString())
.assertFailure()
.resultChecker(r -> {
assertContains(r.output, "is out of the valid range");
});
}
@Test
public void testExtractCWD() throws IOException {
Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");