/* * 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 * * http://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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; 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.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<>(); /** * 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="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/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. * @since Ant 1.8.0 */ public void setPreserve0Permissions(final boolean b) { preserve0Permissions = b; } /** * Assume 0 Unix mode is intentional. * @since Ant 1.8.0 */ public boolean getPreserve0Permissions() { return preserve0Permissions; } /** * Whether to set the language encoding flag. * @since Ant 1.8.0 */ public void setUseLanguageEncodingFlag(final boolean b) { useLanguageEncodingFlag = b; } /** * Whether the language encoding flag will be used. * @since Ant 1.8.0 */ public boolean getUseLanguageEnodingFlag() { return useLanguageEncodingFlag; } /** * Whether Unicode extra fields will be created. * @since Ant 1.8.0 */ public void setCreateUnicodeExtraFields(final UnicodeExtraField b) { createUnicodeExtraFields = b; } /** * Whether Unicode extra fields will be created. * @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> * * @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. * * @since Ant 1.8.0 */ public boolean getFallBackToUTF8() { return fallBackToUTF8; } /** * Whether Zip64 extensions should be used. * @since Ant 1.9.1 */ public void setZip64Mode(final Zip64ModeAttribute b) { zip64Mode = b; } /** * Whether Zip64 extensions will be used. * @since Ant 1.9.1 */ public Zip64ModeAttribute getZip64Mode() { return zip64Mode; } /** * validate and build * @throws BuildException on error */ @Override public void execute() throws BuildException { if (doubleFilePass) { skipWriting = true; executeMain(); skipWriting = false; executeMain(); } else { 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 = 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( "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 (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.length() > 0 && fullpath.length() > 0) { throw new BuildException( "Both prefix and fullpath attributes must not be set on the same fileset."); } if (resources.length != 1 && fullpath.length() > 0) { 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 = 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 (InputStream is = zf.getInputStream(ze)) { zipFile(is, zOut, prefix + name, ze.getTime(), fromArchive, mode, ze.getExtraFields(true)); } finally { doCompress = oldCompress; } } } else { try (InputStream is = 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<ResourceCollection> filesets = new ArrayList<>(); final List<ResourceCollection> rest = new ArrayList<>(); for (ResourceCollection rc : rcs) { if (rc instanceof FileSet) { filesets.add(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 = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return 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()).equals("")) { // 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()).equals("")) { 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(Boolean.valueOf(!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 Collections.sort(dirs, 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 directort 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 (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. * @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. * @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(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(lastModified); ze.setMethod(doCompress ? ZipEntry.DEFLATED : ZipEntry.STORED); /* * 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(); if (!in.markSupported()) { // Store data into a byte[] final ByteArrayOutputStream bos = new ByteArrayOutputStream(); final byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; do { size += count; cal.update(buffer, 0, count); bos.write(buffer, 0, count); count = in.read(buffer, 0, buffer.length); } while (count != -1); in = new ByteArrayInputStream(bos.toByteArray()); } else { in.mark(Integer.MAX_VALUE); final byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; do { size += count; cal.update(buffer, 0, count); count = in.read(buffer, 0, buffer.length); } while (count != -1); in.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 = in.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 (InputStream fIn = Files.newInputStream(file.toPath())) { // ZIPs store time with a granularity of 2 seconds, round up zipFile(fIn, 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; filesetsFromGroupfilesets.forEach(resources::remove); 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 * @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. * * @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; } } /** * Policiy 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 uset 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()); } } }