diff options
Diffstat (limited to 'framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Checksum.java')
-rw-r--r-- | framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Checksum.java | 712 |
1 files changed, 712 insertions, 0 deletions
diff --git a/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Checksum.java b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Checksum.java new file mode 100644 index 00000000..7a94ca09 --- /dev/null +++ b/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/Checksum.java @@ -0,0 +1,712 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.apache.tools.ant.taskdefs; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.text.MessageFormat; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +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.Resource; +import org.apache.tools.ant.types.ResourceCollection; +import org.apache.tools.ant.types.resources.FileProvider; +import org.apache.tools.ant.types.resources.Restrict; +import org.apache.tools.ant.types.resources.Union; +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<File, byte[]> allDigests = new HashMap<File, byte[]>(); + /** + * 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<File, String> relativeFilePaths = new HashMap<File, String>(); + /** + * 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<File, Object> includeFileMap = new Hashtable<File, Object>(); + /** + * 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 (Resource r : resources) { + File src = r.as(FileProvider.class) + .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, r.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 = getRelativeFilePath(file); + 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 (Map.Entry<File, Object> e : includeFileMap.entrySet()) { + messageDigest.reset(); + File src = e.getKey(); + 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 = e.getValue(); + 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(), + FileUtils + .getRelativePath(dest + .getParentFile(), + src), + FileUtils + .getRelativePath(getProject() + .getBaseDir(), + src), + src.getAbsolutePath() + }).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. + File[] keyArray = allDigests.keySet().toArray(new File[allDigests.size()]); + // File is Comparable, but sort-order is platform + // dependent (case-insensitive on Windows) + Arrays.sort(keyArray, new Comparator<File>() { + public int compare(File f1, File f2) { + return f1 == null ? (f2 == null ? 0 : -1) + : (f2 == null ? 1 + : getRelativeFilePath(f1) + .compareTo(getRelativeFilePath(f2))); + } + }); + // Loop over the checksums and generate a total hash. + messageDigest.reset(); + for (File src : keyArray) { + // Add the digest for the file content + byte[] digest = allDigests.get(src); + messageDigest.update(digest); + + // Add the file path + String fileName = getRelativeFilePath(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); + } + } + + /** + * @since Ant 1.8.2 + */ + private String getRelativeFilePath(File f) { + String path = (String) relativeFilePaths.get(f); + if (path == null) { + //bug 37386. this should not occur, but it has, once. + throw new BuildException("Internal error: " + + "relativeFilePaths could not match file " + + f + "\n" + + "please file a bug report on this"); + } + return path; + } + + /** + * Helper class for the format attribute. + * + * @since 1.7 + */ + public static class FormatElement extends EnumeratedAttribute { + private static HashMap<String, MessageFormat> formatMap = new HashMap<String, MessageFormat>(); + 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}; + } + } +} |