| /* |
| * 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]][ ][±ZZ[[:]ZZ]]</code>, or |
| * <code>MM/DD/YYYY HH:MM[:SS] {AM/PM}</code>, where {a/b} indicates |
| * that you must choose one of a or b, and [c] indicates that you |
| * may use or omit c. ±ZZZZ is the timezone offset, and may be |
| * literally "Z" to mean GMT. |
| * @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()); |
| } |
| |
| } |
| } |