JAL-3130 adapted getdown src. attempt 2. first attempt failed due to cp'ed .git files
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / Digest.java
1 //
2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
5
6 package com.threerings.getdown.data;
7
8 import java.io.*;
9 import java.nio.charset.StandardCharsets;
10 import java.security.MessageDigest;
11 import java.security.NoSuchAlgorithmException;
12 import java.util.*;
13 import java.util.concurrent.*;
14
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;
19
20 import static com.threerings.getdown.Log.log;
21 import static java.nio.charset.StandardCharsets.UTF_8;
22
23 /**
24  * Manages the <code>digest.txt</code> file and the computing and processing of digests for an
25  * application.
26  */
27 public class Digest
28 {
29     /** The current version of the digest protocol. */
30     public static final int VERSION = 2;
31
32     /**
33      * Returns the name of the digest file for the specified protocol version.
34      */
35     public static String digestFile (int version) {
36         String infix = version > 1 ? String.valueOf(version) : "";
37         return FILE_NAME + infix + FILE_SUFFIX;
38     }
39
40     /**
41      * Returns the crypto algorithm used to sign digest files of the specified version.
42      */
43     public static String sigAlgorithm (int version) {
44         switch (version) {
45         case 1: return "SHA1withRSA";
46         case 2: return "SHA256withRSA";
47         default: throw new IllegalArgumentException("Invalid digest version " + version);
48         }
49     }
50
51     /**
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.
54      */
55     public static void createDigest (int version, List<Resource> resources, File output)
56         throws IOException
57     {
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;
63
64         long start = System.currentTimeMillis();
65
66         Set<Resource> pending = new HashSet<>(resources);
67         for (final Resource rsrc : resources) {
68             exec.execute(new Runnable() {
69                 public void run () {
70                     try {
71                         MessageDigest md = getMessageDigest(fversion);
72                         digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
73                         completed.add(rsrc);
74                     } catch (Throwable t) {
75                         completed.add(new IOException("Error computing digest for: " + rsrc).
76                                       initCause(t));
77                     }
78                 }
79             });
80         }
81
82         // queue a shutdown of the thread pool when the tasks are done
83         exec.shutdown();
84
85         try {
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);
92                 } else {
93                     throw new AssertionError("What is this? " + done);
94                 }
95             }
96         } catch (InterruptedException ie) {
97             throw new IOException("Timeout computing digests. Wow.");
98         }
99
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);
110             }
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)));
116         }
117
118         long elapsed = System.currentTimeMillis() - start;
119         log.debug("Computed digests [rsrcs=" + resources.size() + ", time=" + elapsed + "ms]");
120     }
121
122     /**
123      * Obtains an appropriate message digest instance for use by the Getdown system.
124      */
125     public static MessageDigest getMessageDigest (int version)
126     {
127         String algo = version > 1 ? "SHA-256" : "MD5";
128         try {
129             return MessageDigest.getInstance(algo);
130         } catch (NoSuchAlgorithmException nsae) {
131             throw new RuntimeException("JVM does not support " + algo + ". Gurp!");
132         }
133     }
134
135     /**
136      * Creates a digest instance which will parse and validate the digest in the supplied
137      * application directory, using the current digest version.
138      */
139     public Digest (File appdir, boolean strictComments) throws IOException {
140         this(appdir, VERSION, strictComments);
141     }
142
143     /**
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.
147      */
148     public Digest (File appdir, int version, boolean strictComments) throws IOException
149     {
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];
162                 break;
163             }
164             _digests.put(pair[0], pair[1]);
165             note(data, pair[0], pair[1]);
166         }
167
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);
175         }
176     }
177
178     /**
179      * Returns the digest for the digest file.
180      */
181     public String getMetaDigest ()
182     {
183         return _metaDigest;
184     }
185
186     /**
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.
189      *
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.
192      */
193     public boolean validateResource (Resource resource, ProgressObserver obs)
194     {
195         try {
196             String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
197             String ehash = _digests.get(resource.getPath());
198             if (chash.equals(ehash)) {
199                 return true;
200             }
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);
205         }
206         return false;
207     }
208
209     /**
210      * Returns the digest of the given {@code resource}.
211      */
212     public String getDigest (Resource resource)
213     {
214         return _digests.get(resource.getPath());
215     }
216
217     /** Used by {@link #createDigest} and {@link Digest}. */
218     protected static void note (StringBuilder data, String path, String digest)
219     {
220         data.append(path).append(" = ").append(digest).append("\n");
221     }
222
223     protected HashMap<String, String> _digests = new HashMap<>();
224     protected String _metaDigest = "";
225
226     protected static final String FILE_NAME = "digest";
227     protected static final String FILE_SUFFIX = ".txt";
228 }