Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow faking of zip entry modification times. #36

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions src/main/org/apache/tools/ant/taskdefs/Touch.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.Touchable;
import org.apache.tools.ant.types.resources.Union;
import org.apache.tools.ant.util.DateUtils;
import org.apache.tools.ant.util.FileNameMapper;
import org.apache.tools.ant.util.FileUtils;

Expand All @@ -60,33 +61,20 @@ public interface DateFormatFactory {
DateFormat getFallbackFormat();
}

/**
* Provides access to DateUtils.EN_US_DATE_FORMAT_MIN (primary) and
* DateUtils.EN_US_DATE_FORMAT_SEC (fallback).
*/
public static final DateFormatFactory DEFAULT_DF_FACTORY
= new DateFormatFactory() {

private ThreadLocal<DateFormat> primary =
new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yyyy hh:mm a",
Locale.US);
}
};
private ThreadLocal<DateFormat> fallback =
new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a",
Locale.US);
}
};

@Override
public DateFormat getPrimaryFormat() {
return primary.get();
return DateUtils.EN_US_DATE_FORMAT_MIN.get();
}
@Override
public DateFormat getFallbackFormat() {
return fallback.get();
return DateUtils.EN_US_DATE_FORMAT_SEC.get();
}
};
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
Expand Down
43 changes: 41 additions & 2 deletions src/main/org/apache/tools/ant/taskdefs/Zip.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -54,6 +55,7 @@
import org.apache.tools.ant.types.resources.Union;
import org.apache.tools.ant.types.resources.ZipResource;
import org.apache.tools.ant.types.resources.selectors.ResourceSelector;
import org.apache.tools.ant.util.DateUtils;
import org.apache.tools.ant.util.FileNameMapper;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.GlobPatternMapper;
Expand Down Expand Up @@ -115,6 +117,9 @@ public class Zip extends MatchingTask {
protected Hashtable<String, String> addedDirs = new Hashtable<>();
private final List<String> addedFiles = new Vector<>();

private String fixedModTime = null; // User-provided.
protected long modTimeMillis = 0; // Calculated.

/**
* If this flag is true, execute() will run most operations twice,
* the first time with {@link #skipWriting skipWriting} set to
Expand Down Expand Up @@ -575,6 +580,27 @@ public Zip64ModeAttribute getZip64Mode() {
return zip64Mode;
}

/**
* Set all stored file modification times to {@code time}.
* @param time Milliseconds since 1970-01-01 00:00, or
* <code>YYYY-MM-DD{T/ }HH:MM[:SS[.SSS]][ ][±ZZ[[:]ZZ]]</code>, or
* <code>MM/DD/YYYY HH:MM[:SS] {AM/PM}</code>, where {a/b} indicates
* that you must choose one of a or b, and [c] indicates that you
* may use or omit c. ±ZZZZ is the timezone offset, and may be
* literally "Z" to mean GMT.
*/
public void setModificationtime(String time) {
fixedModTime = time;
}

/**
* The file modification time previously provided to
* {@link #setModificationtime(String)} or {@code null} if unset.
*/
public String getModificationtime() {
return fixedModTime;
}

/**
* validate and build
* @throws BuildException on error
Expand Down Expand Up @@ -811,6 +837,17 @@ private void checkAttributesAndElements() {
archiveType);
}

if (fixedModTime != null) {
try {
modTimeMillis = DateUtils.parseLenientDateTime(fixedModTime).getTime();
} catch (ParseException pe) {
throw new BuildException("Failed to parse date string %s.", fixedModTime);
}
if (roundUp) {
modTimeMillis += ROUNDUP_MILLIS;
}
}

if (zipFile.exists() && !zipFile.isFile()) {
throw new BuildException("%s is not a file.", zipFile);
}
Expand Down Expand Up @@ -1663,7 +1700,9 @@ protected void zipDir(final Resource dir, final ZipOutputStream zOut, final Stri
// ZIPs store time with a granularity of 2 seconds, round up
final int millisToAdd = roundUp ? ROUNDUP_MILLIS : 0;

if (dir != null && dir.isExists()) {
if (fixedModTime != null) {
ze.setTime(modTimeMillis);
} else if (dir != null && dir.isExists()) {
ze.setTime(dir.getLastModified() + millisToAdd);
} else {
ze.setTime(System.currentTimeMillis() + millisToAdd);
Expand Down Expand Up @@ -1750,7 +1789,7 @@ protected void zipFile(InputStream in, final ZipOutputStream zOut, final String

if (!skipWriting) {
final ZipEntry ze = new ZipEntry(vPath);
ze.setTime(lastModified);
ze.setTime(fixedModTime != null ? modTimeMillis : lastModified);
ze.setMethod(doCompress ? ZipEntry.DEFLATED : ZipEntry.STORED);

/*
Expand Down
74 changes: 74 additions & 0 deletions src/main/org/apache/tools/ant/util/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Helper methods to deal with date/time formatting with a specific
Expand Down Expand Up @@ -89,6 +91,32 @@ public final class DateUtils {
private static final ChoiceFormat SECONDS_FORMAT =
new ChoiceFormat(LIMITS, SECONDS_PART);

/**
* Provides a thread-local US-style date format. Exactly as used by
* {@code <touch>}, to minute precision:
* {@code SimpleDateFormat("MM/dd/yyyy hh:mm a", Locale.US)}
*/
public static final ThreadLocal<DateFormat> EN_US_DATE_FORMAT_MIN =
new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yyyy hh:mm a", Locale.US);
}
};

/**
* Provides a thread-local US-style date format. Exactly as used by
* {@code <touch>}, to second precision:
* {@code SimpleDateFormat("MM/dd/yyyy hh:mm:ss a", Locale.US)}
*/
public static final ThreadLocal<DateFormat> EN_US_DATE_FORMAT_SEC =
new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a", Locale.US);
}
};

static {
MINUTE_SECONDS.setFormat(0, MINUTES_FORMAT);
MINUTE_SECONDS.setFormat(1, SECONDS_FORMAT);
Expand Down Expand Up @@ -298,4 +326,50 @@ public static Date parseIso8601DateTimeOrDate(String datestr)
return parseIso8601Date(datestr);
}
}

final private static ThreadLocal<DateFormat> iso8601WithTimeZone =
new ThreadLocal<DateFormat>() {
@Override protected DateFormat initialValue() {
// An arbitrary easy-to-read format to normalize to.
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z");
}
};
final private static Pattern iso8601normalizer = Pattern.compile(
"^(\\d{4,}-\\d{2}-\\d{2})[Tt ]" + // yyyy-MM-dd
"(\\d{2}:\\d{2}(:\\d{2}(\\.\\d{3})?)?) ?" + // HH:mm:ss.SSS
"(?:Z|([+-]\\d{2})(?::?(\\d{2}))?)?$"); // Z

/**
* Parse a lenient ISO 8601, ms since epoch, or {@code <touch>}-style date.
* That is:
* <ul>
* <li>Milliseconds since 1970-01-01 00:00</li>
* <li><code>YYYY-MM-DD{T| }HH:MM[:SS[.SSS]][ ][±ZZ[[:]ZZ]]</code></li>
* <li><code>MM/DD/YYYY HH:MM[:SS] {AM|PM}</code></li></ul>
* where {a|b} indicates that you must choose one of a or b, and [c]
* indicates that you may use or omit c. ±ZZZZ is the timezone offset, and
* may be literally "Z" to mean GMT.
*/
public static Date parseLenientDateTime(String dateStr) throws ParseException {
try {
return new Date(Long.parseLong(dateStr));
} catch (NumberFormatException nfe) {}

try {
return EN_US_DATE_FORMAT_MIN.get().parse(dateStr);
} catch (ParseException pe) {}

try {
return EN_US_DATE_FORMAT_SEC.get().parse(dateStr);
} catch (ParseException pe) {}

Matcher m = iso8601normalizer.matcher(dateStr);
if (!m.find()) throw new ParseException(dateStr, 0);
String normISO = m.group(1) + " "
+ (m.group(3) == null ? m.group(2) + ":00" : m.group(2))
+ (m.group(4) == null ? ".000 " : " ")
+ (m.group(5) == null ? "+00" : m.group(5))
+ (m.group(6) == null ? "00" : m.group(6));
return iso8601WithTimeZone.get().parse(normISO);
}
}
20 changes: 20 additions & 0 deletions src/tests/antunit/taskdefs/zip-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,24 @@
<fileset dir="${basedir}" includes="zip-test.xml"/>
</zip>
</target>

<target name="testSetZipModTime">
<mkdir dir="${input}/"/>
<mkdir dir="${input}/subdir"/>
<touch file="${input}/test1.txt"/>
<touch file="${input}/subdir/test2.txt"/>
<mkdir dir="${output}"/>
<zip destfile="${output}/test.zip" basedir="${input}" modificationtime="2016-01-01 00:00" />
<checksum file="${output}/test.zip" property="testSetZipModTime_hash" />
<delete file="${output}/test.zip" />

<sleep seconds="4" /> <!-- Necessary to ensure zips would have normally differed in timestamp -->
<touch file="${input}/test1.txt"/>
<touch file="${input}/subdir/test2.txt"/>
<zip destfile="${output}/test.zip" basedir="${input}" modificationtime="2016-01-01 00:00" />
<checksum file="${output}/test.zip" property="${testSetZipModTime_hash}" verifyproperty="testSetZipModTime_okay" />
<au:assertTrue>
<equals arg1="${testSetZipModTime_okay}" arg2="true" />
</au:assertTrue>
</target>
</project>