You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

Checksum.java 24 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. */
  18. package org.apache.tools.ant.taskdefs;
  19. import java.security.DigestInputStream;
  20. import java.security.MessageDigest;
  21. import java.security.NoSuchAlgorithmException;
  22. import java.security.NoSuchProviderException;
  23. import java.io.File;
  24. import java.io.FileOutputStream;
  25. import java.io.FileInputStream;
  26. import java.io.FileReader;
  27. import java.io.BufferedReader;
  28. import java.io.IOException;
  29. import java.util.HashMap;
  30. import java.util.Map;
  31. import java.util.Iterator;
  32. import java.util.Hashtable;
  33. import java.util.Enumeration;
  34. import java.util.Set;
  35. import java.util.Arrays;
  36. import java.text.MessageFormat;
  37. import java.text.ParseException;
  38. import org.apache.tools.ant.BuildException;
  39. import org.apache.tools.ant.Project;
  40. import org.apache.tools.ant.taskdefs.condition.Condition;
  41. import org.apache.tools.ant.types.EnumeratedAttribute;
  42. import org.apache.tools.ant.types.FileSet;
  43. import org.apache.tools.ant.types.ResourceCollection;
  44. import org.apache.tools.ant.types.resources.Union;
  45. import org.apache.tools.ant.types.resources.Restrict;
  46. import org.apache.tools.ant.types.resources.FileResource;
  47. import org.apache.tools.ant.types.resources.selectors.Type;
  48. import org.apache.tools.ant.util.FileUtils;
  49. import org.apache.tools.ant.util.StringUtils;
  50. /**
  51. * Used to create or verify file checksums.
  52. *
  53. * @since Ant 1.5
  54. *
  55. * @ant.task category="control"
  56. */
  57. public class Checksum extends MatchingTask implements Condition {
  58. private static class FileUnion extends Restrict {
  59. private Union u;
  60. FileUnion() {
  61. u = new Union();
  62. super.add(u);
  63. super.add(Type.FILE);
  64. }
  65. public void add(ResourceCollection rc) {
  66. u.add(rc);
  67. }
  68. }
  69. /**
  70. * File for which checksum is to be calculated.
  71. */
  72. private File file = null;
  73. /**
  74. * Root directory in which the checksum files will be written.
  75. * If not specified, the checksum files will be written
  76. * in the same directory as each file.
  77. */
  78. private File todir;
  79. /**
  80. * MessageDigest algorithm to be used.
  81. */
  82. private String algorithm = "MD5";
  83. /**
  84. * MessageDigest Algorithm provider
  85. */
  86. private String provider = null;
  87. /**
  88. * File Extension that is be to used to create or identify
  89. * destination file
  90. */
  91. private String fileext;
  92. /**
  93. * Holds generated checksum and gets set as a Project Property.
  94. */
  95. private String property;
  96. /**
  97. * Holds checksums for all files (both calculated and cached on disk).
  98. * Key: java.util.File (source file)
  99. * Value: java.lang.String (digest)
  100. */
  101. private Map allDigests = new HashMap();
  102. /**
  103. * Holds relative file names for all files (always with a forward slash).
  104. * This is used to calculate the total hash.
  105. * Key: java.util.File (source file)
  106. * Value: java.lang.String (relative file name)
  107. */
  108. private Map relativeFilePaths = new HashMap();
  109. /**
  110. * Property where totalChecksum gets set.
  111. */
  112. private String totalproperty;
  113. /**
  114. * Whether or not to create a new file.
  115. * Defaults to <code>false</code>.
  116. */
  117. private boolean forceOverwrite;
  118. /**
  119. * Contains the result of a checksum verification. ("true" or "false")
  120. */
  121. private String verifyProperty;
  122. /**
  123. * Resource Collection.
  124. */
  125. private FileUnion resources = null;
  126. /**
  127. * Stores SourceFile, DestFile pairs and SourceFile, Property String pairs.
  128. */
  129. private Hashtable includeFileMap = new Hashtable();
  130. /**
  131. * Message Digest instance
  132. */
  133. private MessageDigest messageDigest;
  134. /**
  135. * is this task being used as a nested condition element?
  136. */
  137. private boolean isCondition;
  138. /**
  139. * Size of the read buffer to use.
  140. */
  141. private int readBufferSize = 8 * 1024;
  142. /**
  143. * Formater for the checksum file.
  144. */
  145. private MessageFormat format = FormatElement.getDefault().getFormat();
  146. /**
  147. * Sets the file for which the checksum is to be calculated.
  148. * @param file a <code>File</code> value
  149. */
  150. public void setFile(File file) {
  151. this.file = file;
  152. }
  153. /**
  154. * Sets the root directory where checksum files will be
  155. * written/read
  156. * @param todir the directory to write to
  157. * @since Ant 1.6
  158. */
  159. public void setTodir(File todir) {
  160. this.todir = todir;
  161. }
  162. /**
  163. * Specifies the algorithm to be used to compute the checksum.
  164. * Defaults to "MD5". Other popular algorithms like "SHA" may be used as well.
  165. * @param algorithm a <code>String</code> value
  166. */
  167. public void setAlgorithm(String algorithm) {
  168. this.algorithm = algorithm;
  169. }
  170. /**
  171. * Sets the MessageDigest algorithm provider to be used
  172. * to calculate the checksum.
  173. * @param provider a <code>String</code> value
  174. */
  175. public void setProvider(String provider) {
  176. this.provider = provider;
  177. }
  178. /**
  179. * Sets the file extension that is be to used to
  180. * create or identify destination file.
  181. * @param fileext a <code>String</code> value
  182. */
  183. public void setFileext(String fileext) {
  184. this.fileext = fileext;
  185. }
  186. /**
  187. * Sets the property to hold the generated checksum.
  188. * @param property a <code>String</code> value
  189. */
  190. public void setProperty(String property) {
  191. this.property = property;
  192. }
  193. /**
  194. * Sets the property to hold the generated total checksum
  195. * for all files.
  196. * @param totalproperty a <code>String</code> value
  197. *
  198. * @since Ant 1.6
  199. */
  200. public void setTotalproperty(String totalproperty) {
  201. this.totalproperty = totalproperty;
  202. }
  203. /**
  204. * Sets the verify property. This project property holds
  205. * the result of a checksum verification - "true" or "false"
  206. * @param verifyProperty a <code>String</code> value
  207. */
  208. public void setVerifyproperty(String verifyProperty) {
  209. this.verifyProperty = verifyProperty;
  210. }
  211. /**
  212. * Whether or not to overwrite existing file irrespective of
  213. * whether it is newer than
  214. * the source file. Defaults to false.
  215. * @param forceOverwrite a <code>boolean</code> value
  216. */
  217. public void setForceOverwrite(boolean forceOverwrite) {
  218. this.forceOverwrite = forceOverwrite;
  219. }
  220. /**
  221. * The size of the read buffer to use.
  222. * @param size an <code>int</code> value
  223. */
  224. public void setReadBufferSize(int size) {
  225. this.readBufferSize = size;
  226. }
  227. /**
  228. * Select the in/output pattern via a well know format name.
  229. * @param e an <code>enumerated</code> value
  230. *
  231. * @since 1.7.0
  232. */
  233. public void setFormat(FormatElement e) {
  234. format = e.getFormat();
  235. }
  236. /**
  237. * Specify the pattern to use as a MessageFormat pattern.
  238. *
  239. * <p>{0} gets replaced by the checksum, {1} by the filename.</p>
  240. * @param p a <code>String</code> value
  241. *
  242. * @since 1.7.0
  243. */
  244. public void setPattern(String p) {
  245. format = new MessageFormat(p);
  246. }
  247. /**
  248. * Files to generate checksums for.
  249. * @param set a fileset of files to generate checksums for.
  250. */
  251. public void addFileset(FileSet set) {
  252. add(set);
  253. }
  254. /**
  255. * Add a resource collection.
  256. * @param rc the ResourceCollection to add.
  257. */
  258. public void add(ResourceCollection rc) {
  259. if (rc == null) {
  260. return;
  261. }
  262. resources = (resources == null) ? new FileUnion() : resources;
  263. resources.add(rc);
  264. }
  265. /**
  266. * Calculate the checksum(s).
  267. * @throws BuildException on error
  268. */
  269. public void execute() throws BuildException {
  270. isCondition = false;
  271. boolean value = validateAndExecute();
  272. if (verifyProperty != null) {
  273. getProject().setNewProperty(
  274. verifyProperty,
  275. (value ? Boolean.TRUE.toString() : Boolean.FALSE.toString()));
  276. }
  277. }
  278. /**
  279. * Calculate the checksum(s)
  280. *
  281. * @return Returns true if the checksum verification test passed,
  282. * false otherwise.
  283. * @throws BuildException on error
  284. */
  285. public boolean eval() throws BuildException {
  286. isCondition = true;
  287. return validateAndExecute();
  288. }
  289. /**
  290. * Validate attributes and get down to business.
  291. */
  292. private boolean validateAndExecute() throws BuildException {
  293. String savedFileExt = fileext;
  294. if (file == null && (resources == null || resources.size() == 0)) {
  295. throw new BuildException(
  296. "Specify at least one source - a file or a resource collection.");
  297. }
  298. if (!(resources == null || resources.isFilesystemOnly())) {
  299. throw new BuildException("Can only calculate checksums for file-based resources.");
  300. }
  301. if (file != null && file.exists() && file.isDirectory()) {
  302. throw new BuildException("Checksum cannot be generated for directories");
  303. }
  304. if (file != null && totalproperty != null) {
  305. throw new BuildException("File and Totalproperty cannot co-exist.");
  306. }
  307. if (property != null && fileext != null) {
  308. throw new BuildException("Property and FileExt cannot co-exist.");
  309. }
  310. if (property != null) {
  311. if (forceOverwrite) {
  312. throw new BuildException(
  313. "ForceOverwrite cannot be used when Property is specified");
  314. }
  315. int ct = 0;
  316. if (resources != null) {
  317. ct += resources.size();
  318. }
  319. if (file != null) {
  320. ct++;
  321. }
  322. if (ct > 1) {
  323. throw new BuildException(
  324. "Multiple files cannot be used when Property is specified");
  325. }
  326. }
  327. if (verifyProperty != null) {
  328. isCondition = true;
  329. }
  330. if (verifyProperty != null && forceOverwrite) {
  331. throw new BuildException("VerifyProperty and ForceOverwrite cannot co-exist.");
  332. }
  333. if (isCondition && forceOverwrite) {
  334. throw new BuildException(
  335. "ForceOverwrite cannot be used when conditions are being used.");
  336. }
  337. messageDigest = null;
  338. if (provider != null) {
  339. try {
  340. messageDigest = MessageDigest.getInstance(algorithm, provider);
  341. } catch (NoSuchAlgorithmException noalgo) {
  342. throw new BuildException(noalgo, getLocation());
  343. } catch (NoSuchProviderException noprovider) {
  344. throw new BuildException(noprovider, getLocation());
  345. }
  346. } else {
  347. try {
  348. messageDigest = MessageDigest.getInstance(algorithm);
  349. } catch (NoSuchAlgorithmException noalgo) {
  350. throw new BuildException(noalgo, getLocation());
  351. }
  352. }
  353. if (messageDigest == null) {
  354. throw new BuildException("Unable to create Message Digest", getLocation());
  355. }
  356. if (fileext == null) {
  357. fileext = "." + algorithm;
  358. } else if (fileext.trim().length() == 0) {
  359. throw new BuildException("File extension when specified must not be an empty string");
  360. }
  361. try {
  362. if (resources != null) {
  363. for (Iterator i = resources.iterator(); i.hasNext();) {
  364. FileResource fr = (FileResource) i.next();
  365. File src = fr.getFile();
  366. if (totalproperty != null || todir != null) {
  367. // Use '/' to calculate digest based on file name.
  368. // This is required in order to get the same result
  369. // on different platforms.
  370. relativeFilePaths.put(src, fr.getName().replace(File.separatorChar, '/'));
  371. }
  372. addToIncludeFileMap(src);
  373. }
  374. }
  375. if (file != null) {
  376. if (totalproperty != null || todir != null) {
  377. relativeFilePaths.put(
  378. file, file.getName().replace(File.separatorChar, '/'));
  379. }
  380. addToIncludeFileMap(file);
  381. }
  382. return generateChecksums();
  383. } finally {
  384. fileext = savedFileExt;
  385. includeFileMap.clear();
  386. }
  387. }
  388. /**
  389. * Add key-value pair to the hashtable upon which
  390. * to later operate upon.
  391. */
  392. private void addToIncludeFileMap(File file) throws BuildException {
  393. if (file.exists()) {
  394. if (property == null) {
  395. File checksumFile = getChecksumFile(file);
  396. if (forceOverwrite || isCondition
  397. || (file.lastModified() > checksumFile.lastModified())) {
  398. includeFileMap.put(file, checksumFile);
  399. } else {
  400. log(file + " omitted as " + checksumFile + " is up to date.",
  401. Project.MSG_VERBOSE);
  402. if (totalproperty != null) {
  403. // Read the checksum from disk.
  404. String checksum = readChecksum(checksumFile);
  405. byte[] digest = decodeHex(checksum.toCharArray());
  406. allDigests.put(file, digest);
  407. }
  408. }
  409. } else {
  410. includeFileMap.put(file, property);
  411. }
  412. } else {
  413. String message = "Could not find file "
  414. + file.getAbsolutePath()
  415. + " to generate checksum for.";
  416. log(message);
  417. throw new BuildException(message, getLocation());
  418. }
  419. }
  420. private File getChecksumFile(File file) {
  421. File directory;
  422. if (todir != null) {
  423. // A separate directory was explicitly declared
  424. String path = (String) relativeFilePaths.get(file);
  425. if (path == null) {
  426. //bug 37386. this should not occur, but it has, once.
  427. throw new BuildException(
  428. "Internal error: "
  429. + "relativeFilePaths could not match file"
  430. + file + "\n"
  431. + "please file a bug report on this");
  432. }
  433. directory = new File(todir, path).getParentFile();
  434. // Create the directory, as it might not exist.
  435. directory.mkdirs();
  436. } else {
  437. // Just use the same directory as the file itself.
  438. // This directory will exist
  439. directory = file.getParentFile();
  440. }
  441. File checksumFile = new File(directory, file.getName() + fileext);
  442. return checksumFile;
  443. }
  444. /**
  445. * Generate checksum(s) using the message digest created earlier.
  446. */
  447. private boolean generateChecksums() throws BuildException {
  448. boolean checksumMatches = true;
  449. FileInputStream fis = null;
  450. FileOutputStream fos = null;
  451. byte[] buf = new byte[readBufferSize];
  452. try {
  453. for (Enumeration e = includeFileMap.keys(); e.hasMoreElements();) {
  454. messageDigest.reset();
  455. File src = (File) e.nextElement();
  456. if (!isCondition) {
  457. log("Calculating " + algorithm + " checksum for " + src, Project.MSG_VERBOSE);
  458. }
  459. fis = new FileInputStream(src);
  460. DigestInputStream dis = new DigestInputStream(fis,
  461. messageDigest);
  462. while (dis.read(buf, 0, readBufferSize) != -1) {
  463. // Empty statement
  464. }
  465. dis.close();
  466. fis.close();
  467. fis = null;
  468. byte[] fileDigest = messageDigest.digest ();
  469. if (totalproperty != null) {
  470. allDigests.put(src, fileDigest);
  471. }
  472. String checksum = createDigestString(fileDigest);
  473. //can either be a property name string or a file
  474. Object destination = includeFileMap.get(src);
  475. if (destination instanceof java.lang.String) {
  476. String prop = (String) destination;
  477. if (isCondition) {
  478. checksumMatches
  479. = checksumMatches && checksum.equals(property);
  480. } else {
  481. getProject().setNewProperty(prop, checksum);
  482. }
  483. } else if (destination instanceof java.io.File) {
  484. if (isCondition) {
  485. File existingFile = (File) destination;
  486. if (existingFile.exists()) {
  487. try {
  488. String suppliedChecksum =
  489. readChecksum(existingFile);
  490. checksumMatches = checksumMatches
  491. && checksum.equals(suppliedChecksum);
  492. } catch (BuildException be) {
  493. // file is on wrong format, swallow
  494. checksumMatches = false;
  495. }
  496. } else {
  497. checksumMatches = false;
  498. }
  499. } else {
  500. File dest = (File) destination;
  501. fos = new FileOutputStream(dest);
  502. fos.write(format.format(new Object[] {
  503. checksum,
  504. src.getName(),
  505. }).getBytes());
  506. fos.write(StringUtils.LINE_SEP.getBytes());
  507. fos.close();
  508. fos = null;
  509. }
  510. }
  511. }
  512. if (totalproperty != null) {
  513. // Calculate the total checksum
  514. // Convert the keys (source files) into a sorted array.
  515. Set keys = allDigests.keySet();
  516. Object[] keyArray = keys.toArray();
  517. // File is Comparable, so sorting is trivial
  518. Arrays.sort(keyArray);
  519. // Loop over the checksums and generate a total hash.
  520. messageDigest.reset();
  521. for (int i = 0; i < keyArray.length; i++) {
  522. File src = (File) keyArray[i];
  523. // Add the digest for the file content
  524. byte[] digest = (byte[]) allDigests.get(src);
  525. messageDigest.update(digest);
  526. // Add the file path
  527. String fileName = (String) relativeFilePaths.get(src);
  528. messageDigest.update(fileName.getBytes());
  529. }
  530. String totalChecksum = createDigestString(messageDigest.digest());
  531. getProject().setNewProperty(totalproperty, totalChecksum);
  532. }
  533. } catch (Exception e) {
  534. throw new BuildException(e, getLocation());
  535. } finally {
  536. FileUtils.close(fis);
  537. FileUtils.close(fos);
  538. }
  539. return checksumMatches;
  540. }
  541. private String createDigestString(byte[] fileDigest) {
  542. StringBuffer checksumSb = new StringBuffer();
  543. for (int i = 0; i < fileDigest.length; i++) {
  544. String hexStr = Integer.toHexString(0x00ff & fileDigest[i]);
  545. if (hexStr.length() < 2) {
  546. checksumSb.append("0");
  547. }
  548. checksumSb.append(hexStr);
  549. }
  550. return checksumSb.toString();
  551. }
  552. /**
  553. * Converts an array of characters representing hexadecimal values into an
  554. * array of bytes of those same values. The returned array will be half the
  555. * length of the passed array, as it takes two characters to represent any
  556. * given byte. An exception is thrown if the passed char array has an odd
  557. * number of elements.
  558. *
  559. * NOTE: This code is copied from jakarta-commons codec.
  560. * @param data an array of characters representing hexadecimal values
  561. * @return the converted array of bytes
  562. * @throws BuildException on error
  563. */
  564. public static byte[] decodeHex(char[] data) throws BuildException {
  565. int l = data.length;
  566. if ((l & 0x01) != 0) {
  567. throw new BuildException("odd number of characters.");
  568. }
  569. byte[] out = new byte[l >> 1];
  570. // two characters form the hex value.
  571. for (int i = 0, j = 0; j < l; i++) {
  572. int f = Character.digit(data[j++], 16) << 4;
  573. f = f | Character.digit(data[j++], 16);
  574. out[i] = (byte) (f & 0xFF);
  575. }
  576. return out;
  577. }
  578. /**
  579. * reads the checksum from a file using the specified format.
  580. *
  581. * @since 1.7
  582. */
  583. private String readChecksum(File f) {
  584. BufferedReader diskChecksumReader = null;
  585. try {
  586. diskChecksumReader = new BufferedReader(new FileReader(f));
  587. Object[] result = format.parse(diskChecksumReader.readLine());
  588. if (result == null || result.length == 0 || result[0] == null) {
  589. throw new BuildException("failed to find a checksum");
  590. }
  591. return (String) result[0];
  592. } catch (IOException e) {
  593. throw new BuildException("Couldn't read checksum file " + f, e);
  594. } catch (ParseException e) {
  595. throw new BuildException("Couldn't read checksum file " + f, e);
  596. } finally {
  597. FileUtils.close(diskChecksumReader);
  598. }
  599. }
  600. /**
  601. * Helper class for the format attribute.
  602. *
  603. * @since 1.7
  604. */
  605. public static class FormatElement extends EnumeratedAttribute {
  606. private static HashMap formatMap = new HashMap();
  607. private static final String CHECKSUM = "CHECKSUM";
  608. private static final String MD5SUM = "MD5SUM";
  609. private static final String SVF = "SVF";
  610. static {
  611. formatMap.put(CHECKSUM, new MessageFormat("{0}"));
  612. formatMap.put(MD5SUM, new MessageFormat("{0} *{1}"));
  613. formatMap.put(SVF, new MessageFormat("MD5 ({1}) = {0}"));
  614. }
  615. /** Constructor for FormatElement */
  616. public FormatElement() {
  617. super();
  618. }
  619. /**
  620. * Get the default value - CHECKSUM.
  621. * @return the defaul value.
  622. */
  623. public static FormatElement getDefault() {
  624. FormatElement e = new FormatElement();
  625. e.setValue(CHECKSUM);
  626. return e;
  627. }
  628. /**
  629. * Convert this enumerated type to a <code>MessageFormat</code>.
  630. * @return a <code>MessageFormat</code> object.
  631. */
  632. public MessageFormat getFormat() {
  633. return (MessageFormat) formatMap.get(getValue());
  634. }
  635. /**
  636. * Get the valid values.
  637. * @return an array of values.
  638. */
  639. public String[] getValues() {
  640. return new String[] {CHECKSUM, MD5SUM, SVF};
  641. }
  642. }
  643. }