Skip to content
Permalink
Browse files
8276766: Enable jar and jmod to produce deterministic timestamped con…
…tent

Reviewed-by: ihse, lancea, alanb, jgneff
  • Loading branch information
Andrew Leonard committed Dec 11, 2021
1 parent 6eb6ec0 commit db68a0ce1ce152345320e70acb7e9842d2f1ece4
Showing 8 changed files with 480 additions and 22 deletions.
@@ -34,12 +34,21 @@
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 @@ void process(Main jartool, String opt, String arg) {
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.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.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 int hashCode() {
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 @@ void create(OutputStream out, Manifest manifest) throws IOException
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 @@ boolean update(InputStream in, OutputStream out,
// 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 @@ private void addIndex(JarIndex index, ZipOutputStream zos)
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 @@ private void updateModuleInfo(Map<String, ModuleInfoEntry> moduleInfos, ZipOutpu
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 @@ private boolean updateManifest(Manifest m, ZipOutputStream zos)
addMultiRelease(m);
}
ZipEntry e = new ZipEntry(MANIFEST_NAME);
e.setTime(System.currentTimeMillis());
setZipEntryTime(e);
if (flag0) {
crc32Manifest(e, m);
}
@@ -1204,7 +1210,7 @@ void addFile(ZipOutputStream zos, Entry entry) throws IOException {
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 @@ ModuleHashes computeHashes(String name) {
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.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 @@ public void writeEntry(InputStream in, Entry e) throws IOException {
// 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 @@ private ZipEntry newEntry(Section section, String path) throws IOException {
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.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 @@ static class Options {
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 @@ private boolean create() throws IOException {
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 @@ private void updateModularJar(Path target, Path tempTarget,
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 @@ private void updateJmodFile(Path target, Path tempTarget,
{

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 Version convert(String value) {
@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 @@ private void handleOptions(String[] args) {
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 @@ private void handleOptions(String[] args) {
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}

3 comments on commit db68a0c

@openjdk-notifier
Copy link

@openjdk-notifier openjdk-notifier bot commented on db68a0c Dec 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-m-leonard
Copy link

@andrew-m-leonard andrew-m-leonard commented on db68a0c Jan 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@openjdk
Copy link

@openjdk openjdk bot commented on db68a0c Jan 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-m-leonard Could not automatically backport db68a0ce to openjdk/jdk17u-dev due to conflicts in the following files:

  • src/jdk.jartool/share/classes/sun/tools/jar/Main.java
  • test/jdk/tools/jmod/JmodTest.java

To manually resolve these conflicts run the following commands in your personal fork of openjdk/jdk17u-dev:

$ git checkout -b andrew-m-leonard-backport-db68a0ce
$ git fetch --no-tags https://git.openjdk.java.net/jdk db68a0ce1ce152345320e70acb7e9842d2f1ece4
$ git cherry-pick --no-commit db68a0ce1ce152345320e70acb7e9842d2f1ece4
$ # Resolve conflicts
$ git add files/with/resolved/conflicts
$ git commit -m 'Backport db68a0ce1ce152345320e70acb7e9842d2f1ece4'

Once you have resolved the conflicts as explained above continue with creating a pull request towards the openjdk/jdk17u-dev with the title Backport db68a0ce1ce152345320e70acb7e9842d2f1ece4.

Please sign in to comment.