| /* |
| * 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.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.file.Files; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.Vector; |
| |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.Task; |
| import org.apache.tools.ant.types.FileSet; |
| import org.apache.tools.ant.types.Mapper; |
| import org.apache.tools.ant.types.PatternSet; |
| import org.apache.tools.ant.types.Resource; |
| import org.apache.tools.ant.types.ResourceCollection; |
| import org.apache.tools.ant.types.resources.FileProvider; |
| import org.apache.tools.ant.types.resources.Union; |
| import org.apache.tools.ant.types.selectors.SelectorUtils; |
| import org.apache.tools.ant.util.FileNameMapper; |
| import org.apache.tools.ant.util.FileUtils; |
| import org.apache.tools.ant.util.IdentityMapper; |
| import org.apache.tools.zip.ZipEntry; |
| import org.apache.tools.zip.ZipFile; |
| |
| /** |
| * Unzip a file. |
| * |
| * @since Ant 1.1 |
| * |
| * @ant.task category="packaging" |
| * name="unzip" |
| * name="unjar" |
| * name="unwar" |
| */ |
| public class Expand extends Task { |
| public static final String NATIVE_ENCODING = "native-encoding"; |
| |
| /** Error message when more that one mapper is defined */ |
| public static final String ERROR_MULTIPLE_MAPPERS = "Cannot define more than one mapper"; |
| |
| private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); |
| |
| private static final int BUFFER_SIZE = 1024; |
| private File dest; //req |
| private File source; // req |
| private boolean overwrite = true; |
| private Mapper mapperElement = null; |
| private List<PatternSet> patternsets = new Vector<>(); |
| private Union resources = new Union(); |
| private boolean resourcesSpecified = false; |
| private boolean failOnEmptyArchive = false; |
| private boolean stripAbsolutePathSpec = true; |
| private boolean scanForUnicodeExtraFields = true; |
| private Boolean allowFilesToEscapeDest = null; |
| |
| private String encoding; |
| |
| /** |
| * Creates an Expand instance and sets encoding to UTF-8. |
| */ |
| public Expand() { |
| this("UTF8"); |
| } |
| |
| /** |
| * Creates an Expand instance and sets the given encoding. |
| * |
| * @param encoding String |
| * @since Ant 1.9.5 |
| */ |
| protected Expand(String encoding) { |
| this.encoding = encoding; |
| } |
| |
| /** |
| * Whether try ing to expand an empty archive would be an error. |
| * |
| * @param b boolean |
| * @since Ant 1.8.0 |
| */ |
| public void setFailOnEmptyArchive(boolean b) { |
| failOnEmptyArchive = b; |
| } |
| |
| /** |
| * Whether try ing to expand an empty archive would be an error. |
| * |
| * @return boolean |
| * @since Ant 1.8.0 |
| */ |
| public boolean getFailOnEmptyArchive() { |
| return failOnEmptyArchive; |
| } |
| |
| /** |
| * Do the work. |
| * |
| * @exception BuildException Thrown in unrecoverable error. |
| */ |
| @Override |
| public void execute() throws BuildException { |
| if ("expand".equals(getTaskType())) { |
| log("!! expand is deprecated. Use unzip instead. !!"); |
| } |
| |
| if (source == null && !resourcesSpecified) { |
| throw new BuildException( |
| "src attribute and/or resources must be specified"); |
| } |
| |
| if (dest == null) { |
| throw new BuildException( |
| "Dest attribute must be specified"); |
| } |
| |
| if (dest.exists() && !dest.isDirectory()) { |
| throw new BuildException("Dest must be a directory.", getLocation()); |
| } |
| |
| if (source != null) { |
| if (source.isDirectory()) { |
| throw new BuildException("Src must not be a directory." |
| + " Use nested filesets instead.", getLocation()); |
| } |
| if (!source.exists()) { |
| throw new BuildException("src '" + source + "' doesn't exist."); |
| } |
| if (!source.canRead()) { |
| throw new BuildException("src '" + source + "' cannot be read."); |
| } |
| expandFile(FILE_UTILS, source, dest); |
| } |
| for (Resource r : resources) { |
| if (!r.isExists()) { |
| log("Skipping '" + r.getName() + "' because it doesn't exist."); |
| continue; |
| } |
| |
| FileProvider fp = r.as(FileProvider.class); |
| if (fp != null) { |
| expandFile(FILE_UTILS, fp.getFile(), dest); |
| } else { |
| expandResource(r, dest); |
| } |
| } |
| } |
| |
| /** |
| * This method is to be overridden by extending unarchival tasks. |
| * |
| * @param fileUtils the fileUtils |
| * @param srcF the source file |
| * @param dir the destination directory |
| */ |
| protected void expandFile(FileUtils fileUtils, File srcF, File dir) { |
| log("Expanding: " + srcF + " into " + dir, Project.MSG_INFO); |
| FileNameMapper mapper = getMapper(); |
| if (!srcF.exists()) { |
| throw new BuildException("Unable to expand " |
| + srcF |
| + " as the file does not exist", |
| getLocation()); |
| } |
| try (ZipFile zf = new ZipFile(srcF, encoding, scanForUnicodeExtraFields)) { |
| boolean empty = true; |
| Enumeration<ZipEntry> entries = zf.getEntries(); |
| while (entries.hasMoreElements()) { |
| ZipEntry ze = entries.nextElement(); |
| empty = false; |
| InputStream is = null; |
| log("extracting " + ze.getName(), Project.MSG_DEBUG); |
| try { |
| extractFile(fileUtils, srcF, dir, |
| is = zf.getInputStream(ze), //NOSONAR |
| ze.getName(), new Date(ze.getTime()), |
| ze.isDirectory(), mapper); |
| } finally { |
| FileUtils.close(is); |
| } |
| } |
| if (empty && getFailOnEmptyArchive()) { |
| throw new BuildException("archive '%s' is empty", srcF); |
| } |
| log("expand complete", Project.MSG_VERBOSE); |
| } catch (IOException ioe) { |
| throw new BuildException( |
| "Error while expanding " + srcF.getPath() |
| + "\n" + ioe.toString(), |
| ioe); |
| } |
| } |
| |
| /** |
| * This method is to be overridden by extending unarchival tasks. |
| * |
| * @param srcR the source resource |
| * @param dir the destination directory |
| */ |
| protected void expandResource(Resource srcR, File dir) { |
| throw new BuildException( |
| "only filesystem based resources are supported by this task."); |
| } |
| |
| /** |
| * get a mapper for a file |
| * @return a filenamemapper for a file |
| */ |
| protected FileNameMapper getMapper() { |
| if (mapperElement != null) { |
| return mapperElement.getImplementation(); |
| } |
| return new IdentityMapper(); |
| } |
| |
| // CheckStyle:ParameterNumberCheck OFF - bc |
| /** |
| * extract a file to a directory |
| * @param fileUtils a fileUtils object |
| * @param srcF the source file |
| * @param dir the destination directory |
| * @param compressedInputStream the input stream |
| * @param entryName the name of the entry |
| * @param entryDate the date of the entry |
| * @param isDirectory if this is true the entry is a directory |
| * @param mapper the filename mapper to use |
| * @throws IOException on error |
| */ |
| protected void extractFile(FileUtils fileUtils, File srcF, File dir, |
| InputStream compressedInputStream, |
| String entryName, Date entryDate, |
| boolean isDirectory, FileNameMapper mapper) |
| throws IOException { |
| |
| final boolean entryNameStartsWithPathSpec = !entryName.isEmpty() |
| && (entryName.charAt(0) == File.separatorChar |
| || entryName.charAt(0) == '/' |
| || entryName.charAt(0) == '\\'); |
| if (stripAbsolutePathSpec && entryNameStartsWithPathSpec) { |
| log("stripped absolute path spec from " + entryName, |
| Project.MSG_VERBOSE); |
| entryName = entryName.substring(1); |
| } |
| boolean allowedOutsideOfDest = Boolean.TRUE == getAllowFilesToEscapeDest() |
| || null == getAllowFilesToEscapeDest() && !stripAbsolutePathSpec && entryNameStartsWithPathSpec; |
| |
| if (patternsets != null && !patternsets.isEmpty()) { |
| String name = entryName.replace('/', File.separatorChar) |
| .replace('\\', File.separatorChar); |
| |
| Set<String> includePatterns = new HashSet<>(); |
| Set<String> excludePatterns = new HashSet<>(); |
| for (PatternSet p : patternsets) { |
| String[] incls = p.getIncludePatterns(getProject()); |
| if (incls == null || incls.length == 0) { |
| // no include pattern implicitly means includes="**" |
| incls = new String[]{"**"}; |
| } |
| |
| for (String incl : incls) { |
| String pattern = incl.replace('/', File.separatorChar) |
| .replace('\\', File.separatorChar); |
| if (pattern.endsWith(File.separator)) { |
| pattern += "**"; |
| } |
| includePatterns.add(pattern); |
| } |
| |
| String[] excls = p.getExcludePatterns(getProject()); |
| if (excls != null) { |
| for (String excl : excls) { |
| String pattern = excl.replace('/', File.separatorChar) |
| .replace('\\', File.separatorChar); |
| if (pattern.endsWith(File.separator)) { |
| pattern += "**"; |
| } |
| excludePatterns.add(pattern); |
| } |
| } |
| } |
| |
| boolean included = false; |
| for (String pattern : includePatterns) { |
| if (SelectorUtils.matchPath(pattern, name)) { |
| included = true; |
| break; |
| } |
| } |
| |
| for (String pattern : excludePatterns) { |
| if (SelectorUtils.matchPath(pattern, name)) { |
| included = false; |
| break; |
| } |
| } |
| |
| if (!included) { |
| // Do not process this file |
| log("skipping " + entryName |
| + " as it is excluded or not included.", |
| Project.MSG_VERBOSE); |
| return; |
| } |
| } |
| String[] mappedNames = mapper.mapFileName(entryName); |
| if (mappedNames == null || mappedNames.length == 0) { |
| mappedNames = new String[] {entryName}; |
| } |
| File f = fileUtils.resolveFile(dir, mappedNames[0]); |
| if (!allowedOutsideOfDest && !fileUtils.isLeadingPath(dir, f, true)) { |
| log("skipping " + entryName + " as its target " + f.getCanonicalPath() |
| + " is outside of " + dir.getCanonicalPath() + ".", Project.MSG_VERBOSE); |
| return; |
| } |
| |
| try { |
| if (!overwrite && f.exists() |
| && f.lastModified() >= entryDate.getTime()) { |
| log("Skipping " + f + " as it is up-to-date", |
| Project.MSG_DEBUG); |
| return; |
| } |
| |
| log("expanding " + entryName + " to " + f, |
| Project.MSG_VERBOSE); |
| // create intermediary directories - sometimes zip don't add them |
| File dirF = f.getParentFile(); |
| if (dirF != null) { |
| dirF.mkdirs(); |
| } |
| |
| if (isDirectory) { |
| f.mkdirs(); |
| } else { |
| byte[] buffer = new byte[BUFFER_SIZE]; |
| try (OutputStream fos = Files.newOutputStream(f.toPath())) { |
| int length; |
| while ((length = compressedInputStream.read(buffer)) >= 0) { |
| fos.write(buffer, 0, length); |
| } |
| } |
| } |
| |
| fileUtils.setFileLastModified(f, entryDate.getTime()); |
| } catch (FileNotFoundException ex) { |
| log("Unable to expand to file " + f.getPath(), |
| ex, |
| Project.MSG_WARN); |
| } |
| |
| } |
| // CheckStyle:ParameterNumberCheck ON |
| |
| /** |
| * Set the destination directory. File will be unzipped into the |
| * destination directory. |
| * |
| * @param d Path to the directory. |
| */ |
| public void setDest(File d) { |
| this.dest = d; |
| } |
| |
| /** |
| * Set the path to zip-file. |
| * |
| * @param s Path to zip-file. |
| */ |
| public void setSrc(File s) { |
| this.source = s; |
| } |
| |
| /** |
| * Should we overwrite files in dest, even if they are newer than |
| * the corresponding entries in the archive? |
| * @param b a <code>boolean</code> value |
| */ |
| public void setOverwrite(boolean b) { |
| overwrite = b; |
| } |
| |
| /** |
| * Add a patternset. |
| * @param set a pattern set |
| */ |
| public void addPatternset(PatternSet set) { |
| patternsets.add(set); |
| } |
| |
| /** |
| * Add a fileset |
| * @param set a file set |
| */ |
| public void addFileset(FileSet set) { |
| add(set); |
| } |
| |
| /** |
| * Add a resource collection. |
| * @param rc a resource collection. |
| * @since Ant 1.7 |
| */ |
| public void add(ResourceCollection rc) { |
| resourcesSpecified = true; |
| resources.add(rc); |
| } |
| |
| /** |
| * Defines the mapper to map source entries to destination files. |
| * @return a mapper to be configured |
| * @exception BuildException if more than one mapper is defined |
| * @since Ant1.7 |
| */ |
| public Mapper createMapper() throws BuildException { |
| if (mapperElement != null) { |
| throw new BuildException(ERROR_MULTIPLE_MAPPERS, |
| getLocation()); |
| } |
| mapperElement = new Mapper(getProject()); |
| return mapperElement; |
| } |
| |
| /** |
| * A nested filenamemapper |
| * @param fileNameMapper the mapper to add |
| * @since Ant 1.6.3 |
| */ |
| public void add(FileNameMapper fileNameMapper) { |
| createMapper().add(fileNameMapper); |
| } |
| |
| |
| /** |
| * Sets the encoding to assume for file names and comments. |
| * |
| * <p>Set to <code>native-encoding</code> if you want your |
| * platform's native encoding, defaults to UTF8.</p> |
| * @param encoding the name of the character encoding |
| * @since Ant 1.6 |
| */ |
| public void setEncoding(String encoding) { |
| internalSetEncoding(encoding); |
| } |
| |
| /** |
| * Supports grand-children that want to support the attribute |
| * where the child-class doesn't (i.e. Unzip in the compress |
| * Antlib). |
| * |
| * @param encoding String |
| * @since Ant 1.8.0 |
| */ |
| protected void internalSetEncoding(String encoding) { |
| if (NATIVE_ENCODING.equals(encoding)) { |
| encoding = null; |
| } |
| this.encoding = encoding; |
| } |
| |
| /** |
| * @return String |
| * @since Ant 1.8.0 |
| */ |
| public String getEncoding() { |
| return encoding; |
| } |
| |
| /** |
| * Whether leading path separators should be stripped. |
| * |
| * @param b boolean |
| * @since Ant 1.8.0 |
| */ |
| public void setStripAbsolutePathSpec(boolean b) { |
| stripAbsolutePathSpec = b; |
| } |
| |
| /** |
| * Whether unicode extra fields will be used if present. |
| * |
| * @param b boolean |
| * @since Ant 1.8.0 |
| */ |
| public void setScanForUnicodeExtraFields(boolean b) { |
| internalSetScanForUnicodeExtraFields(b); |
| } |
| |
| /** |
| * Supports grand-children that want to support the attribute |
| * where the child-class doesn't (i.e. Unzip in the compress |
| * Antlib). |
| * |
| * @param b boolean |
| * @since Ant 1.8.0 |
| */ |
| protected void internalSetScanForUnicodeExtraFields(boolean b) { |
| scanForUnicodeExtraFields = b; |
| } |
| |
| /** |
| * @return boolean |
| * @since Ant 1.8.0 |
| */ |
| public boolean getScanForUnicodeExtraFields() { |
| return scanForUnicodeExtraFields; |
| } |
| |
| /** |
| * Whether to allow the extracted file or directory to be outside of the dest directory. |
| * |
| * @param b the flag |
| * @since Ant 1.10.4 |
| */ |
| public void setAllowFilesToEscapeDest(boolean b) { |
| allowFilesToEscapeDest = b; |
| } |
| |
| /** |
| * Whether to allow the extracted file or directory to be outside of the dest directory. |
| * |
| * @return {@code null} if the flag hasn't been set explicitly, |
| * otherwise the value set by the user. |
| * @since Ant 1.10.4 |
| */ |
| public Boolean getAllowFilesToEscapeDest() { |
| return allowFilesToEscapeDest; |
| } |
| |
| } |