1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs;
  18. import java.security.DigestInputStream;
  19. import java.security.MessageDigest;
  20. import java.security.NoSuchAlgorithmException;
  21. import java.security.NoSuchProviderException;
  22. import java.io.File;
  23. import java.io.FileOutputStream;
  24. import java.io.FileInputStream;
  25. import java.io.FileReader;
  26. import java.io.BufferedReader;
  27. import java.io.IOException;
  28. import java.io.InputStreamReader;
  29. import java.util.HashMap;
  30. import java.util.Map;
  31. import java.util.Vector;
  32. import java.util.Hashtable;
  33. import java.util.Enumeration;
  34. import java.util.Set;
  35. import java.util.Arrays;
  36. import org.apache.tools.ant.BuildException;
  37. import org.apache.tools.ant.DirectoryScanner;
  38. import org.apache.tools.ant.Project;
  39. import org.apache.tools.ant.taskdefs.condition.Condition;
  40. import org.apache.tools.ant.types.FileSet;
  41. /**
  42. * Used to create or verify file checksums.
  43. *
  44. *
  45. * @since Ant 1.5
  46. *
  47. * @ant.task category="control"
  48. */
  49. public class Checksum extends MatchingTask implements Condition {
  50. /**
  51. * File for which checksum is to be calculated.
  52. */
  53. private File file = null;
  54. /**
  55. * Root directory in which the checksum files will be written.
  56. * If not specified, the checksum files will be written
  57. * in the same directory as each file.
  58. */
  59. private File todir;
  60. /**
  61. * MessageDigest algorithm to be used.
  62. */
  63. private String algorithm = "MD5";
  64. /**
  65. * MessageDigest Algorithm provider
  66. */
  67. private String provider = null;
  68. /**
  69. * File Extension that is be to used to create or identify
  70. * destination file
  71. */
  72. private String fileext;
  73. /**
  74. * Holds generated checksum and gets set as a Project Property.
  75. */
  76. private String property;
  77. /**
  78. * Holds checksums for all files (both calculated and cached on disk).
  79. * Key: java.util.File (source file)
  80. * Value: java.lang.String (digest)
  81. */
  82. private Map allDigests = new HashMap();
  83. /**
  84. * Holds relative file names for all files (always with a forward slash).
  85. * This is used to calculate the total hash.
  86. * Key: java.util.File (source file)
  87. * Value: java.lang.String (relative file name)
  88. */
  89. private Map relativeFilePaths = new HashMap();
  90. /**
  91. * Property where totalChecksum gets set.
  92. */
  93. private String totalproperty;
  94. /**
  95. * Whether or not to create a new file.
  96. * Defaults to <code>false</code>.
  97. */
  98. private boolean forceOverwrite;
  99. /**
  100. * Contains the result of a checksum verification. ("true" or "false")
  101. */
  102. private String verifyProperty;
  103. /**
  104. * Vector to hold source file sets.
  105. */
  106. private Vector filesets = new Vector();
  107. /**
  108. * Stores SourceFile, DestFile pairs and SourceFile, Property String pairs.
  109. */
  110. private Hashtable includeFileMap = new Hashtable();
  111. /**
  112. * Message Digest instance
  113. */
  114. private MessageDigest messageDigest;
  115. /**
  116. * is this task being used as a nested condition element?
  117. */
  118. private boolean isCondition;
  119. /**
  120. * Size of the read buffer to use.
  121. */
  122. private int readBufferSize = 8 * 1024;
  123. /**
  124. * Sets the file for which the checksum is to be calculated.
  125. */
  126. public void setFile(File file) {
  127. this.file = file;
  128. }
  129. /**
  130. * Sets the root directory where checksum files will be
  131. * written/read
  132. *
  133. * @since Ant 1.6
  134. */
  135. public void setTodir(File todir) {
  136. this.todir = todir;
  137. }
  138. /**
  139. * Specifies the algorithm to be used to compute the checksum.
  140. * Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
  141. */
  142. public void setAlgorithm(String algorithm) {
  143. this.algorithm = algorithm;
  144. }
  145. /**
  146. * Sets the MessageDigest algorithm provider to be used
  147. * to calculate the checksum.
  148. */
  149. public void setProvider(String provider) {
  150. this.provider = provider;
  151. }
  152. /**
  153. * Sets the file extension that is be to used to
  154. * create or identify destination file.
  155. */
  156. public void setFileext(String fileext) {
  157. this.fileext = fileext;
  158. }
  159. /**
  160. * Sets the property to hold the generated checksum.
  161. */
  162. public void setProperty(String property) {
  163. this.property = property;
  164. }
  165. /**
  166. * Sets the property to hold the generated total checksum
  167. * for all files.
  168. *
  169. * @since Ant 1.6
  170. */
  171. public void setTotalproperty(String totalproperty) {
  172. this.totalproperty = totalproperty;
  173. }
  174. /**
  175. * Sets the verify property. This project property holds
  176. * the result of a checksum verification - "true" or "false"
  177. */
  178. public void setVerifyproperty(String verifyProperty) {
  179. this.verifyProperty = verifyProperty;
  180. }
  181. /**
  182. * Whether or not to overwrite existing file irrespective of
  183. * whether it is newer than
  184. * the source file. Defaults to false.
  185. */
  186. public void setForceOverwrite(boolean forceOverwrite) {
  187. this.forceOverwrite = forceOverwrite;
  188. }
  189. /**
  190. * The size of the read buffer to use.
  191. */
  192. public void setReadBufferSize(int size) {
  193. this.readBufferSize = size;
  194. }
  195. /**
  196. * Files to generate checksums for.
  197. */
  198. public void addFileset(FileSet set) {
  199. filesets.addElement(set);
  200. }
  201. /**
  202. * Calculate the checksum(s).
  203. */
  204. public void execute() throws BuildException {
  205. isCondition = false;
  206. boolean value = validateAndExecute();
  207. if (verifyProperty != null) {
  208. getProject().setNewProperty(verifyProperty,
  209. new Boolean(value).toString());
  210. }
  211. }
  212. /**
  213. * Calculate the checksum(s)
  214. *
  215. * @return Returns true if the checksum verification test passed,
  216. * false otherwise.
  217. */
  218. public boolean eval() throws BuildException {
  219. isCondition = true;
  220. return validateAndExecute();
  221. }
  222. /**
  223. * Validate attributes and get down to business.
  224. */
  225. private boolean validateAndExecute() throws BuildException {
  226. String savedFileExt = fileext;
  227. if (file == null && filesets.size() == 0) {
  228. throw new BuildException(
  229. "Specify at least one source - a file or a fileset.");
  230. }
  231. if (file != null && file.exists() && file.isDirectory()) {
  232. throw new BuildException(
  233. "Checksum cannot be generated for directories");
  234. }
  235. if (file != null && totalproperty != null) {
  236. throw new BuildException(
  237. "File and Totalproperty cannot co-exist.");
  238. }
  239. if (property != null && fileext != null) {
  240. throw new BuildException(
  241. "Property and FileExt cannot co-exist.");
  242. }
  243. if (property != null) {
  244. if (forceOverwrite) {
  245. throw new BuildException(
  246. "ForceOverwrite cannot be used when Property is specified");
  247. }
  248. if (file != null) {
  249. if (filesets.size() > 0) {
  250. throw new BuildException("Multiple files cannot be used "
  251. + "when Property is specified");
  252. }
  253. } else {
  254. if (filesets.size() > 1) {
  255. throw new BuildException("Multiple files cannot be used "
  256. + "when Property is specified");
  257. }
  258. }
  259. }
  260. if (verifyProperty != null) {
  261. isCondition = true;
  262. }
  263. if (verifyProperty != null && forceOverwrite) {
  264. throw new BuildException(
  265. "VerifyProperty and ForceOverwrite cannot co-exist.");
  266. }
  267. if (isCondition && forceOverwrite) {
  268. throw new BuildException("ForceOverwrite cannot be used when "
  269. + "conditions are being used.");
  270. }
  271. messageDigest = null;
  272. if (provider != null) {
  273. try {
  274. messageDigest = MessageDigest.getInstance(algorithm, provider);
  275. } catch (NoSuchAlgorithmException noalgo) {
  276. throw new BuildException(noalgo, getLocation());
  277. } catch (NoSuchProviderException noprovider) {
  278. throw new BuildException(noprovider, getLocation());
  279. }
  280. } else {
  281. try {
  282. messageDigest = MessageDigest.getInstance(algorithm);
  283. } catch (NoSuchAlgorithmException noalgo) {
  284. throw new BuildException(noalgo, getLocation());
  285. }
  286. }
  287. if (messageDigest == null) {
  288. throw new BuildException("Unable to create Message Digest",
  289. getLocation());
  290. }
  291. if (fileext == null) {
  292. fileext = "." + algorithm;
  293. } else if (fileext.trim().length() == 0) {
  294. throw new BuildException(
  295. "File extension when specified must not be an empty string");
  296. }
  297. try {
  298. int sizeofFileSet = filesets.size();
  299. for (int i = 0; i < sizeofFileSet; i++) {
  300. FileSet fs = (FileSet) filesets.elementAt(i);
  301. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  302. String[] srcFiles = ds.getIncludedFiles();
  303. for (int j = 0; j < srcFiles.length; j++) {
  304. File src = new File(fs.getDir(getProject()), srcFiles[j]);
  305. if (totalproperty != null || todir != null) {
  306. // Use '/' to calculate digest based on file name.
  307. // This is required in order to get the same result
  308. // on different platforms.
  309. String relativePath = srcFiles[j].replace(File.separatorChar, '/');
  310. relativeFilePaths.put(src, relativePath);
  311. }
  312. addToIncludeFileMap(src);
  313. }
  314. }
  315. addToIncludeFileMap(file);
  316. return generateChecksums();
  317. } finally {
  318. fileext = savedFileExt;
  319. includeFileMap.clear();
  320. }
  321. }
  322. /**
  323. * Add key-value pair to the hashtable upon which
  324. * to later operate upon.
  325. */
  326. private void addToIncludeFileMap(File file) throws BuildException {
  327. if (file != null) {
  328. if (file.exists()) {
  329. if (property == null) {
  330. File checksumFile = getChecksumFile(file);
  331. if (forceOverwrite || isCondition
  332. || (file.lastModified() > checksumFile.lastModified())) {
  333. includeFileMap.put(file, checksumFile);
  334. } else {
  335. log(file + " omitted as " + checksumFile + " is up to date.",
  336. Project.MSG_VERBOSE);
  337. if (totalproperty != null) {
  338. // Read the checksum from disk.
  339. String checksum = null;
  340. try {
  341. BufferedReader diskChecksumReader
  342. = new BufferedReader(new FileReader(checksumFile));
  343. checksum = diskChecksumReader.readLine();
  344. } catch (IOException e) {
  345. throw new BuildException("Couldn't read checksum file "
  346. + checksumFile, e);
  347. }
  348. byte[] digest = decodeHex(checksum.toCharArray());
  349. allDigests.put(file, digest);
  350. }
  351. }
  352. } else {
  353. includeFileMap.put(file, property);
  354. }
  355. } else {
  356. String message = "Could not find file "
  357. + file.getAbsolutePath()
  358. + " to generate checksum for.";
  359. log(message);
  360. throw new BuildException(message, getLocation());
  361. }
  362. }
  363. }
  364. private File getChecksumFile(File file) {
  365. File directory;
  366. if (todir != null) {
  367. // A separate directory was explicitly declared
  368. String path = (String) relativeFilePaths.get(file);
  369. directory = new File(todir, path).getParentFile();
  370. // Create the directory, as it might not exist.
  371. directory.mkdirs();
  372. } else {
  373. // Just use the same directory as the file itself.
  374. // This directory will exist
  375. directory = file.getParentFile();
  376. }
  377. File checksumFile = new File(directory, file.getName() + fileext);
  378. return checksumFile;
  379. }
  380. /**
  381. * Generate checksum(s) using the message digest created earlier.
  382. */
  383. private boolean generateChecksums() throws BuildException {
  384. boolean checksumMatches = true;
  385. FileInputStream fis = null;
  386. FileOutputStream fos = null;
  387. byte[] buf = new byte[readBufferSize];
  388. try {
  389. for (Enumeration e = includeFileMap.keys(); e.hasMoreElements();) {
  390. messageDigest.reset();
  391. File src = (File) e.nextElement();
  392. if (!isCondition) {
  393. log("Calculating " + algorithm + " checksum for " + src, Project.MSG_VERBOSE);
  394. }
  395. fis = new FileInputStream(src);
  396. DigestInputStream dis = new DigestInputStream(fis,
  397. messageDigest);
  398. while (dis.read(buf, 0, readBufferSize) != -1) {
  399. ;
  400. }
  401. dis.close();
  402. fis.close();
  403. fis = null;
  404. byte[] fileDigest = messageDigest.digest ();
  405. if (totalproperty != null) {
  406. allDigests.put(src, fileDigest);
  407. }
  408. String checksum = createDigestString(fileDigest);
  409. //can either be a property name string or a file
  410. Object destination = includeFileMap.get(src);
  411. if (destination instanceof java.lang.String) {
  412. String prop = (String) destination;
  413. if (isCondition) {
  414. checksumMatches
  415. = checksumMatches && checksum.equals(property);
  416. } else {
  417. getProject().setNewProperty(prop, checksum);
  418. }
  419. } else if (destination instanceof java.io.File) {
  420. if (isCondition) {
  421. File existingFile = (File) destination;
  422. if (existingFile.exists()) {
  423. fis = new FileInputStream(existingFile);
  424. InputStreamReader isr = new InputStreamReader(fis);
  425. BufferedReader br = new BufferedReader(isr);
  426. String suppliedChecksum = br.readLine();
  427. fis.close();
  428. fis = null;
  429. br.close();
  430. isr.close();
  431. checksumMatches = checksumMatches
  432. && checksum.equals(suppliedChecksum);
  433. } else {
  434. checksumMatches = false;
  435. }
  436. } else {
  437. File dest = (File) destination;
  438. fos = new FileOutputStream(dest);
  439. fos.write(checksum.getBytes());
  440. fos.close();
  441. fos = null;
  442. }
  443. }
  444. }
  445. if (totalproperty != null) {
  446. // Calculate the total checksum
  447. // Convert the keys (source files) into a sorted array.
  448. Set keys = allDigests.keySet();
  449. Object[] keyArray = keys.toArray();
  450. // File is Comparable, so sorting is trivial
  451. Arrays.sort(keyArray);
  452. // Loop over the checksums and generate a total hash.
  453. messageDigest.reset();
  454. for (int i = 0; i < keyArray.length; i++) {
  455. File src = (File) keyArray[i];
  456. // Add the digest for the file content
  457. byte[] digest = (byte[]) allDigests.get(src);
  458. messageDigest.update(digest);
  459. // Add the file path
  460. String fileName = (String) relativeFilePaths.get(src);
  461. messageDigest.update(fileName.getBytes());
  462. }
  463. String totalChecksum = createDigestString(messageDigest.digest());
  464. getProject().setNewProperty(totalproperty, totalChecksum);
  465. }
  466. } catch (Exception e) {
  467. throw new BuildException(e, getLocation());
  468. } finally {
  469. if (fis != null) {
  470. try {
  471. fis.close();
  472. } catch (IOException e) {
  473. // ignore
  474. }
  475. }
  476. if (fos != null) {
  477. try {
  478. fos.close();
  479. } catch (IOException e) {
  480. // ignore
  481. }
  482. }
  483. }
  484. return checksumMatches;
  485. }
  486. private String createDigestString(byte[] fileDigest) {
  487. StringBuffer checksumSb = new StringBuffer();
  488. for (int i = 0; i < fileDigest.length; i++) {
  489. String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
  490. if (hexStr.length() < 2) {
  491. checksumSb.append("0");
  492. }
  493. checksumSb.append(hexStr);
  494. }
  495. return checksumSb.toString();
  496. }
  497. /**
  498. * Converts an array of characters representing hexadecimal values into an
  499. * array of bytes of those same values. The returned array will be half the
  500. * length of the passed array, as it takes two characters to represent any
  501. * given byte. An exception is thrown if the passed char array has an odd
  502. * number of elements.
  503. *
  504. * NOTE: This code is copied from jakarta-commons codec.
  505. */
  506. public static byte[] decodeHex(char[] data) throws BuildException {
  507. int l = data.length;
  508. if ((l & 0x01) != 0) {
  509. throw new BuildException("odd number of characters.");
  510. }
  511. byte[] out = new byte[l >> 1];
  512. // two characters form the hex value.
  513. for (int i = 0, j = 0; j < l; i++) {
  514. int f = Character.digit(data[j++], 16) << 4;
  515. f = f | Character.digit(data[j++], 16);
  516. out[i] = (byte) (f & 0xFF);
  517. }
  518. return out;
  519. }
  520. }