8276766: Enable jar and jmod to produce deterministic timestamped content
Reviewed-by: ihse, lancea, alanb, jgneff
This commit is contained in:
parent
6eb6ec05fd
commit
db68a0ce1c
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}
|
||||
|
290
test/jdk/tools/jar/ReproducibleJar.java
Normal file
290
test/jdk/tools/jar/ReproducibleJar.java
Normal file
@ -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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user