blob: 2d5c8820a0bfe3440d8b680ada3424ca8df8f24f [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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.tools.ant.taskdefs;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Set;
import java.util.Arrays;
import java.text.MessageFormat;
import java.text.ParseException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.condition.Condition;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.Union;
import org.apache.tools.ant.types.resources.Restrict;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.selectors.Type;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.StringUtils;
/**
* Used to create or verify file checksums.
*
* @since Ant 1.5
*
* @ant.task category="control"
*/
public class Checksum extends MatchingTask implements Condition {
private static final int NIBBLE = 4;
private static final int WORD = 16;
private static final int BUFFER_SIZE = 8 * 1024;
private static final int BYTE_MASK = 0xFF;
private static class FileUnion extends Restrict {
private Union u;
FileUnion() {
u = new Union();
super.add(u);
super.add(Type.FILE);
}
public void add(ResourceCollection rc) {
u.add(rc);
}
}
/**
* File for which checksum is to be calculated.
*/
private File file = null;
/**
* Root directory in which the checksum files will be written.
* If not specified, the checksum files will be written
* in the same directory as each file.
*/
private File todir;
/**
* MessageDigest algorithm to be used.
*/
private String algorithm = "MD5";
/**
* MessageDigest Algorithm provider
*/
private String provider = null;
/**
* File Extension that is be to used to create or identify
* destination file
*/
private String fileext;
/**
* Holds generated checksum and gets set as a Project Property.
*/
private String property;
/**
* Holds checksums for all files (both calculated and cached on disk).
* Key: java.util.File (source file)
* Value: java.lang.String (digest)
*/
private Map allDigests = new HashMap();
/**
* Holds relative file names for all files (always with a forward slash).
* This is used to calculate the total hash.
* Key: java.util.File (source file)
* Value: java.lang.String (relative file name)
*/
private Map relativeFilePaths = new HashMap();
/**
* Property where totalChecksum gets set.
*/
private String totalproperty;
/**
* Whether or not to create a new file.
* Defaults to <code>false</code>.
*/
private boolean forceOverwrite;
/**
* Contains the result of a checksum verification. ("true" or "false")
*/
private String verifyProperty;
/**
* Resource Collection.
*/
private FileUnion resources = null;
/**
* Stores SourceFile, DestFile pairs and SourceFile, Property String pairs.
*/
private Hashtable includeFileMap = new Hashtable();
/**
* Message Digest instance
*/
private MessageDigest messageDigest;
/**
* is this task being used as a nested condition element?
*/
private boolean isCondition;
/**
* Size of the read buffer to use.
*/
private int readBufferSize = BUFFER_SIZE;
/**
* Formater for the checksum file.
*/
private MessageFormat format = FormatElement.getDefault().getFormat();
/**
* Sets the file for which the checksum is to be calculated.
* @param file a <code>File</code> value
*/
public void setFile(File file) {
this.file = file;
}
/**
* Sets the root directory where checksum files will be
* written/read
* @param todir the directory to write to
* @since Ant 1.6
*/
public void setTodir(File todir) {
this.todir = todir;
}
/**
* Specifies the algorithm to be used to compute the checksum.
* Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
* @param algorithm a <code>String</code> value
*/
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
/**
* Sets the MessageDigest algorithm provider to be used
* to calculate the checksum.
* @param provider a <code>String</code> value
*/
public void setProvider(String provider) {
this.provider = provider;
}
/**
* Sets the file extension that is be to used to
* create or identify destination file.
* @param fileext a <code>String</code> value
*/
public void setFileext(String fileext) {
this.fileext = fileext;
}
/**
* Sets the property to hold the generated checksum.
* @param property a <code>String</code> value
*/
public void setProperty(String property) {
this.property = property;
}
/**
* Sets the property to hold the generated total checksum
* for all files.
* @param totalproperty a <code>String</code> value
*
* @since Ant 1.6
*/
public void setTotalproperty(String totalproperty) {
this.totalproperty = totalproperty;
}
/**
* Sets the verify property. This project property holds
* the result of a checksum verification - "true" or "false"
* @param verifyProperty a <code>String</code> value
*/
public void setVerifyproperty(String verifyProperty) {
this.verifyProperty = verifyProperty;
}
/**
* Whether or not to overwrite existing file irrespective of
* whether it is newer than
* the source file. Defaults to false.
* @param forceOverwrite a <code>boolean</code> value
*/
public void setForceOverwrite(boolean forceOverwrite) {
this.forceOverwrite = forceOverwrite;
}
/**
* The size of the read buffer to use.
* @param size an <code>int</code> value
*/
public void setReadBufferSize(int size) {
this.readBufferSize = size;
}
/**
* Select the in/output pattern via a well know format name.
* @param e an <code>enumerated</code> value
*
* @since 1.7.0
*/
public void setFormat(FormatElement e) {
format = e.getFormat();
}
/**
* Specify the pattern to use as a MessageFormat pattern.
*
* <p>{0} gets replaced by the checksum, {1} by the filename.</p>
* @param p a <code>String</code> value
*
* @since 1.7.0
*/
public void setPattern(String p) {
format = new MessageFormat(p);
}
/**
* Files to generate checksums for.
* @param set a fileset of files to generate checksums for.
*/
public void addFileset(FileSet set) {
add(set);
}
/**
* Add a resource collection.
* @param rc the ResourceCollection to add.
*/
public void add(ResourceCollection rc) {
if (rc == null) {
return;
}
resources = (resources == null) ? new FileUnion() : resources;
resources.add(rc);
}
/**
* Calculate the checksum(s).
* @throws BuildException on error
*/
public void execute() throws BuildException {
isCondition = false;
boolean value = validateAndExecute();
if (verifyProperty != null) {
getProject().setNewProperty(
verifyProperty,
(value ? Boolean.TRUE.toString() : Boolean.FALSE.toString()));
}
}
/**
* Calculate the checksum(s)
*
* @return Returns true if the checksum verification test passed,
* false otherwise.
* @throws BuildException on error
*/
public boolean eval() throws BuildException {
isCondition = true;
return validateAndExecute();
}
/**
* Validate attributes and get down to business.
*/
private boolean validateAndExecute() throws BuildException {
String savedFileExt = fileext;
if (file == null && (resources == null || resources.size() == 0)) {
throw new BuildException(
"Specify at least one source - a file or a resource collection.");
}
if (!(resources == null || resources.isFilesystemOnly())) {
throw new BuildException("Can only calculate checksums for file-based resources.");
}
if (file != null && file.exists() && file.isDirectory()) {
throw new BuildException("Checksum cannot be generated for directories");
}
if (file != null && totalproperty != null) {
throw new BuildException("File and Totalproperty cannot co-exist.");
}
if (property != null && fileext != null) {
throw new BuildException("Property and FileExt cannot co-exist.");
}
if (property != null) {
if (forceOverwrite) {
throw new BuildException(
"ForceOverwrite cannot be used when Property is specified");
}
int ct = 0;
if (resources != null) {
ct += resources.size();
}
if (file != null) {
ct++;
}
if (ct > 1) {
throw new BuildException(
"Multiple files cannot be used when Property is specified");
}
}
if (verifyProperty != null) {
isCondition = true;
}
if (verifyProperty != null && forceOverwrite) {
throw new BuildException("VerifyProperty and ForceOverwrite cannot co-exist.");
}
if (isCondition && forceOverwrite) {
throw new BuildException(
"ForceOverwrite cannot be used when conditions are being used.");
}
messageDigest = null;
if (provider != null) {
try {
messageDigest = MessageDigest.getInstance(algorithm, provider);
} catch (NoSuchAlgorithmException noalgo) {
throw new BuildException(noalgo, getLocation());
} catch (NoSuchProviderException noprovider) {
throw new BuildException(noprovider, getLocation());
}
} else {
try {
messageDigest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException noalgo) {
throw new BuildException(noalgo, getLocation());
}
}
if (messageDigest == null) {
throw new BuildException("Unable to create Message Digest", getLocation());
}
if (fileext == null) {
fileext = "." + algorithm;
} else if (fileext.trim().length() == 0) {
throw new BuildException("File extension when specified must not be an empty string");
}
try {
if (resources != null) {
for (Iterator i = resources.iterator(); i.hasNext();) {
FileResource fr = (FileResource) i.next();
File src = fr.getFile();
if (totalproperty != null || todir != null) {
// Use '/' to calculate digest based on file name.
// This is required in order to get the same result
// on different platforms.
relativeFilePaths.put(src, fr.getName().replace(File.separatorChar, '/'));
}
addToIncludeFileMap(src);
}
}
if (file != null) {
if (totalproperty != null || todir != null) {
relativeFilePaths.put(
file, file.getName().replace(File.separatorChar, '/'));
}
addToIncludeFileMap(file);
}
return generateChecksums();
} finally {
fileext = savedFileExt;
includeFileMap.clear();
}
}
/**
* Add key-value pair to the hashtable upon which
* to later operate upon.
*/
private void addToIncludeFileMap(File file) throws BuildException {
if (file.exists()) {
if (property == null) {
File checksumFile = getChecksumFile(file);
if (forceOverwrite || isCondition
|| (file.lastModified() > checksumFile.lastModified())) {
includeFileMap.put(file, checksumFile);
} else {
log(file + " omitted as " + checksumFile + " is up to date.",
Project.MSG_VERBOSE);
if (totalproperty != null) {
// Read the checksum from disk.
String checksum = readChecksum(checksumFile);
byte[] digest = decodeHex(checksum.toCharArray());
allDigests.put(file, digest);
}
}
} else {
includeFileMap.put(file, property);
}
} else {
String message = "Could not find file "
+ file.getAbsolutePath()
+ " to generate checksum for.";
log(message);
throw new BuildException(message, getLocation());
}
}
private File getChecksumFile(File file) {
File directory;
if (todir != null) {
// A separate directory was explicitly declared
String path = (String) relativeFilePaths.get(file);
if (path == null) {
//bug 37386. this should not occur, but it has, once.
throw new BuildException(
"Internal error: "
+ "relativeFilePaths could not match file"
+ file + "\n"
+ "please file a bug report on this");
}
directory = new File(todir, path).getParentFile();
// Create the directory, as it might not exist.
directory.mkdirs();
} else {
// Just use the same directory as the file itself.
// This directory will exist
directory = file.getParentFile();
}
File checksumFile = new File(directory, file.getName() + fileext);
return checksumFile;
}
/**
* Generate checksum(s) using the message digest created earlier.
*/
private boolean generateChecksums() throws BuildException {
boolean checksumMatches = true;
FileInputStream fis = null;
FileOutputStream fos = null;
byte[] buf = new byte[readBufferSize];
try {
for (Enumeration e = includeFileMap.keys(); e.hasMoreElements();) {
messageDigest.reset();
File src = (File) e.nextElement();
if (!isCondition) {
log("Calculating " + algorithm + " checksum for " + src, Project.MSG_VERBOSE);
}
fis = new FileInputStream(src);
DigestInputStream dis = new DigestInputStream(fis,
messageDigest);
while (dis.read(buf, 0, readBufferSize) != -1) {
// Empty statement
}
dis.close();
fis.close();
fis = null;
byte[] fileDigest = messageDigest.digest ();
if (totalproperty != null) {
allDigests.put(src, fileDigest);
}
String checksum = createDigestString(fileDigest);
//can either be a property name string or a file
Object destination = includeFileMap.get(src);
if (destination instanceof java.lang.String) {
String prop = (String) destination;
if (isCondition) {
checksumMatches
= checksumMatches && checksum.equals(property);
} else {
getProject().setNewProperty(prop, checksum);
}
} else if (destination instanceof java.io.File) {
if (isCondition) {
File existingFile = (File) destination;
if (existingFile.exists()) {
try {
String suppliedChecksum =
readChecksum(existingFile);
checksumMatches = checksumMatches
&& checksum.equals(suppliedChecksum);
} catch (BuildException be) {
// file is on wrong format, swallow
checksumMatches = false;
}
} else {
checksumMatches = false;
}
} else {
File dest = (File) destination;
fos = new FileOutputStream(dest);
fos.write(format.format(new Object[] {
checksum,
src.getName(),
}).getBytes());
fos.write(StringUtils.LINE_SEP.getBytes());
fos.close();
fos = null;
}
}
}
if (totalproperty != null) {
// Calculate the total checksum
// Convert the keys (source files) into a sorted array.
Set keys = allDigests.keySet();
Object[] keyArray = keys.toArray();
// File is Comparable, so sorting is trivial
Arrays.sort(keyArray);
// Loop over the checksums and generate a total hash.
messageDigest.reset();
for (int i = 0; i < keyArray.length; i++) {
File src = (File) keyArray[i];
// Add the digest for the file content
byte[] digest = (byte[]) allDigests.get(src);
messageDigest.update(digest);
// Add the file path
String fileName = (String) relativeFilePaths.get(src);
messageDigest.update(fileName.getBytes());
}
String totalChecksum = createDigestString(messageDigest.digest());
getProject().setNewProperty(totalproperty, totalChecksum);
}
} catch (Exception e) {
throw new BuildException(e, getLocation());
} finally {
FileUtils.close(fis);
FileUtils.close(fos);
}
return checksumMatches;
}
private String createDigestString(byte[] fileDigest) {
StringBuffer checksumSb = new StringBuffer();
for (int i = 0; i < fileDigest.length; i++) {
String hexStr = Integer.toHexString(BYTE_MASK & fileDigest[i]);
if (hexStr.length() < 2) {
checksumSb.append("0");
}
checksumSb.append(hexStr);
}
return checksumSb.toString();
}
/**
* Converts an array of characters representing hexadecimal values into an
* array of bytes of those same values. The returned array will be half the
* length of the passed array, as it takes two characters to represent any
* given byte. An exception is thrown if the passed char array has an odd
* number of elements.
*
* NOTE: This code is copied from jakarta-commons codec.
* @param data an array of characters representing hexadecimal values
* @return the converted array of bytes
* @throws BuildException on error
*/
public static byte[] decodeHex(char[] data) throws BuildException {
int l = data.length;
if ((l & 0x01) != 0) {
throw new BuildException("odd number of characters.");
}
byte[] out = new byte[l >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < l; i++) {
int f = Character.digit(data[j++], WORD) << NIBBLE;
f = f | Character.digit(data[j++], WORD);
out[i] = (byte) (f & BYTE_MASK);
}
return out;
}
/**
* reads the checksum from a file using the specified format.
*
* @since 1.7
*/
private String readChecksum(File f) {
BufferedReader diskChecksumReader = null;
try {
diskChecksumReader = new BufferedReader(new FileReader(f));
Object[] result = format.parse(diskChecksumReader.readLine());
if (result == null || result.length == 0 || result[0] == null) {
throw new BuildException("failed to find a checksum");
}
return (String) result[0];
} catch (IOException e) {
throw new BuildException("Couldn't read checksum file " + f, e);
} catch (ParseException e) {
throw new BuildException("Couldn't read checksum file " + f, e);
} finally {
FileUtils.close(diskChecksumReader);
}
}
/**
* Helper class for the format attribute.
*
* @since 1.7
*/
public static class FormatElement extends EnumeratedAttribute {
private static HashMap formatMap = new HashMap();
private static final String CHECKSUM = "CHECKSUM";
private static final String MD5SUM = "MD5SUM";
private static final String SVF = "SVF";
static {
formatMap.put(CHECKSUM, new MessageFormat("{0}"));
formatMap.put(MD5SUM, new MessageFormat("{0} *{1}"));
formatMap.put(SVF, new MessageFormat("MD5 ({1}) = {0}"));
}
/** Constructor for FormatElement */
public FormatElement() {
super();
}
/**
* Get the default value - CHECKSUM.
* @return the defaul value.
*/
public static FormatElement getDefault() {
FormatElement e = new FormatElement();
e.setValue(CHECKSUM);
return e;
}
/**
* Convert this enumerated type to a <code>MessageFormat</code>.
* @return a <code>MessageFormat</code> object.
*/
public MessageFormat getFormat() {
return (MessageFormat) formatMap.get(getValue());
}
/**
* Get the valid values.
* @return an array of values.
*/
public String[] getValues() {
return new String[] {CHECKSUM, MD5SUM, SVF};
}
}
}