2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.data;
9 import java.nio.charset.StandardCharsets;
10 import java.security.MessageDigest;
11 import java.security.NoSuchAlgorithmException;
13 import java.util.concurrent.*;
15 import com.threerings.getdown.util.Config;
16 import com.threerings.getdown.util.MessageUtil;
17 import com.threerings.getdown.util.ProgressObserver;
18 import com.threerings.getdown.util.StringUtil;
20 import static com.threerings.getdown.Log.log;
21 import static java.nio.charset.StandardCharsets.UTF_8;
24 * Manages the <code>digest.txt</code> file and the computing and processing of digests for an
29 /** The current version of the digest protocol. */
30 public static final int VERSION = 2;
33 * Returns the name of the digest file for the specified protocol version.
35 public static String digestFile (int version) {
36 String infix = version > 1 ? String.valueOf(version) : "";
37 return FILE_NAME + infix + FILE_SUFFIX;
41 * Returns the crypto algorithm used to sign digest files of the specified version.
43 public static String sigAlgorithm (int version) {
45 case 1: return "SHA1withRSA";
46 case 2: return "SHA256withRSA";
47 default: throw new IllegalArgumentException("Invalid digest version " + version);
52 * Creates a digest file at the specified location using the supplied list of resources.
53 * @param version the version of the digest protocol to use.
55 public static void createDigest (int version, List<Resource> resources, File output)
58 // first compute the digests for all the resources in parallel
59 ExecutorService exec = Executors.newFixedThreadPool(SysProps.threadPoolSize());
60 final Map<Resource, String> digests = new ConcurrentHashMap<>();
61 final BlockingQueue<Object> completed = new LinkedBlockingQueue<>();
62 final int fversion = version;
64 long start = System.currentTimeMillis();
66 Set<Resource> pending = new HashSet<>(resources);
67 for (final Resource rsrc : resources) {
68 exec.execute(new Runnable() {
71 MessageDigest md = getMessageDigest(fversion);
72 digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
74 } catch (Throwable t) {
75 completed.add(new IOException("Error computing digest for: " + rsrc).
82 // queue a shutdown of the thread pool when the tasks are done
86 while (pending.size() > 0) {
87 Object done = completed.poll(600, TimeUnit.SECONDS);
88 if (done instanceof IOException) {
89 throw (IOException)done;
90 } else if (done instanceof Resource) {
91 pending.remove((Resource)done);
93 throw new AssertionError("What is this? " + done);
96 } catch (InterruptedException ie) {
97 throw new IOException("Timeout computing digests. Wow.");
100 StringBuilder data = new StringBuilder();
101 try (FileOutputStream fos = new FileOutputStream(output);
102 OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
103 PrintWriter pout = new PrintWriter(osw)) {
104 // compute and append the digest of each resource in the list
105 for (Resource rsrc : resources) {
106 String path = rsrc.getPath();
107 String digest = digests.get(rsrc);
108 note(data, path, digest);
109 pout.println(path + " = " + digest);
111 // finally compute and append the digest for the file contents
112 MessageDigest md = getMessageDigest(version);
113 byte[] contents = data.toString().getBytes(UTF_8);
114 String filename = digestFile(version);
115 pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
118 long elapsed = System.currentTimeMillis() - start;
119 log.debug("Computed digests [rsrcs=" + resources.size() + ", time=" + elapsed + "ms]");
123 * Obtains an appropriate message digest instance for use by the Getdown system.
125 public static MessageDigest getMessageDigest (int version)
127 String algo = version > 1 ? "SHA-256" : "MD5";
129 return MessageDigest.getInstance(algo);
130 } catch (NoSuchAlgorithmException nsae) {
131 throw new RuntimeException("JVM does not support " + algo + ". Gurp!");
136 * Creates a digest instance which will parse and validate the digest in the supplied
137 * application directory, using the current digest version.
139 public Digest (File appdir, boolean strictComments) throws IOException {
140 this(appdir, VERSION, strictComments);
144 * Creates a digest instance which will parse and validate the digest in the supplied
145 * application directory.
146 * @param version the version of the digest protocol to use.
148 public Digest (File appdir, int version, boolean strictComments) throws IOException
150 // parse and validate our digest file contents
151 String filename = digestFile(version);
152 StringBuilder data = new StringBuilder();
153 File dfile = new File(appdir, filename);
154 Config.ParseOpts opts = Config.createOpts(false);
155 opts.strictComments = strictComments;
156 // bias = toward key: the key is the filename and could conceivably contain = signs, value
157 // is the hex encoded hash which will not contain =
158 opts.biasToKey = true;
159 for (String[] pair : Config.parsePairs(dfile, opts)) {
160 if (pair[0].equals(filename)) {
161 _metaDigest = pair[1];
164 _digests.put(pair[0], pair[1]);
165 note(data, pair[0], pair[1]);
168 // we've reached the end, validate our contents
169 MessageDigest md = getMessageDigest(version);
170 byte[] contents = data.toString().getBytes(UTF_8);
171 String hash = StringUtil.hexlate(md.digest(contents));
172 if (!hash.equals(_metaDigest)) {
173 String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash);
174 throw new IOException(err);
179 * Returns the digest for the digest file.
181 public String getMetaDigest ()
187 * Computes the hash of the specified resource and compares it with the value parsed from
188 * the digest file. Logs a message if the resource fails validation.
190 * @return true if the resource is valid, false if it failed the digest check or if an I/O
191 * error was encountered during the validation process.
193 public boolean validateResource (Resource resource, ProgressObserver obs)
196 String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
197 String ehash = _digests.get(resource.getPath());
198 if (chash.equals(ehash)) {
201 log.info("Resource failed digest check",
202 "rsrc", resource, "computed", chash, "expected", ehash);
203 } catch (Throwable t) {
204 log.info("Resource failed digest check", "rsrc", resource, "error", t);
210 * Returns the digest of the given {@code resource}.
212 public String getDigest (Resource resource)
214 return _digests.get(resource.getPath());
217 /** Used by {@link #createDigest} and {@link Digest}. */
218 protected static void note (StringBuilder data, String path, String digest)
220 data.append(path).append(" = ").append(digest).append("\n");
223 protected HashMap<String, String> _digests = new HashMap<>();
224 protected String _metaDigest = "";
226 protected static final String FILE_NAME = "digest";
227 protected static final String FILE_SUFFIX = ".txt";