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 25 kB

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