blob: aa95aa2194dafe2ca7432cf6b90908d645fea5a3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.tools.ant.taskdefs;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
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.Comparator;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;
import java.util.stream.Stream;
import java.util.zip.CRC32;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.ArchiveFileSet;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.ZipFileSet;
import org.apache.tools.ant.types.ZipScanner;
import org.apache.tools.ant.types.resources.ArchiveResource;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.types.resources.FileResource;
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;
import org.apache.tools.ant.util.IdentityMapper;
import org.apache.tools.ant.util.MergingMapper;
import org.apache.tools.ant.util.ResourceUtils;
import org.apache.tools.zip.UnixStat;
import org.apache.tools.zip.Zip64Mode;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipExtraField;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;
import org.apache.tools.zip.ZipOutputStream.UnicodeExtraFieldPolicy;
/**
* Create a Zip file.
*
* @since Ant 1.1
*
* @ant.task category="packaging"
*/
public class Zip extends MatchingTask {
private static final int BUFFER_SIZE = 8 * 1024;
/**
* The granularity of timestamps inside a ZIP archive.
*/
private static final int ZIP_FILE_TIMESTAMP_GRANULARITY = 2000;
private static final int ROUNDUP_MILLIS = ZIP_FILE_TIMESTAMP_GRANULARITY - 1;
// CheckStyle:VisibilityModifier OFF - bc
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
// For directories:
private static final long EMPTY_CRC = new CRC32().getValue();
private static final ResourceSelector MISSING_SELECTOR =
target -> !target.isExists();
private static final ResourceUtils.ResourceSelectorProvider
MISSING_DIR_PROVIDER = sr -> MISSING_SELECTOR;
protected File zipFile;
// use to scan own archive
private ZipScanner zs;
private File baseDir;
protected Hashtable<String, String> entries = new Hashtable<>();
private final List<FileSet> groupfilesets = new Vector<>();
private final List<ZipFileSet> filesetsFromGroupfilesets = new Vector<>();
protected String duplicate = "add";
private boolean doCompress = true;
private boolean doUpdate = false;
// shadow of the above if the value is altered in execute
private boolean savedDoUpdate = false;
private boolean doFilesonly = false;
protected String archiveType = "zip";
protected String emptyBehavior = "skip";
private final List<ResourceCollection> resources = new Vector<>();
protected Hashtable<String, String> addedDirs = new Hashtable<>();
private final List<String> addedFiles = new Vector<>();
private String fixedModTime = null; // User-provided.
private long modTimeMillis = 0; // Calculated.
/**
* If this flag is true, execute() will run most operations twice,
* the first time with {@link #skipWriting skipWriting} set to
* true and the second time with setting it to false.
*
* <p>The only situation in Ant's current code base where this is
* ever going to be true is if the jar task has been configured
* with a filesetmanifest other than "skip".</p>
*/
protected boolean doubleFilePass = false;
/**
* whether the methods should just perform some sort of dry-run.
*
* <p>Will only ever be true in the first pass if the task
* performs two passes because {@link #doubleFilePass
* doubleFilePass} is true.</p>
*/
protected boolean skipWriting = false;
/**
* Whether this is the first time the archive building methods are invoked.
*
* @return true if either {@link #doubleFilePass doubleFilePass}
* is false or {@link #skipWriting skipWriting} is true.
*
* @since Ant 1.8.0
*/
protected final boolean isFirstPass() {
return !doubleFilePass || skipWriting;
}
// CheckStyle:VisibilityModifier ON
// This boolean is set if the task detects that the
// target is outofdate and has written to the target file.
private boolean updatedFile = false;
/**
* true when we are adding new files into the Zip file, as opposed
* to adding back the unchanged files
*/
private boolean addingNewFiles = false;
/**
* Encoding to use for filenames, defaults to the platform's
* default encoding.
*/
private String encoding;
/**
* Whether the original compression of entries coming from a ZIP
* archive should be kept (for example when updating an archive).
*
* @since Ant 1.6
*/
private boolean keepCompression = false;
/**
* Whether the file modification times will be rounded up to the
* next even number of seconds.
*
* @since Ant 1.6.2
*/
private boolean roundUp = true;
/**
* Comment for the archive.
* @since Ant 1.6.3
*/
private String comment = "";
private int level = ZipOutputStream.DEFAULT_COMPRESSION;
/**
* Assume 0 Unix mode is intentional.
* @since Ant 1.8.0
*/
private boolean preserve0Permissions = false;
/**
* Whether to set the language encoding flag when creating the archive.
*
* @since Ant 1.8.0
*/
private boolean useLanguageEncodingFlag = true;
/**
* Whether to add unicode extra fields.
*
* @since Ant 1.8.0
*/
private UnicodeExtraField createUnicodeExtraFields =
UnicodeExtraField.NEVER;
/**
* Whether to fall back to UTF-8 if a name cannot be encoded using
* the specified encoding.
*
* @since Ant 1.8.0
*/
private boolean fallBackToUTF8 = false;
/**
* Whether to enable Zip64 extensions.
*
* @since Ant 1.9.1
*/
private Zip64ModeAttribute zip64Mode = Zip64ModeAttribute.AS_NEEDED;
/**
* This is the name/location of where to
* create the .zip file.
* @param zipFile the path of the zipFile
* @deprecated since 1.5.x.
* Use setDestFile(File) instead.
* @ant.attribute ignore="true"
*/
@Deprecated
public void setZipfile(final File zipFile) {
setDestFile(zipFile);
}
/**
* This is the name/location of where to
* create the file.
* @param file the path of the zipFile
* @since Ant 1.5
* @deprecated since 1.5.x.
* Use setDestFile(File) instead.
* @ant.attribute ignore="true"
*/
@Deprecated
public void setFile(final File file) {
setDestFile(file);
}
/**
* The file to create; required.
* @since Ant 1.5
* @param destFile The new destination File
*/
public void setDestFile(final File destFile) {
this.zipFile = destFile;
}
/**
* The file to create.
* @return the destination file
* @since Ant 1.5.2
*/
public File getDestFile() {
return zipFile;
}
/**
* Directory from which to archive files; optional.
* @param baseDir the base directory
*/
public void setBasedir(final File baseDir) {
this.baseDir = baseDir;
}
/**
* Whether we want to compress the files or only store them;
* optional, default=true;
* @param c if true, compress the files
*/
public void setCompress(final boolean c) {
doCompress = c;
}
/**
* Whether we want to compress the files or only store them;
* @return true if the files are to be compressed
* @since Ant 1.5.2
*/
public boolean isCompress() {
return doCompress;
}
/**
* If true, emulate Sun's jar utility by not adding parent directories;
* optional, defaults to false.
* @param f if true, emulate sun's jar by not adding parent directories
*/
public void setFilesonly(final boolean f) {
doFilesonly = f;
}
/**
* If true, updates an existing file, otherwise overwrite
* any existing one; optional defaults to false.
* @param c if true, updates an existing zip file
*/
public void setUpdate(final boolean c) {
doUpdate = c;
savedDoUpdate = c;
}
/**
* Are we updating an existing archive?
* @return true if updating an existing archive
*/
public boolean isInUpdateMode() {
return doUpdate;
}
/**
* Adds a set of files.
* @param set the fileset to add
*/
public void addFileset(final FileSet set) {
add(set);
}
/**
* Adds a set of files that can be
* read from an archive and be given a prefix/fullpath.
* @param set the zipfileset to add
*/
public void addZipfileset(final ZipFileSet set) {
add(set);
}
/**
* Add a collection of resources to be archived.
* @param a the resources to archive
* @since Ant 1.7
*/
public void add(final ResourceCollection a) {
resources.add(a);
}
/**
* Adds a group of zip files.
* @param set the group (a fileset) to add
*/
public void addZipGroupFileset(final FileSet set) {
groupfilesets.add(set);
}
/**
* Sets behavior for when a duplicate file is about to be added -
* one of <code>add</code>, <code>preserve</code> or <code>fail</code>.
* Possible values are: <code>add</code> (keep both
* of the files); <code>preserve</code> (keep the first version
* of the file found); <code>fail</code> halt a problem
* Default for zip tasks is <code>add</code>
* @param df a <code>Duplicate</code> enumerated value
*/
public void setDuplicate(final Duplicate df) {
duplicate = df.getValue();
}
/**
* Possible behaviors when there are no matching files for the task:
* "fail", "skip", or "create".
*/
public static class WhenEmpty extends EnumeratedAttribute {
/**
* The string values for the enumerated value
* @return the values
*/
@Override
public String[] getValues() {
return new String[] {"fail", "skip", "create"};
}
}
/**
* Sets behavior of the task when no files match.
* Possible values are: <code>fail</code> (throw an exception
* and halt the build); <code>skip</code> (do not create
* any archive, but issue a warning); <code>create</code>
* (make an archive with no entries).
* Default for zip tasks is <code>skip</code>;
* for jar tasks, <code>create</code>.
* @param we a <code>WhenEmpty</code> enumerated value
*/
public void setWhenempty(final WhenEmpty we) {
emptyBehavior = we.getValue();
}
/**
* Encoding to use for filenames, defaults to the platform's
* default encoding.
*
* <p>For a list of possible values see <a
* href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html</a>.</p>
* @param encoding the encoding name
*/
public void setEncoding(final String encoding) {
this.encoding = encoding;
}
/**
* Encoding to use for filenames.
* @return the name of the encoding to use
* @since Ant 1.5.2
*/
public String getEncoding() {
return encoding;
}
/**
* Whether the original compression of entries coming from a ZIP
* archive should be kept (for example when updating an archive).
* Default is false.
* @param keep if true, keep the original compression
* @since Ant 1.6
*/
public void setKeepCompression(final boolean keep) {
keepCompression = keep;
}
/**
* Comment to use for archive.
*
* @param comment The content of the comment.
* @since Ant 1.6.3
*/
public void setComment(final String comment) {
this.comment = comment;
}
/**
* Comment of the archive
*
* @return Comment of the archive.
* @since Ant 1.6.3
*/
public String getComment() {
return comment;
}
/**
* Set the compression level to use. Default is
* ZipOutputStream.DEFAULT_COMPRESSION.
* @param level compression level.
* @since Ant 1.7
*/
public void setLevel(final int level) {
this.level = level;
}
/**
* Get the compression level.
* @return compression level.
* @since Ant 1.7
*/
public int getLevel() {
return level;
}
/**
* Whether the file modification times will be rounded up to the
* next even number of seconds.
*
* <p>Zip archives store file modification times with a
* granularity of two seconds, so the times will either be rounded
* up or down. If you round down, the archive will always seem
* out-of-date when you rerun the task, so the default is to round
* up. Rounding up may lead to a different type of problems like
* JSPs inside a web archive that seem to be slightly more recent
* than precompiled pages, rendering precompilation useless.</p>
* @param r a <code>boolean</code> value
* @since Ant 1.6.2
*/
public void setRoundUp(final boolean r) {
roundUp = r;
}
/**
* Assume 0 Unix mode is intentional.
* @param b boolean
* @since Ant 1.8.0
*/
public void setPreserve0Permissions(final boolean b) {
preserve0Permissions = b;
}
/**
* Assume 0 Unix mode is intentional.
* @return boolean
* @since Ant 1.8.0
*/
public boolean getPreserve0Permissions() {
return preserve0Permissions;
}
/**
* Whether to set the language encoding flag.
* @param b boolean
* @since Ant 1.8.0
*/
public void setUseLanguageEncodingFlag(final boolean b) {
useLanguageEncodingFlag = b;
}
/**
* Whether the language encoding flag will be used.
* @return boolean
* @since Ant 1.8.0
*/
public boolean getUseLanguageEnodingFlag() {
return useLanguageEncodingFlag;
}
/**
* Whether Unicode extra fields will be created.
* @param b boolean
* @since Ant 1.8.0
*/
public void setCreateUnicodeExtraFields(final UnicodeExtraField b) {
createUnicodeExtraFields = b;
}
/**
* Whether Unicode extra fields will be created.
* @return boolean
* @since Ant 1.8.0
*/
public UnicodeExtraField getCreateUnicodeExtraFields() {
return createUnicodeExtraFields;
}
/**
* Whether to fall back to UTF-8 if a name cannot be encoded using
* the specified encoding.
*
* <p>Defaults to false.</p>
*
* @param b boolean
* @since Ant 1.8.0
*/
public void setFallBackToUTF8(final boolean b) {
fallBackToUTF8 = b;
}
/**
* Whether to fall back to UTF-8 if a name cannot be encoded using
* the specified encoding.
*
* @return boolean
* @since Ant 1.8.0
*/
public boolean getFallBackToUTF8() {
return fallBackToUTF8;
}
/**
* Whether Zip64 extensions should be used.
* @param b boolean
* @since Ant 1.9.1
*/
public void setZip64Mode(final Zip64ModeAttribute b) {
zip64Mode = b;
}
/**
* Whether Zip64 extensions will be used.
* @return boolean
* @since Ant 1.9.1
*/
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]][ ][&plusmn;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. &plusmn;ZZZZ is the timezone offset, and may be
* literally "Z" to mean GMT.
* @since Ant 1.10.2
*/
public void setModificationtime(String time) {
fixedModTime = time;
}
/**
* The file modification time previously provided to
* {@link #setModificationtime(String)} or {@code null} if unset.
* @return String
* @since Ant 1.10.2
*/
public String getModificationtime() {
return fixedModTime;
}
/**
* validate and build
* @throws BuildException on error
*/
@Override
public void execute() throws BuildException {
if (doubleFilePass) {
skipWriting = true;
executeMain();
skipWriting = false;
}
executeMain();
}
/**
* Get the value of the updatedFile attribute.
* This should only be called after executeMain has been
* called.
* @return true if executeMain has written to the zip file.
*/
protected boolean hasUpdatedFile() {
return updatedFile;
}
/**
* Build the zip file.
* This is called twice if doubleFilePass is true.
* @throws BuildException on error
*/
public void executeMain() throws BuildException {
checkAttributesAndElements();
// Renamed version of original file, if it exists
File renamedFile = null;
addingNewFiles = true;
processDoUpdate();
processGroupFilesets();
// collect filesets to pass them to getResourcesToAdd
final List<ResourceCollection> vfss = new ArrayList<>();
if (baseDir != null) {
final FileSet fs = (FileSet) getImplicitFileSet().clone();
fs.setDir(baseDir);
vfss.add(fs);
}
vfss.addAll(resources);
final ResourceCollection[] fss =
vfss.toArray(new ResourceCollection[vfss.size()]);
boolean success = false;
try {
// can also handle empty archives
final ArchiveState state = getResourcesToAdd(fss, zipFile, false);
// quick exit if the target is up to date
if (!state.isOutOfDate()) {
return;
}
final File parent = zipFile.getParentFile();
if (parent != null && !parent.isDirectory()
&& !(parent.mkdirs() || parent.isDirectory())) {
throw new BuildException(
"Failed to create missing parent directory for %s",
zipFile);
}
updatedFile = true;
if (!zipFile.exists() && state.isWithoutAnyResources()) {
createEmptyZip(zipFile);
return;
}
final Resource[][] addThem = state.getResourcesToAdd();
if (doUpdate) {
renamedFile = renameFile();
}
final String action = doUpdate ? "Updating " : "Building ";
if (!skipWriting) {
log(action + archiveType + ": " + zipFile.getAbsolutePath());
}
ZipOutputStream zOut = null;
try {
if (!skipWriting) {
zOut = new ZipOutputStream(zipFile);
zOut.setEncoding(encoding);
zOut.setUseLanguageEncodingFlag(useLanguageEncodingFlag);
zOut.setCreateUnicodeExtraFields(createUnicodeExtraFields.
getPolicy());
zOut.setFallbackToUTF8(fallBackToUTF8);
zOut.setMethod(doCompress
? ZipOutputStream.DEFLATED : ZipOutputStream.STORED);
zOut.setLevel(level);
zOut.setUseZip64(zip64Mode.getMode());
}
initZipOutputStream(zOut);
// Add the explicit resource collections to the archive.
for (int i = 0; i < fss.length; i++) {
if (addThem[i].length != 0) {
addResources(fss[i], addThem[i], zOut);
}
}
if (doUpdate) {
addingNewFiles = false;
final ZipFileSet oldFiles = new ZipFileSet();
oldFiles.setProject(getProject());
oldFiles.setSrc(renamedFile);
oldFiles.setDefaultexcludes(false);
for (String addedFile : addedFiles) {
oldFiles.createExclude().setName(addedFile);
}
final DirectoryScanner ds =
oldFiles.getDirectoryScanner(getProject());
((ZipScanner) ds).setEncoding(encoding);
Stream<String> includedResourceNames =
Stream.of(ds.getIncludedFiles());
if (!doFilesonly) {
includedResourceNames =
Stream.concat(includedResourceNames,
Stream.of(ds.getIncludedDirectories()));
}
Resource[] r = includedResourceNames.map(ds::getResource)
.toArray(Resource[]::new);
addResources(oldFiles, r, zOut);
}
if (zOut != null) {
zOut.setComment(comment);
}
finalizeZipOutputStream(zOut);
// If we've been successful on an update, delete the
// temporary file
if (doUpdate) {
if (!renamedFile.delete()) {
log("Warning: unable to delete temporary file "
+ renamedFile.getName(), Project.MSG_WARN);
}
}
success = true;
} finally {
// Close the output stream.
closeZout(zOut, success);
}
} catch (final IOException ioe) {
String msg = "Problem creating " + archiveType + ": "
+ ioe.getMessage();
// delete a bogus ZIP file (but only if it's not the original one)
if ((!doUpdate || renamedFile != null) && !zipFile.delete()) {
msg += " (and the archive is probably corrupt but I could not "
+ "delete it)";
}
if (doUpdate && renamedFile != null) {
try {
FILE_UTILS.rename(renamedFile, zipFile);
} catch (final IOException e) {
msg += " (and I couldn't rename the temporary file "
+ renamedFile.getName() + " back)";
}
}
throw new BuildException(msg, ioe, getLocation());
} finally {
cleanUp();
}
}
/** rename the zip file. */
private File renameFile() {
final File renamedFile = FILE_UTILS.createTempFile(
getProject(), "zip", ".tmp", zipFile.getParentFile(), true, false);
try {
FILE_UTILS.rename(zipFile, renamedFile);
} catch (final SecurityException | IOException e) {
throw new BuildException(
"Unable to rename old file (%s) to temporary file",
zipFile.getAbsolutePath());
}
return renamedFile;
}
/** Close zout */
private void closeZout(final ZipOutputStream zOut, final boolean success)
throws IOException {
if (zOut == null) {
return;
}
try {
zOut.close();
} catch (final IOException ex) {
// If we're in this finally clause because of an
// exception, we don't really care if there's an
// exception when closing the stream. E.g. if it
// throws "ZIP file must have at least one entry",
// because an exception happened before we added
// any files, then we must swallow this
// exception. Otherwise, the error that's reported
// will be the close() error, which is not the
// real cause of the problem.
if (success) {
throw ex;
}
}
}
/** Check the attributes and elements */
private void checkAttributesAndElements() {
if (baseDir == null && resources.isEmpty() && groupfilesets.isEmpty()
&& "zip".equals(archiveType)) {
throw new BuildException(
"basedir attribute must be set, or at least one resource collection must be given!");
}
if (zipFile == null) {
throw new BuildException("You must specify the %s file to create!",
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);
}
if (zipFile.exists() && !zipFile.canWrite()) {
throw new BuildException("%s is read-only.", zipFile);
}
}
/** Process doupdate */
private void processDoUpdate() {
// Whether or not an actual update is required -
// we don't need to update if the original file doesn't exist
if (doUpdate && !zipFile.exists()) {
doUpdate = false;
logWhenWriting("ignoring update attribute as " + archiveType
+ " doesn't exist.", Project.MSG_DEBUG);
}
}
/** Process groupfilesets */
private void processGroupFilesets() {
// Add the files found in groupfileset to fileset
for (FileSet fs : groupfilesets) {
logWhenWriting("Processing groupfileset ", Project.MSG_VERBOSE);
final FileScanner scanner = fs.getDirectoryScanner(getProject());
final File basedir = scanner.getBasedir();
for (String file : scanner.getIncludedFiles()) {
logWhenWriting("Adding file " + file + " to fileset",
Project.MSG_VERBOSE);
final ZipFileSet zf = new ZipFileSet();
zf.setProject(getProject());
zf.setSrc(new File(basedir, file));
add(zf);
filesetsFromGroupfilesets.add(zf);
}
}
}
/**
* Indicates if the task is adding new files into the archive as opposed to
* copying back unchanged files from the backup copy
* @return true if adding new files
*/
protected final boolean isAddingNewFiles() {
return addingNewFiles;
}
/**
* Add the given resources.
*
* @param fileset may give additional information like fullpath or
* permissions.
* @param resources the resources to add
* @param zOut the stream to write to
* @throws IOException on error
*
* @since Ant 1.5.2
*/
protected final void addResources(final FileSet fileset, final Resource[] resources,
final ZipOutputStream zOut)
throws IOException {
String prefix = "";
String fullpath = "";
int dirMode = ArchiveFileSet.DEFAULT_DIR_MODE;
int fileMode = ArchiveFileSet.DEFAULT_FILE_MODE;
ArchiveFileSet zfs = null;
if (fileset instanceof ArchiveFileSet) {
zfs = (ArchiveFileSet) fileset;
prefix = zfs.getPrefix(getProject());
fullpath = zfs.getFullpath(getProject());
dirMode = zfs.getDirMode(getProject());
fileMode = zfs.getFileMode(getProject());
}
if (!prefix.isEmpty() && !fullpath.isEmpty()) {
throw new BuildException(
"Both prefix and fullpath attributes must not be set on the same fileset.");
}
if (resources.length != 1 && !fullpath.isEmpty()) {
throw new BuildException(
"fullpath attribute may only be specified for filesets that specify a single file.");
}
if (!prefix.isEmpty()) {
if (!prefix.endsWith("/") && !prefix.endsWith("\\")) {
prefix += "/";
}
addParentDirs(null, prefix, zOut, "", dirMode);
}
ZipFile zf = null;
try {
boolean dealingWithFiles = false;
File base = null;
if (zfs == null || zfs.getSrc(getProject()) == null) {
dealingWithFiles = true;
base = fileset.getDir(getProject());
} else if (zfs instanceof ZipFileSet) {
zf = new ZipFile(zfs.getSrc(getProject()), encoding);
}
for (Resource resource : resources) {
String name;
if (fullpath.isEmpty()) {
name = resource.getName();
} else {
name = fullpath;
}
name = name.replace(File.separatorChar, '/');
if (name.isEmpty()) {
continue;
}
if (resource.isDirectory()) {
if (doFilesonly) {
continue;
}
final int thisDirMode = zfs != null && zfs.hasDirModeBeenSet()
? dirMode : getUnixMode(resource, zf, dirMode);
addDirectoryResource(resource, name, prefix,
base, zOut,
dirMode, thisDirMode);
} else { // !isDirectory
addParentDirs(base, name, zOut, prefix, dirMode);
if (dealingWithFiles) {
final File f = FILE_UTILS.resolveFile(base,
resource.getName());
zipFile(f, zOut, prefix + name, fileMode);
} else {
final int thisFileMode =
zfs != null && zfs.hasFileModeBeenSet()
? fileMode : getUnixMode(resource, zf,
fileMode);
addResource(resource, name, prefix,
zOut, thisFileMode, zf,
zfs == null
? null : zfs.getSrc(getProject()));
}
}
}
} finally {
if (zf != null) {
zf.close();
}
}
}
/**
* Add a directory entry to the archive using a specified
* Unix-mode and the default mode for its parent directories (if
* necessary).
*/
private void addDirectoryResource(final Resource r, String name, final String prefix,
final File base, final ZipOutputStream zOut,
final int defaultDirMode, final int thisDirMode)
throws IOException {
if (!name.endsWith("/")) {
name += "/";
}
final int nextToLastSlash = name.lastIndexOf('/', name.length() - 2);
if (nextToLastSlash != -1) {
addParentDirs(base, name.substring(0, nextToLastSlash + 1),
zOut, prefix, defaultDirMode);
}
zipDir(r, zOut, prefix + name, thisDirMode,
r instanceof ZipResource
? ((ZipResource) r).getExtraFields() : null);
}
/**
* Determine a Resource's Unix mode or return the given default
* value if not available.
*/
private int getUnixMode(final Resource r, final ZipFile zf, final int defaultMode) {
int unixMode = defaultMode;
if (zf != null) {
final ZipEntry ze = zf.getEntry(r.getName());
unixMode = ze.getUnixMode();
if ((unixMode == 0 || unixMode == UnixStat.DIR_FLAG)
&& !preserve0Permissions) {
unixMode = defaultMode;
}
} else if (r instanceof ArchiveResource) {
unixMode = ((ArchiveResource) r).getMode();
}
return unixMode;
}
/**
* Add a file entry.
*/
private void addResource(final Resource r, final String name, final String prefix,
final ZipOutputStream zOut, final int mode,
final ZipFile zf, final File fromArchive)
throws IOException {
if (zf != null) {
final ZipEntry ze = zf.getEntry(r.getName());
if (ze != null) {
final boolean oldCompress = doCompress;
if (keepCompression) {
doCompress = (ze.getMethod() == ZipEntry.DEFLATED);
}
try (final BufferedInputStream is = new BufferedInputStream(zf.getInputStream(ze))) {
zipFile(is, zOut, prefix + name, ze.getTime(),
fromArchive, mode, ze.getExtraFields(true));
} finally {
doCompress = oldCompress;
}
}
} else {
try (final BufferedInputStream is = new BufferedInputStream(r.getInputStream())) {
zipFile(is, zOut, prefix + name, r.getLastModified(),
fromArchive, mode, r instanceof ZipResource
? ((ZipResource) r).getExtraFields() : null);
}
}
}
/**
* Add the given resources.
*
* @param rc may give additional information like fullpath or
* permissions.
* @param resources the resources to add
* @param zOut the stream to write to
* @throws IOException on error
*
* @since Ant 1.7
*/
protected final void addResources(final ResourceCollection rc,
final Resource[] resources,
final ZipOutputStream zOut)
throws IOException {
if (rc instanceof FileSet) {
addResources((FileSet) rc, resources, zOut);
return;
}
for (final Resource resource : resources) {
String name = resource.getName();
if (name == null) {
continue;
}
name = name.replace(File.separatorChar, '/');
if (name.isEmpty()) {
continue;
}
if (resource.isDirectory() && doFilesonly) {
continue;
}
File base = null;
final FileProvider fp = resource.as(FileProvider.class);
if (fp != null) {
base = ResourceUtils.asFileResource(fp).getBaseDir();
}
if (resource.isDirectory()) {
addDirectoryResource(resource, name, "", base, zOut,
ArchiveFileSet.DEFAULT_DIR_MODE,
ArchiveFileSet.DEFAULT_DIR_MODE);
} else {
addParentDirs(base, name, zOut, "",
ArchiveFileSet.DEFAULT_DIR_MODE);
if (fp != null) {
final File f = (fp).getFile();
zipFile(f, zOut, name, ArchiveFileSet.DEFAULT_FILE_MODE);
} else {
addResource(resource, name, "", zOut,
ArchiveFileSet.DEFAULT_FILE_MODE,
null, null);
}
}
}
}
/**
* method for subclasses to override
* @param zOut the zip output stream
* @throws IOException on output error
* @throws BuildException on other errors
*/
protected void initZipOutputStream(final ZipOutputStream zOut)
throws IOException, BuildException {
}
/**
* method for subclasses to override
* @param zOut the zip output stream
* @throws IOException on output error
* @throws BuildException on other errors
*/
protected void finalizeZipOutputStream(final ZipOutputStream zOut)
throws IOException, BuildException {
}
/**
* Create an empty zip file
* @param zipFile the zip file
* @return true for historic reasons
* @throws BuildException on error
*/
protected boolean createEmptyZip(final File zipFile) throws BuildException {
// In this case using java.util.zip will not work
// because it does not permit a zero-entry archive.
// Must create it manually.
if (!skipWriting) {
log("Note: creating empty " + archiveType + " archive " + zipFile,
Project.MSG_INFO);
}
try (OutputStream os = Files.newOutputStream(zipFile.toPath())) {
// CheckStyle:MagicNumber OFF
// Cf. PKZIP specification.
final byte[] empty = new byte[22];
empty[0] = 80; // P
empty[1] = 75; // K
empty[2] = 5;
empty[3] = 6;
// remainder zeros
// CheckStyle:MagicNumber ON
os.write(empty);
} catch (final IOException ioe) {
throw new BuildException("Could not create empty ZIP archive "
+ "(" + ioe.getMessage() + ")", ioe,
getLocation());
}
return true;
}
/**
* @since Ant 1.5.2
*/
private synchronized ZipScanner getZipScanner() {
if (zs == null) {
zs = new ZipScanner();
zs.setEncoding(encoding);
zs.setSrc(zipFile);
}
return zs;
}
/**
* Collect the resources that are newer than the corresponding
* entries (or missing) in the original archive.
*
* <p>If we are going to recreate the archive instead of updating
* it, all resources should be considered as new, if a single one
* is. Because of this, subclasses overriding this method must
* call <code>super.getResourcesToAdd</code> and indicate with the
* third arg if they already know that the archive is
* out-of-date.</p>
*
* <p>This method first delegates to getNonFileSetResourcesToAdd
* and then invokes the FileSet-arg version. All this to keep
* backwards compatibility for subclasses that don't know how to
* deal with non-FileSet ResourceCollections.</p>
*
* @param rcs The resource collections to grab resources from
* @param zipFile intended archive file (may or may not exist)
* @param needsUpdate whether we already know that the archive is
* out-of-date. Subclasses overriding this method are supposed to
* set this value correctly in their call to
* <code>super.getResourcesToAdd</code>.
* @return an array of resources to add for each fileset passed in as well
* as a flag that indicates whether the archive is uptodate.
*
* @exception BuildException if it likes
* @since Ant 1.7
*/
protected ArchiveState getResourcesToAdd(final ResourceCollection[] rcs,
final File zipFile,
final boolean needsUpdate)
throws BuildException {
final List<FileSet> filesets = new ArrayList<>();
final List<ResourceCollection> rest = new ArrayList<>();
for (ResourceCollection rc : rcs) {
if (rc instanceof FileSet) {
filesets.add((FileSet) rc);
} else {
rest.add(rc);
}
}
final ResourceCollection[] rc =
rest.toArray(new ResourceCollection[rest.size()]);
ArchiveState as = getNonFileSetResourcesToAdd(rc, zipFile,
needsUpdate);
final FileSet[] fs = filesets.toArray(new FileSet[filesets.size()]);
final ArchiveState as2 = getResourcesToAdd(fs, zipFile, as.isOutOfDate());
if (!as.isOutOfDate() && as2.isOutOfDate()) {
/*
* Bad luck.
*
* There are resources in the filesets that make the
* archive out of date, but not in the non-fileset
* resources. We need to rescan the non-FileSets to grab
* all of them now.
*/
as = getNonFileSetResourcesToAdd(rc, zipFile, true);
}
final Resource[][] toAdd = new Resource[rcs.length][];
int fsIndex = 0;
int restIndex = 0;
for (int i = 0; i < rcs.length; i++) {
if (rcs[i] instanceof FileSet) {
toAdd[i] = as2.getResourcesToAdd()[fsIndex++];
} else {
toAdd[i] = as.getResourcesToAdd()[restIndex++];
}
}
return new ArchiveState(as2.isOutOfDate(), toAdd);
}
/*
* This is yet another hacky construct to extend the FileSet[]
* getResourcesToAdd method so we can pass the information whether
* non-fileset resources have been available to it without having
* to move the withEmpty behavior checks (since either would break
* subclasses in several ways).
*/
private static final ThreadLocal<Boolean> HAVE_NON_FILE_SET_RESOURCES_TO_ADD = ThreadLocal.withInitial(() -> Boolean.FALSE);
/**
* Collect the resources that are newer than the corresponding
* entries (or missing) in the original archive.
*
* <p>If we are going to recreate the archive instead of updating
* it, all resources should be considered as new, if a single one
* is. Because of this, subclasses overriding this method must
* call <code>super.getResourcesToAdd</code> and indicate with the
* third arg if they already know that the archive is
* out-of-date.</p>
*
* @param filesets The filesets to grab resources from
* @param zipFile intended archive file (may or may not exist)
* @param needsUpdate whether we already know that the archive is
* out-of-date. Subclasses overriding this method are supposed to
* set this value correctly in their call to
* <code>super.getResourcesToAdd</code>.
* @return an array of resources to add for each fileset passed in as well
* as a flag that indicates whether the archive is uptodate.
*
* @exception BuildException if it likes
*/
protected ArchiveState getResourcesToAdd(final FileSet[] filesets,
final File zipFile,
boolean needsUpdate)
throws BuildException {
final Resource[][] initialResources = grabResources(filesets);
if (isEmpty(initialResources)) {
if (Boolean.FALSE.equals(HAVE_NON_FILE_SET_RESOURCES_TO_ADD.get())) {
if (needsUpdate && doUpdate) {
/*
* This is a rather hairy case.
*
* One of our subclasses knows that we need to
* update the archive, but at the same time, there
* are no resources known to us that would need to
* be added. Only the subclass seems to know
* what's going on.
*
* This happens if <jar> detects that the manifest
* has changed, for example. The manifest is not
* part of any resources because of our support
* for inline <manifest>s.
*
* If we invoke createEmptyZip like Ant 1.5.2 did,
* we'll loose all stuff that has been in the
* original archive (bugzilla report 17780).
*/
return new ArchiveState(true, initialResources);
}
if ("skip".equals(emptyBehavior)) {
if (doUpdate) {
logWhenWriting(archiveType + " archive " + zipFile
+ " not updated because no new files were"
+ " included.", Project.MSG_VERBOSE);
} else {
logWhenWriting("Warning: skipping " + archiveType
+ " archive " + zipFile
+ " because no files were included.",
Project.MSG_WARN);
}
} else if ("fail".equals(emptyBehavior)) {
throw new BuildException("Cannot create " + archiveType
+ " archive " + zipFile
+ ": no files were included.",
getLocation());
} else {
// Create.
if (!zipFile.exists()) {
needsUpdate = true;
}
}
}
// either there are non-fileset resources or we
// (re-)create the archive anyway
return new ArchiveState(needsUpdate, initialResources);
}
// initialResources is not empty
if (!zipFile.exists()) {
return new ArchiveState(true, initialResources);
}
if (needsUpdate && !doUpdate) {
// we are recreating the archive, need all resources
return new ArchiveState(true, initialResources);
}
final Resource[][] newerResources = new Resource[filesets.length][];
for (int i = 0; i < filesets.length; i++) {
if (!(fileset instanceof ZipFileSet)
|| ((ZipFileSet) fileset).getSrc(getProject()) == null) {
final File base = filesets[i].getDir(getProject());
for (int j = 0; j < initialResources[i].length; j++) {
final File resourceAsFile =
FILE_UTILS.resolveFile(base,
initialResources[i][j].getName());
if (resourceAsFile.equals(zipFile)) {
throw new BuildException("A zip file cannot include "
+ "itself", getLocation());
}
}
}
}
for (int i = 0; i < filesets.length; i++) {
if (initialResources[i].length == 0) {
newerResources[i] = new Resource[] {};
continue;
}
FileNameMapper myMapper = new IdentityMapper();
if (filesets[i] instanceof ZipFileSet) {
final ZipFileSet zfs = (ZipFileSet) filesets[i];
if (zfs.getFullpath(getProject()) != null
&& !zfs.getFullpath(getProject()).isEmpty()) {
// in this case all files from origin map to
// the fullPath attribute of the zipfileset at
// destination
final MergingMapper fm = new MergingMapper();
fm.setTo(zfs.getFullpath(getProject()));
myMapper = fm;
} else if (zfs.getPrefix(getProject()) != null
&& !zfs.getPrefix(getProject()).isEmpty()) {
final GlobPatternMapper gm = new GlobPatternMapper();
gm.setFrom("*");
String prefix = zfs.getPrefix(getProject());
if (!prefix.endsWith("/") && !prefix.endsWith("\\")) {
prefix += "/";
}
gm.setTo(prefix + "*");
myMapper = gm;
}
}
newerResources[i] = selectOutOfDateResources(initialResources[i],
myMapper);
needsUpdate = needsUpdate || (newerResources[i].length > 0);
if (needsUpdate && !doUpdate) {
// we will return initialResources anyway, no reason
// to scan further.
break;
}
}
if (needsUpdate && !doUpdate) {
// we are recreating the archive, need all resources
return new ArchiveState(true, initialResources);
}
return new ArchiveState(needsUpdate, newerResources);
}
/**
* Collect the resources that are newer than the corresponding
* entries (or missing) in the original archive.
*
* <p>If we are going to recreate the archive instead of updating
* it, all resources should be considered as new, if a single one
* is. Because of this, subclasses overriding this method must
* call <code>super.getResourcesToAdd</code> and indicate with the
* third arg if they already know that the archive is
* out-of-date.</p>
*
* @param rcs The filesets to grab resources from
* @param zipFile intended archive file (may or may not exist)
* @param needsUpdate whether we already know that the archive is
* out-of-date. Subclasses overriding this method are supposed to
* set this value correctly in their call to
* <code>super.getResourcesToAdd</code>.
* @return an array of resources to add for each fileset passed in as well
* as a flag that indicates whether the archive is uptodate.
*
* @exception BuildException if it likes
*/
protected ArchiveState getNonFileSetResourcesToAdd(final ResourceCollection[] rcs,
final File zipFile,
boolean needsUpdate)
throws BuildException {
/*
* Backwards compatibility forces us to repeat the logic of
* getResourcesToAdd(FileSet[], ...) here once again.
*/
final Resource[][] initialResources = grabNonFileSetResources(rcs);
final boolean empty = isEmpty(initialResources);
HAVE_NON_FILE_SET_RESOURCES_TO_ADD.set(!empty);
if (empty) {
// no emptyBehavior handling since the FileSet version
// will take care of it.
return new ArchiveState(needsUpdate, initialResources);
}
// initialResources is not empty
if (!zipFile.exists()) {
return new ArchiveState(true, initialResources);
}
if (needsUpdate && !doUpdate) {
// we are recreating the archive, need all resources
return new ArchiveState(true, initialResources);
}
final Resource[][] newerResources = new Resource[rcs.length][];
for (int i = 0; i < rcs.length; i++) {
if (initialResources[i].length == 0) {
newerResources[i] = new Resource[] {};
continue;
}
for (int j = 0; j < initialResources[i].length; j++) {
final FileProvider fp =
initialResources[i][j].as(FileProvider.class);
if (fp != null && zipFile.equals(fp.getFile())) {
throw new BuildException("A zip file cannot include itself",
getLocation());
}
}
newerResources[i] = selectOutOfDateResources(initialResources[i],
new IdentityMapper());
needsUpdate = needsUpdate || (newerResources[i].length > 0);
if (needsUpdate && !doUpdate) {
// we will return initialResources anyway, no reason
// to scan further.
break;
}
}
if (needsUpdate && !doUpdate) {
// we are recreating the archive, need all resources
return new ArchiveState(true, initialResources);
}
return new ArchiveState(needsUpdate, newerResources);
}
private Resource[] selectOutOfDateResources(final Resource[] initial,
final FileNameMapper mapper) {
final Resource[] rs = selectFileResources(initial);
Resource[] result =
ResourceUtils.selectOutOfDateSources(this, rs, mapper,
getZipScanner(),
ZIP_FILE_TIMESTAMP_GRANULARITY);
if (!doFilesonly) {
final Union u = new Union();
u.addAll(Arrays.asList(selectDirectoryResources(initial)));
final ResourceCollection rc =
ResourceUtils.selectSources(this, u, mapper,
getZipScanner(),
MISSING_DIR_PROVIDER);
if (!rc.isEmpty()) {
final List<Resource> newer = new ArrayList<>();
newer.addAll(Arrays.asList(((Union) rc).listResources()));
newer.addAll(Arrays.asList(result));
result = newer.toArray(result);
}
}
return result;
}
/**
* Fetch all included and not excluded resources from the sets.
*
* <p>Included directories will precede included files.</p>
* @param filesets an array of filesets
* @return the resources included
* @since Ant 1.5.2
*/
protected Resource[][] grabResources(final FileSet[] filesets) {
final Resource[][] result = new Resource[filesets.length][];
for (int i = 0; i < filesets.length; i++) {
boolean skipEmptyNames = true;
if (filesets[i] instanceof ZipFileSet) {
final ZipFileSet zfs = (ZipFileSet) filesets[i];
skipEmptyNames = zfs.getPrefix(getProject()).isEmpty()
&& zfs.getFullpath(getProject()).isEmpty();
}
final DirectoryScanner rs =
filesets[i].getDirectoryScanner(getProject());
if (rs instanceof ZipScanner) {
((ZipScanner) rs).setEncoding(encoding);
}
final List<Resource> resources = new Vector<>();
if (!doFilesonly) {
for (String d : rs.getIncludedDirectories()) {
if (!d.isEmpty() || !skipEmptyNames) {
resources.add(rs.getResource(d));
}
}
}
for (String f : rs.getIncludedFiles()) {
if (!f.isEmpty() || !skipEmptyNames) {
resources.add(rs.getResource(f));
}
}
result[i] = resources.toArray(new Resource[resources.size()]);
}
return result;
}
/**
* Fetch all included and not excluded resources from the collections.
*
* <p>Included directories will precede included files.</p>
* @param rcs an array of resource collections
* @return the resources included
* @since Ant 1.7
*/
protected Resource[][] grabNonFileSetResources(final ResourceCollection[] rcs) {
final Resource[][] result = new Resource[rcs.length][];
for (int i = 0; i < rcs.length; i++) {
final List<Resource> dirs = new ArrayList<>();
final List<Resource> files = new ArrayList<>();
for (final Resource r : rcs[i]) {
if (r.isDirectory()) {
dirs.add(r);
} else if (r.isExists()) {
files.add(r);
}
}
// make sure directories are in alpha-order - this also
// ensures parents come before their children
dirs.sort(Comparator.comparing(Resource::getName));
final List<Resource> rs = new ArrayList<>(dirs);
rs.addAll(files);
result[i] = rs.toArray(new Resource[rs.size()]);
}
return result;
}
/**
* Add a directory to the zip stream.
* @param dir the directory to add to the archive
* @param zOut the stream to write to
* @param vPath the name this entry shall have in the archive
* @param mode the Unix permissions to set.
* @throws IOException on error
* @since Ant 1.5.2
*/
protected void zipDir(final File dir, final ZipOutputStream zOut, final String vPath,
final int mode)
throws IOException {
zipDir(dir, zOut, vPath, mode, null);
}
/**
* Add a directory to the zip stream.
* @param dir the directory to add to the archive
* @param zOut the stream to write to
* @param vPath the name this entry shall have in the archive
* @param mode the Unix permissions to set.
* @param extra ZipExtraFields to add
* @throws IOException on error
* @since Ant 1.6.3
*/
protected void zipDir(final File dir, final ZipOutputStream zOut, final String vPath,
final int mode, final ZipExtraField[] extra)
throws IOException {
zipDir(dir == null ? null : new FileResource(dir), zOut, vPath, mode,
extra);
}
/**
* Add a directory to the zip stream.
* @param dir the directory to add to the archive
* @param zOut the stream to write to
* @param vPath the name this entry shall have in the archive
* @param mode the Unix permissions to set.
* @param extra ZipExtraFields to add
* @throws IOException on error
* @since Ant 1.8.0
*/
protected void zipDir(final Resource dir, final ZipOutputStream zOut, final String vPath,
final int mode, final ZipExtraField[] extra)
throws IOException {
if (doFilesonly) {
logWhenWriting("skipping directory " + vPath
+ " for file-only archive",
Project.MSG_VERBOSE);
return;
}
if (addedDirs.get(vPath) != null) {
// don't add directories we've already added.
// no warning if we try, it is harmless in and of itself
return;
}
logWhenWriting("adding directory " + vPath, Project.MSG_VERBOSE);
addedDirs.put(vPath, vPath);
if (!skipWriting) {
final ZipEntry ze = new ZipEntry(vPath);
// ZIPs store time with a granularity of 2 seconds, round up
final int millisToAdd = roundUp ? ROUNDUP_MILLIS : 0;
if (fixedModTime != null) {
ze.setTime(modTimeMillis);
} else if (dir != null && dir.isExists()) {
ze.setTime(dir.getLastModified() + millisToAdd);
} else {
ze.setTime(System.currentTimeMillis() + millisToAdd);
}
ze.setSize(0);
ze.setMethod(ZipEntry.STORED);
// This is faintly ridiculous:
ze.setCrc(EMPTY_CRC);
ze.setUnixMode(mode);
if (extra != null) {
ze.setExtraFields(extra);
}
zOut.putNextEntry(ze);
}
}
/*
* This is a hacky construct to extend the zipFile method to
* support a new parameter (extra fields to preserve) without
* breaking subclasses that override the old method signature.
*/
private static final ThreadLocal<ZipExtraField[]> CURRENT_ZIP_EXTRA = new ThreadLocal<>();
/**
* Provides the extra fields for the zip entry currently being
* added to the archive - if any.
* @return ZipExtraField[]
* @since Ant 1.8.0
*/
protected final ZipExtraField[] getCurrentExtraFields() {
return CURRENT_ZIP_EXTRA.get();
}
/**
* Sets the extra fields for the zip entry currently being
* added to the archive - if any.
* @param extra ZipExtraField[]
* @since Ant 1.8.0
*/
protected final void setCurrentExtraFields(final ZipExtraField[] extra) {
CURRENT_ZIP_EXTRA.set(extra);
}
/**
* Adds a new entry to the archive, takes care of duplicates as well.
*
* @param in the stream to read data for the entry from. The
* caller of the method is responsible for closing the stream.
* @param zOut the stream to write to.
* @param vPath the name this entry shall have in the archive.
* @param lastModified last modification time for the entry.
* @param fromArchive the original archive we are copying this
* entry from, will be null if we are not copying from an archive.
* @param mode the Unix permissions to set.
*
* @since Ant 1.5.2
* @throws IOException on error
*/
protected void zipFile(final InputStream in, final ZipOutputStream zOut, final String vPath,
final long lastModified, final File fromArchive, final int mode)
throws IOException {
// fromArchive is used in subclasses overriding this method
if (entries.containsKey(vPath)) {
if ("preserve".equals(duplicate)) {
logWhenWriting(vPath + " already added, skipping",
Project.MSG_INFO);
return;
}
if ("fail".equals(duplicate)) {
throw new BuildException(
"Duplicate file %s was found and the duplicate attribute is 'fail'.",
vPath);
}
// duplicate equal to add, so we continue
logWhenWriting("duplicate file " + vPath
+ " found, adding.", Project.MSG_VERBOSE);
} else {
logWhenWriting("adding entry " + vPath, Project.MSG_VERBOSE);
}
entries.put(vPath, vPath);
if (!skipWriting) {
final ZipEntry ze = new ZipEntry(vPath);
ze.setTime(fixedModTime != null ? modTimeMillis : lastModified);
ze.setMethod(doCompress ? ZipEntry.DEFLATED : ZipEntry.STORED);
// if the input stream doesn't support mark/reset ability, we wrap it in a
// stream that adds that support.
// Note: We do *not* close this newly created wrapping input stream, since
// we don't "own" the underlying input stream that's passed to us and closing
// that is the responsibility of the caller.
final InputStream markableInputStream = in.markSupported() ? in : new BufferedInputStream(in);
/*
* ZipOutputStream.putNextEntry expects the ZipEntry to
* know its size and the CRC sum before you start writing
* the data when using STORED mode - unless it is seekable.
*
* This forces us to process the data twice.
*/
if (!zOut.isSeekable() && !doCompress) {
long size = 0;
final CRC32 cal = new CRC32();
markableInputStream.mark(Integer.MAX_VALUE);
final byte[] buffer = new byte[BUFFER_SIZE];
int count = 0;
do {
size += count;
cal.update(buffer, 0, count);
count = markableInputStream.read(buffer, 0, buffer.length);
} while (count != -1);
markableInputStream.reset();
ze.setSize(size);
ze.setCrc(cal.getValue());
}
ze.setUnixMode(mode);
final ZipExtraField[] extra = getCurrentExtraFields();
if (extra != null) {
ze.setExtraFields(extra);
}
zOut.putNextEntry(ze);
final byte[] buffer = new byte[BUFFER_SIZE];
int count = 0;
do {
if (count != 0) {
zOut.write(buffer, 0, count);
}
count = markableInputStream.read(buffer, 0, buffer.length);
} while (count != -1);
}
addedFiles.add(vPath);
}
/**
* Adds a new entry to the archive, takes care of duplicates as well.
*
* @param in the stream to read data for the entry from. The
* caller of the method is responsible for closing the stream.
* @param zOut the stream to write to.
* @param vPath the name this entry shall have in the archive.
* @param lastModified last modification time for the entry.
* @param fromArchive the original archive we are copying this
* entry from, will be null if we are not copying from an archive.
* @param mode the Unix permissions to set.
* @param extra ZipExtraFields to add
*
* @since Ant 1.8.0
* @throws IOException on error
*/
protected final void zipFile(final InputStream in, final ZipOutputStream zOut,
final String vPath, final long lastModified,
final File fromArchive, final int mode,
final ZipExtraField[] extra)
throws IOException {
try {
setCurrentExtraFields(extra);
zipFile(in, zOut, vPath, lastModified, fromArchive, mode);
} finally {
setCurrentExtraFields(null);
}
}
/**
* Method that gets called when adding from <code>java.io.File</code> instances.
*
* <p>This implementation delegates to the six-arg version.</p>
*
* @param file the file to add to the archive
* @param zOut the stream to write to
* @param vPath the name this entry shall have in the archive
* @param mode the Unix permissions to set.
* @throws IOException on error
*
* @since Ant 1.5.2
*/
protected void zipFile(final File file, final ZipOutputStream zOut, final String vPath,
final int mode)
throws IOException {
if (file.equals(zipFile)) {
throw new BuildException("A zip file cannot include itself",
getLocation());
}
try (final BufferedInputStream bIn = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
// ZIPs store time with a granularity of 2 seconds, round up
zipFile(bIn, zOut, vPath,
file.lastModified() + (roundUp ? ROUNDUP_MILLIS : 0),
null, mode);
}
}
/**
* Ensure all parent dirs of a given entry have been added.
* @param baseDir the base directory to use (may be null)
* @param entry the entry name to create directories from
* @param zOut the stream to write to
* @param prefix a prefix to place on the created entries
* @param dirMode the directory mode
* @throws IOException on error
* @since Ant 1.5.2
*/
protected final void addParentDirs(final File baseDir, final String entry,
final ZipOutputStream zOut, final String prefix,
final int dirMode)
throws IOException {
if (!doFilesonly) {
final Stack<String> directories = new Stack<>();
int slashPos = entry.length();
while ((slashPos = entry.lastIndexOf('/', slashPos - 1)) != -1) {
final String dir = entry.substring(0, slashPos + 1);
if (addedDirs.get(prefix + dir) != null) {
break;
}
directories.push(dir);
}
while (!directories.isEmpty()) {
final String dir = directories.pop();
File f;
if (baseDir != null) {
f = new File(baseDir, dir);
} else {
f = new File(dir);
}
zipDir(f, zOut, prefix + dir, dirMode);
}
}
}
/**
* Do any clean up necessary to allow this instance to be used again.
*
* <p>When we get here, the Zip file has been closed and all we
* need to do is to reset some globals.</p>
*
* <p>This method will only reset globals that have been changed
* during execute(), it will not alter the attributes or nested
* child elements. If you want to reset the instance so that you
* can later zip a completely different set of files, you must use
* the reset method.</p>
*
* @see #reset
*/
protected void cleanUp() {
addedDirs.clear();
addedFiles.clear();
entries.clear();
addingNewFiles = false;
doUpdate = savedDoUpdate;
resources.removeAll(filesetsFromGroupfilesets);
filesetsFromGroupfilesets.clear();
HAVE_NON_FILE_SET_RESOURCES_TO_ADD.set(Boolean.FALSE);
}
/**
* Makes this instance reset all attributes to their default
* values and forget all children.
*
* @since Ant 1.5
*
* @see #cleanUp
*/
public void reset() {
resources.clear();
zipFile = null;
baseDir = null;
groupfilesets.clear();
duplicate = "add";
archiveType = "zip";
doCompress = true;
emptyBehavior = "skip";
doUpdate = false;
doFilesonly = false;
encoding = null;
}
/**
* Check is the resource arrays are empty.
* @param r the arrays to check
* @return true if all individual arrays are empty
*
* @since Ant 1.5.2
*/
protected static final boolean isEmpty(final Resource[][] r) {
for (Resource[] element : r) {
if (element.length > 0) {
return false;
}
}
return true;
}
/**
* Drops all non-file resources from the given array.
* @param orig the resources to filter
* @return the filters resources
* @since Ant 1.6
*/
protected Resource[] selectFileResources(final Resource[] orig) {
return selectResources(orig,
r -> {
if (!r.isDirectory()) {
return true;
}
if (doFilesonly) {
logWhenWriting("Ignoring directory "
+ r.getName()
+ " as only files will"
+ " be added.",
Project.MSG_VERBOSE);
}
return false;
});
}
/**
* Drops all non-directory resources from the given array.
* @param orig the resources to filter
* @return the filters resources
* @since Ant 1.8.0
*/
protected Resource[] selectDirectoryResources(final Resource[] orig) {
return selectResources(orig, Resource::isDirectory);
}
/**
* Drops all resources from the given array that are not selected
* @param orig the resources to filter
* @param selector ResourceSelector
* @return the filters resources
* @since Ant 1.8.0
*/
protected Resource[] selectResources(final Resource[] orig,
final ResourceSelector selector) {
if (orig.length == 0) {
return orig;
}
Resource[] result = Stream.of(orig).filter(selector::isSelected)
.toArray(Resource[]::new);
return result.length == orig.length ? orig : result;
}
/**
* Logs a message at the given output level, but only if this is
* the pass that will actually create the archive.
*
* @param msg String
* @param level int
* @since Ant 1.8.0
*/
protected void logWhenWriting(final String msg, final int level) {
if (!skipWriting) {
log(msg, level);
}
}
/**
* Possible behaviors when a duplicate file is added:
* "add", "preserve" or "fail"
*/
public static class Duplicate extends EnumeratedAttribute {
/**
* @see EnumeratedAttribute#getValues()
* {@inheritDoc}
*/
@Override
public String[] getValues() {
return new String[] {"add", "preserve", "fail"};
}
}
/**
* Holds the up-to-date status and the out-of-date resources of
* the original archive.
*
* @since Ant 1.5.3
*/
public static class ArchiveState {
private final boolean outOfDate;
private final Resource[][] resourcesToAdd;
ArchiveState(final boolean state, final Resource[][] r) {
outOfDate = state;
resourcesToAdd = r;
}
/**
* Return the outofdate status.
* @return the outofdate status
*/
public boolean isOutOfDate() {
return outOfDate;
}
/**
* Get the resources to add.
* @return the resources to add
*/
public Resource[][] getResourcesToAdd() {
return resourcesToAdd;
}
/**
* find out if there are absolutely no resources to add
* @since Ant 1.6.3
* @return true if there are no resources to add
*/
public boolean isWithoutAnyResources() {
if (resourcesToAdd == null) {
return true;
}
for (Resource[] element : resourcesToAdd) {
if (element != null && element.length > 0) {
return false;
}
}
return true;
}
}
/**
* Policy for creation of Unicode extra fields: never, always or
* not-encodeable.
*
* @since Ant 1.8.0
*/
public static final class UnicodeExtraField extends EnumeratedAttribute {
private static final Map<String, UnicodeExtraFieldPolicy> POLICIES = new HashMap<>();
private static final String NEVER_KEY = "never";
private static final String ALWAYS_KEY = "always";
private static final String N_E_KEY = "not-encodeable";
static {
POLICIES.put(NEVER_KEY,
ZipOutputStream.UnicodeExtraFieldPolicy.NEVER);
POLICIES.put(ALWAYS_KEY,
ZipOutputStream.UnicodeExtraFieldPolicy.ALWAYS);
POLICIES.put(N_E_KEY,
ZipOutputStream.UnicodeExtraFieldPolicy
.NOT_ENCODEABLE);
}
@Override
public String[] getValues() {
return new String[] {NEVER_KEY, ALWAYS_KEY, N_E_KEY};
}
public static final UnicodeExtraField NEVER =
new UnicodeExtraField(NEVER_KEY);
private UnicodeExtraField(final String name) {
setValue(name);
}
public UnicodeExtraField() {
}
public ZipOutputStream.UnicodeExtraFieldPolicy getPolicy() {
return POLICIES.get(getValue());
}
}
/**
* The choices for Zip64 extensions.
*
* <p><b>never</b>: never add any Zip64 extensions. This will
* cause the task to fail if you try to add entries bigger than
* 4GB or create an archive bigger than 4GB or holding more that
* 65535 entries.</p>
*
* <p><b>as-needed</b>: create Zip64 extensions only when the
* entry's size is bigger than 4GB or one of the archive limits is
* hit. This mode also adds partial Zip64 extensions for all
* deflated entries written by Ant.</p>
*
* <p><b>always</b>: create Zip64 extensions for all entries.</p>
*
* <p><b>Note</b> some ZIP implementations don't handle Zip64
* extensions well and others may fail if the Zip64 extra field
* data is only present inside the local file header but not the
* central directory - which is what <em>as-needed</em> may result
* in. Java5 and Microsoft Visual Studio's Extension loader are
* known to fconsider the archive broken in such cases. If you
* are targeting such an archiver use the value <em>never</em>
* unless you know you need Zip64 extensions.</p>
*
* @since Ant 1.9.1
*/
public static final class Zip64ModeAttribute extends EnumeratedAttribute {
private static final Map<String, Zip64Mode> MODES = new HashMap<>();
private static final String NEVER_KEY = "never";
private static final String ALWAYS_KEY = "always";
private static final String A_N_KEY = "as-needed";
static {
MODES.put(NEVER_KEY, Zip64Mode.Never);
MODES.put(ALWAYS_KEY, Zip64Mode.Always);
MODES.put(A_N_KEY, Zip64Mode.AsNeeded);
}
@Override
public String[] getValues() {
return new String[] {NEVER_KEY, ALWAYS_KEY, A_N_KEY};
}
public static final Zip64ModeAttribute NEVER =
new Zip64ModeAttribute(NEVER_KEY);
public static final Zip64ModeAttribute AS_NEEDED =
new Zip64ModeAttribute(A_N_KEY);
private Zip64ModeAttribute(final String name) {
setValue(name);
}
public Zip64ModeAttribute() {
}
public Zip64Mode getMode() {
return MODES.get(getValue());
}
}
}