blob: 19f27fb4b9664a43a0cd215eb2f6f4648cbeea88 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.tools.ant.taskdefs;
import java.io.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;
}
}