Merge branch 'develop' into trialMerge
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / tools / Differ.java
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
new file mode 100644 (file)
index 0000000..c2e740b
--- /dev/null
@@ -0,0 +1,232 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.tools;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import java.security.MessageDigest;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Digest;
+import com.threerings.getdown.data.EnvConfig;
+import com.threerings.getdown.data.Resource;
+import com.threerings.getdown.util.FileUtil;
+import com.threerings.getdown.util.StreamUtil;
+
+/**
+ * Generates patch files between two particular revisions of an
+ * application. The differences between all the files in the two
+ * revisions are bundled into a single patch file which is placed into the
+ * target version directory.
+ */
+public class Differ
+{
+    /**
+     * Creates a single patch file that contains the differences between
+     * the two specified application directories. The patch file will be
+     * created in the <code>nvdir</code> directory with name
+     * <code>patchV.dat</code> where V is the old application version.
+     */
+    public void createDiff (File nvdir, File ovdir, boolean verbose)
+        throws IOException
+    {
+        // sanity check
+        String nvers = nvdir.getName();
+        String overs = ovdir.getName();
+        try {
+            if (Long.parseLong(nvers) <= Long.parseLong(overs)) {
+                String err = "New version (" + nvers + ") must be greater " +
+                    "than old version (" + overs + ").";
+                throw new IOException(err);
+            }
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Non-numeric versions? [nvers=" + nvers +
+                                  ", overs=" + overs + "].");
+        }
+
+        Application oapp = new Application(new EnvConfig(ovdir));
+        oapp.init(false);
+        ArrayList<Resource> orsrcs = new ArrayList<>();
+        orsrcs.addAll(oapp.getCodeResources());
+        orsrcs.addAll(oapp.getResources());
+
+        Application napp = new Application(new EnvConfig(nvdir));
+        napp.init(false);
+        ArrayList<Resource> nrsrcs = new ArrayList<>();
+        nrsrcs.addAll(napp.getCodeResources());
+        nrsrcs.addAll(napp.getResources());
+
+        // first create a patch for the main application
+        File patch = new File(nvdir, "patch" + overs + ".dat");
+        createPatch(patch, orsrcs, nrsrcs, verbose);
+
+        // next create patches for any auxiliary resource groups
+        for (Application.AuxGroup ag : napp.getAuxGroups()) {
+            orsrcs = new ArrayList<>();
+            Application.AuxGroup oag = oapp.getAuxGroup(ag.name);
+            if (oag != null) {
+                orsrcs.addAll(oag.codes);
+                orsrcs.addAll(oag.rsrcs);
+            }
+            nrsrcs = new ArrayList<>();
+            nrsrcs.addAll(ag.codes);
+            nrsrcs.addAll(ag.rsrcs);
+            patch = new File(nvdir, "patch-" + ag.name + overs + ".dat");
+            createPatch(patch, orsrcs, nrsrcs, verbose);
+        }
+    }
+
+    protected void createPatch (File patch, ArrayList<Resource> orsrcs,
+                                ArrayList<Resource> nrsrcs, boolean verbose)
+        throws IOException
+    {
+        int version = Digest.VERSION;
+        MessageDigest md = Digest.getMessageDigest(version);
+        try (FileOutputStream fos = new FileOutputStream(patch);
+             BufferedOutputStream buffered = new BufferedOutputStream(fos);
+             JarOutputStream jout = new JarOutputStream(buffered)) {
+
+            // for each file in the new application, it either already exists
+            // in the old application, or it is new
+            for (Resource rsrc : nrsrcs) {
+                int oidx = orsrcs.indexOf(rsrc);
+                Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
+                if (orsrc != null) {
+                    // first see if they are the same
+                    String odig = orsrc.computeDigest(version, md, null);
+                    String ndig = rsrc.computeDigest(version, md, null);
+                    if (odig.equals(ndig)) {
+                        if (verbose) {
+                            System.out.println("Unchanged: " + rsrc.getPath());
+                        }
+                        // by leaving it out, it will be left as is during the
+                        // patching process
+                        continue;
+                    }
+
+                    // otherwise potentially create a jar diff
+                    if (rsrc.getPath().endsWith(".jar")) {
+                        if (verbose) {
+                            System.out.println("JarDiff: " + rsrc.getPath());
+                        }
+                        // here's a juicy one: JarDiff blindly pulls ZipEntry
+                        // objects out of one jar file and stuffs them into
+                        // another without clearing out things like the
+                        // compressed size, so if, for whatever reason (like
+                        // different JRE versions or phase of the moon) the
+                        // compressed size in the old jar file is different
+                        // than the compressed size generated when creating the
+                        // jardiff jar file, ZipOutputStream will choke and
+                        // we'll be hosed; so we recreate the jar files in
+                        // their entirety before running jardiff on 'em
+                        File otemp = rebuildJar(orsrc.getLocal());
+                        File temp = rebuildJar(rsrc.getLocal());
+                        jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.PATCH));
+                        jarDiff(otemp, temp, jout);
+                        FileUtil.deleteHarder(otemp);
+                        FileUtil.deleteHarder(temp);
+                        continue;
+                    }
+                }
+
+                if (verbose) {
+                    System.out.println("Addition: " + rsrc.getPath());
+                }
+                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.CREATE));
+                pipe(rsrc.getLocal(), jout);
+            }
+
+            // now any file remaining in orsrcs needs to be removed
+            for (Resource rsrc : orsrcs) {
+                // add an entry with the resource name and the deletion suffix
+                if (verbose) {
+                    System.out.println("Removal: " + rsrc.getPath());
+                }
+                jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.DELETE));
+            }
+
+            System.out.println("Created patch file: " + patch);
+
+        } catch (IOException ioe) {
+            FileUtil.deleteHarder(patch);
+            throw ioe;
+        }
+    }
+
+    protected File rebuildJar (File target)
+        throws IOException
+    {
+        File temp = File.createTempFile("differ", "jar");
+        try (JarFile jar = new JarFile(target);
+             FileOutputStream tempFos = new FileOutputStream(temp);
+             BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
+             JarOutputStream jout = new JarOutputStream(tempBos)) {
+            byte[] buffer = new byte[4096];
+            for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) {
+                JarEntry entry = iter.nextElement();
+                entry.setCompressedSize(-1);
+                jout.putNextEntry(entry);
+                try (InputStream in = jar.getInputStream(entry)) {
+                    int size = in.read(buffer);
+                    while (size != -1) {
+                        jout.write(buffer, 0, size);
+                        size = in.read(buffer);
+                    }
+                }
+            }
+        }
+        return temp;
+    }
+
+    protected void jarDiff (File ofile, File nfile, JarOutputStream jout)
+        throws IOException
+    {
+        JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
+    }
+
+    public static void main (String[] args)
+    {
+        if (args.length < 2) {
+            System.err.println(
+                "Usage: Differ [-verbose] new_vers_dir old_vers_dir");
+            System.exit(255);
+        }
+        Differ differ = new Differ();
+        boolean verbose = false;
+        int aidx = 0;
+        if (args[0].equals("-verbose")) {
+            verbose = true;
+            aidx++;
+        }
+        try {
+            differ.createDiff(new File(args[aidx++]),
+                              new File(args[aidx++]), verbose);
+        } catch (IOException ioe) {
+            System.err.println("Error: " + ioe.getMessage());
+            System.exit(255);
+        }
+    }
+
+    protected static void pipe (File file, JarOutputStream jout)
+        throws IOException
+    {
+        try (FileInputStream fin = new FileInputStream(file)) {
+            StreamUtil.copy(fin, jout);
+        }
+    }
+}