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.tools;
8 import java.io.BufferedOutputStream;
10 import java.io.FileInputStream;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
15 import java.util.ArrayList;
16 import java.util.Enumeration;
17 import java.util.jar.JarEntry;
18 import java.util.jar.JarFile;
19 import java.util.jar.JarOutputStream;
20 import java.util.zip.ZipEntry;
22 import java.security.MessageDigest;
24 import com.threerings.getdown.data.Application;
25 import com.threerings.getdown.data.Digest;
26 import com.threerings.getdown.data.EnvConfig;
27 import com.threerings.getdown.data.Resource;
28 import com.threerings.getdown.util.FileUtil;
29 import com.threerings.getdown.util.StreamUtil;
32 * Generates patch files between two particular revisions of an
33 * application. The differences between all the files in the two
34 * revisions are bundled into a single patch file which is placed into the
35 * target version directory.
40 * Creates a single patch file that contains the differences between
41 * the two specified application directories. The patch file will be
42 * created in the <code>nvdir</code> directory with name
43 * <code>patchV.dat</code> where V is the old application version.
45 public void createDiff (File nvdir, File ovdir, boolean verbose)
49 String nvers = nvdir.getName();
50 String overs = ovdir.getName();
52 if (Long.parseLong(nvers) <= Long.parseLong(overs)) {
53 String err = "New version (" + nvers + ") must be greater " +
54 "than old version (" + overs + ").";
55 throw new IOException(err);
57 } catch (NumberFormatException nfe) {
58 throw new IOException("Non-numeric versions? [nvers=" + nvers +
59 ", overs=" + overs + "].");
62 Application oapp = new Application(new EnvConfig(ovdir));
64 ArrayList<Resource> orsrcs = new ArrayList<>();
65 orsrcs.addAll(oapp.getCodeResources());
66 orsrcs.addAll(oapp.getResources());
68 Application napp = new Application(new EnvConfig(nvdir));
70 ArrayList<Resource> nrsrcs = new ArrayList<>();
71 nrsrcs.addAll(napp.getCodeResources());
72 nrsrcs.addAll(napp.getResources());
74 // first create a patch for the main application
75 File patch = new File(nvdir, "patch" + overs + ".dat");
76 createPatch(patch, orsrcs, nrsrcs, verbose);
78 // next create patches for any auxiliary resource groups
79 for (Application.AuxGroup ag : napp.getAuxGroups()) {
80 orsrcs = new ArrayList<>();
81 Application.AuxGroup oag = oapp.getAuxGroup(ag.name);
83 orsrcs.addAll(oag.codes);
84 orsrcs.addAll(oag.rsrcs);
86 nrsrcs = new ArrayList<>();
87 nrsrcs.addAll(ag.codes);
88 nrsrcs.addAll(ag.rsrcs);
89 patch = new File(nvdir, "patch-" + ag.name + overs + ".dat");
90 createPatch(patch, orsrcs, nrsrcs, verbose);
94 protected void createPatch (File patch, ArrayList<Resource> orsrcs,
95 ArrayList<Resource> nrsrcs, boolean verbose)
98 int version = Digest.VERSION;
99 MessageDigest md = Digest.getMessageDigest(version);
100 try (FileOutputStream fos = new FileOutputStream(patch);
101 BufferedOutputStream buffered = new BufferedOutputStream(fos);
102 JarOutputStream jout = new JarOutputStream(buffered)) {
104 // for each file in the new application, it either already exists
105 // in the old application, or it is new
106 for (Resource rsrc : nrsrcs) {
107 int oidx = orsrcs.indexOf(rsrc);
108 Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
110 // first see if they are the same
111 String odig = orsrc.computeDigest(version, md, null);
112 String ndig = rsrc.computeDigest(version, md, null);
113 if (odig.equals(ndig)) {
115 System.out.println("Unchanged: " + rsrc.getPath());
117 // by leaving it out, it will be left as is during the
122 // otherwise potentially create a jar diff
123 if (rsrc.getPath().endsWith(".jar")) {
125 System.out.println("JarDiff: " + rsrc.getPath());
127 // here's a juicy one: JarDiff blindly pulls ZipEntry
128 // objects out of one jar file and stuffs them into
129 // another without clearing out things like the
130 // compressed size, so if, for whatever reason (like
131 // different JRE versions or phase of the moon) the
132 // compressed size in the old jar file is different
133 // than the compressed size generated when creating the
134 // jardiff jar file, ZipOutputStream will choke and
135 // we'll be hosed; so we recreate the jar files in
136 // their entirety before running jardiff on 'em
137 File otemp = rebuildJar(orsrc.getLocal());
138 File temp = rebuildJar(rsrc.getLocal());
139 jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.PATCH));
140 jarDiff(otemp, temp, jout);
141 FileUtil.deleteHarder(otemp);
142 FileUtil.deleteHarder(temp);
148 System.out.println("Addition: " + rsrc.getPath());
150 jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.CREATE));
151 pipe(rsrc.getLocal(), jout);
154 // now any file remaining in orsrcs needs to be removed
155 for (Resource rsrc : orsrcs) {
156 // add an entry with the resource name and the deletion suffix
158 System.out.println("Removal: " + rsrc.getPath());
160 jout.putNextEntry(new ZipEntry(rsrc.getPath() + Patcher.DELETE));
163 System.out.println("Created patch file: " + patch);
165 } catch (IOException ioe) {
166 FileUtil.deleteHarder(patch);
171 protected File rebuildJar (File target)
174 File temp = File.createTempFile("differ", "jar");
175 try (JarFile jar = new JarFile(target);
176 FileOutputStream tempFos = new FileOutputStream(temp);
177 BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
178 JarOutputStream jout = new JarOutputStream(tempBos)) {
179 byte[] buffer = new byte[4096];
180 for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) {
181 JarEntry entry = iter.nextElement();
182 entry.setCompressedSize(-1);
183 jout.putNextEntry(entry);
184 try (InputStream in = jar.getInputStream(entry)) {
185 int size = in.read(buffer);
187 jout.write(buffer, 0, size);
188 size = in.read(buffer);
196 protected void jarDiff (File ofile, File nfile, JarOutputStream jout)
199 JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
202 public static void main (String[] args)
204 if (args.length < 2) {
206 "Usage: Differ [-verbose] new_vers_dir old_vers_dir");
209 Differ differ = new Differ();
210 boolean verbose = false;
212 if (args[0].equals("-verbose")) {
217 differ.createDiff(new File(args[aidx++]),
218 new File(args[aidx++]), verbose);
219 } catch (IOException ioe) {
220 System.err.println("Error: " + ioe.getMessage());
225 protected static void pipe (File file, JarOutputStream jout)
228 try (FileInputStream fin = new FileInputStream(file)) {
229 StreamUtil.copy(fin, jout);