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

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