X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=getdown%2Fsrc%2Fgetdown%2Fcore%2Fsrc%2Fmain%2Fjava%2Fcom%2Fthreerings%2Fgetdown%2Ftools%2FDiffer.java;fp=getdown%2Fsrc%2Fgetdown%2Fcore%2Fsrc%2Fmain%2Fjava%2Fcom%2Fthreerings%2Fgetdown%2Ftools%2FDiffer.java;h=c2e740b6e17769adde41dbed8b6e866c883b2f0d;hb=8946f41687f4c822ac8d15ee8551f23f156735c4;hp=0000000000000000000000000000000000000000;hpb=f27f7be4c32780de615e2678f11a5e80702c5e25;p=jalview.git 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 index 0000000..c2e740b --- /dev/null +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java @@ -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 nvdir directory with name + * patchV.dat 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 orsrcs = new ArrayList<>(); + orsrcs.addAll(oapp.getCodeResources()); + orsrcs.addAll(oapp.getResources()); + + Application napp = new Application(new EnvConfig(nvdir)); + napp.init(false); + ArrayList 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 orsrcs, + ArrayList 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); + } + } +}