All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.getdown.data.Digest Maven / Gradle / Ivy

There is a newer version: 1.8.7
Show newest version
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE

package com.threerings.getdown.data;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.*;

import com.threerings.getdown.util.Config;
import com.threerings.getdown.util.MessageUtil;
import com.threerings.getdown.util.ProgressObserver;
import com.threerings.getdown.util.StringUtil;

import static com.threerings.getdown.Log.log;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Manages the digest.txt file and the computing and processing of digests for an
 * application.
 */
public class Digest
{
    /** The current version of the digest protocol. */
    public static final int VERSION = 2;

    /**
     * Returns the name of the digest file for the specified protocol version.
     */
    public static String digestFile (int version) {
        String infix = version > 1 ? String.valueOf(version) : "";
        return FILE_NAME + infix + FILE_SUFFIX;
    }

    /**
     * Returns the crypto algorithm used to sign digest files of the specified version.
     */
    public static String sigAlgorithm (int version) {
        switch (version) {
        case 1: return "SHA1withRSA";
        case 2: return "SHA256withRSA";
        default: throw new IllegalArgumentException("Invalid digest version " + version);
        }
    }

    /**
     * Creates a digest file at the specified location using the supplied list of resources.
     * @param version the version of the digest protocol to use.
     */
    public static void createDigest (int version, List resources, File output)
        throws IOException
    {
        // first compute the digests for all the resources in parallel
        ExecutorService exec = Executors.newFixedThreadPool(SysProps.threadPoolSize());
        final Map digests = new ConcurrentHashMap<>();
        final BlockingQueue completed = new LinkedBlockingQueue<>();
        final int fversion = version;

        long start = System.currentTimeMillis();

        Set pending = new HashSet<>(resources);
        for (final Resource rsrc : resources) {
            exec.execute(new Runnable() {
                public void run () {
                    try {
                        MessageDigest md = getMessageDigest(fversion);
                        digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
                        completed.add(rsrc);
                    } catch (Throwable t) {
                        completed.add(new IOException("Error computing digest for: " + rsrc).
                                      initCause(t));
                    }
                }
            });
        }

        // queue a shutdown of the thread pool when the tasks are done
        exec.shutdown();

        try {
            while (pending.size() > 0) {
                Object done = completed.poll(600, TimeUnit.SECONDS);
                if (done instanceof IOException) {
                    throw (IOException)done;
                } else if (done instanceof Resource) {
                    pending.remove((Resource)done);
                } else {
                    throw new AssertionError("What is this? " + done);
                }
            }
        } catch (InterruptedException ie) {
            throw new IOException("Timeout computing digests. Wow.");
        }

        StringBuilder data = new StringBuilder();
        try (FileOutputStream fos = new FileOutputStream(output);
             OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
             PrintWriter pout = new PrintWriter(osw)) {
            // compute and append the digest of each resource in the list
            for (Resource rsrc : resources) {
                String path = rsrc.getPath();
                String digest = digests.get(rsrc);
                note(data, path, digest);
                pout.println(path + " = " + digest);
            }
            // finally compute and append the digest for the file contents
            MessageDigest md = getMessageDigest(version);
            byte[] contents = data.toString().getBytes(UTF_8);
            String filename = digestFile(version);
            pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
        }

        long elapsed = System.currentTimeMillis() - start;
        log.debug("Computed digests [rsrcs=" + resources.size() + ", time=" + elapsed + "ms]");
    }

    /**
     * Obtains an appropriate message digest instance for use by the Getdown system.
     */
    public static MessageDigest getMessageDigest (int version)
    {
        String algo = version > 1 ? "SHA-256" : "MD5";
        try {
            return MessageDigest.getInstance(algo);
        } catch (NoSuchAlgorithmException nsae) {
            throw new RuntimeException("JVM does not support " + algo + ". Gurp!");
        }
    }

    /**
     * Creates a digest instance which will parse and validate the digest in the supplied
     * application directory, using the current digest version.
     */
    public Digest (File appdir, boolean strictComments) throws IOException {
        this(appdir, VERSION, strictComments);
    }

    /**
     * Creates a digest instance which will parse and validate the digest in the supplied
     * application directory.
     * @param version the version of the digest protocol to use.
     */
    public Digest (File appdir, int version, boolean strictComments) throws IOException
    {
        // parse and validate our digest file contents
        String filename = digestFile(version);
        StringBuilder data = new StringBuilder();
        File dfile = new File(appdir, filename);
        Config.ParseOpts opts = Config.createOpts(false);
        opts.strictComments = strictComments;
        // bias = toward key: the key is the filename and could conceivably contain = signs, value
        // is the hex encoded hash which will not contain =
        opts.biasToKey = true;
        for (String[] pair : Config.parsePairs(dfile, opts)) {
            if (pair[0].equals(filename)) {
                _metaDigest = pair[1];
                break;
            }
            _digests.put(pair[0], pair[1]);
            note(data, pair[0], pair[1]);
        }

        // we've reached the end, validate our contents
        MessageDigest md = getMessageDigest(version);
        byte[] contents = data.toString().getBytes(UTF_8);
        String hash = StringUtil.hexlate(md.digest(contents));
        if (!hash.equals(_metaDigest)) {
            String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash);
            throw new IOException(err);
        }
    }

    /**
     * Returns the digest for the digest file.
     */
    public String getMetaDigest ()
    {
        return _metaDigest;
    }

    /**
     * Computes the hash of the specified resource and compares it with the value parsed from
     * the digest file. Logs a message if the resource fails validation.
     *
     * @return true if the resource is valid, false if it failed the digest check or if an I/O
     * error was encountered during the validation process.
     */
    public boolean validateResource (Resource resource, ProgressObserver obs)
    {
        try {
            String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
            String ehash = _digests.get(resource.getPath());
            if (chash.equals(ehash)) {
                return true;
            }
            log.info("Resource failed digest check",
                     "rsrc", resource, "computed", chash, "expected", ehash);
        } catch (Throwable t) {
            log.info("Resource failed digest check", "rsrc", resource, "error", t);
        }
        return false;
    }

    /**
     * Returns the digest of the given {@code resource}.
     */
    public String getDigest (Resource resource)
    {
        return _digests.get(resource.getPath());
    }

    /** Used by {@link #createDigest} and {@link Digest}. */
    protected static void note (StringBuilder data, String path, String digest)
    {
        data.append(path).append(" = ").append(digest).append("\n");
    }

    protected HashMap _digests = new HashMap<>();
    protected String _metaDigest = "";

    protected static final String FILE_NAME = "digest";
    protected static final String FILE_SUFFIX = ".txt";
}