Merged the tools module into core.

It wasn't actually providing any value. Neither tools nor core have any special
dependencies. The code in tools operates on the same files, formats and data
structures that core/runtime code operates on. It is simpler to have them in
the same module.
This commit is contained in:
Michael Bayne
2018-09-04 16:06:37 -07:00
parent 97ec8632ae
commit be5490dce7
11 changed files with 1 additions and 72 deletions
@@ -0,0 +1,231 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 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.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(ovdir, null);
oapp.init(false);
ArrayList<Resource> orsrcs = new ArrayList<>();
orsrcs.addAll(oapp.getCodeResources());
orsrcs.addAll(oapp.getResources());
Application napp = new Application(nvdir, null);
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);
}
}
}
@@ -0,0 +1,128 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.tools;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.ArrayList;
import java.util.List;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Handles the generation of the digest.txt file.
*/
public class Digester
{
/**
* A command line entry point for the digester.
*/
public static void main (String[] args)
throws IOException, GeneralSecurityException
{
switch (args.length) {
case 1:
createDigests(new File(args[0]), null, null, null);
break;
case 4:
createDigests(new File(args[0]), new File(args[1]), args[2], args[3]);
break;
default:
System.err.println("Usage: Digester app_dir [keystore_path password alias]");
System.exit(255);
}
}
/**
* Creates digest file(s) and optionally signs them if {@code keystore} is not null.
*/
public static void createDigests (File appdir, File keystore, String password, String alias)
throws IOException, GeneralSecurityException
{
for (int version = 1; version <= Digest.VERSION; version++) {
createDigest(version, appdir);
if (keystore != null) {
signDigest(version, appdir, keystore, password, alias);
}
}
}
/**
* Creates a digest file in the specified application directory.
*/
public static void createDigest (int version, File appdir)
throws IOException
{
File target = new File(appdir, Digest.digestFile(version));
System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business
Application app = new Application(appdir, null);
app.init(false);
List<Resource> rsrcs = new ArrayList<>();
rsrcs.add(app.getConfigResource());
rsrcs.addAll(app.getCodeResources());
rsrcs.addAll(app.getResources());
for (Application.AuxGroup ag : app.getAuxGroups()) {
rsrcs.addAll(ag.codes);
rsrcs.addAll(ag.rsrcs);
}
// now generate the digest file
Digest.createDigest(version, rsrcs, target);
}
/**
* Creates a digest file in the specified application directory.
*/
public static void signDigest (int version, File appdir,
File storePath, String storePass, String storeAlias)
throws IOException, GeneralSecurityException
{
String filename = Digest.digestFile(version);
File inputFile = new File(appdir, filename);
File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX);
try (FileInputStream storeInput = new FileInputStream(storePath);
FileInputStream dataInput = new FileInputStream(inputFile);
FileOutputStream signatureOutput = new FileOutputStream(signatureFile)) {
// initialize the keystore
KeyStore store = KeyStore.getInstance("JKS");
store.load(storeInput, storePass.toCharArray());
PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray());
// sign the digest file
String algo = Digest.sigAlgorithm(version);
Signature sig = Signature.getInstance(algo);
byte[] buffer = new byte[8192];
int length;
sig.initSign(key);
while ((length = dataInput.read(buffer)) != -1) {
sig.update(buffer, 0, length);
}
// Write out the signature
String signed = Base64.encodeToString(sig.sign(), Base64.DEFAULT);
signatureOutput.write(signed.getBytes(UTF_8));
}
}
}
@@ -0,0 +1,449 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
/*
* @(#)JarDiff.java 1.7 05/11/17
*
* Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* -Redistribution of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* -Redistribution in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of Sun Microsystems, Inc. or the names of contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
* ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
* AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
* AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
* DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
* REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
* INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
* OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
* EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed, licensed or intended
* for use in the design, construction, operation or maintenance of any
* nuclear facility.
*/
package com.threerings.getdown.tools;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* JarDiff is able to create a jar file containing the delta between two jar files (old and new).
* The delta jar file can then be applied to the old jar file to reconstruct the new jar file.
*
* <p> Refer to the JNLP spec for details on how this is done.
*
* @version 1.13, 06/26/03
*/
public class JarDiff implements JarDiffCodes
{
private static final int DEFAULT_READ_SIZE = 2048;
private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
// The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java
// and other stuff here. Instead, we use an explicit _debug flag.
private static boolean _debug;
/**
* Creates a patch from the two passed in files, writing the result to <code>os</code>.
*/
public static void createPatch (String oldPath, String newPath,
OutputStream os, boolean minimal) throws IOException
{
try (JarFile2 oldJar = new JarFile2(oldPath);
JarFile2 newJar = new JarFile2(newPath)) {
HashMap<String,String> moved = new HashMap<>();
HashSet<String> implicit = new HashSet<>();
HashSet<String> moveSrc = new HashSet<>();
HashSet<String> newEntries = new HashSet<>();
// FIRST PASS
// Go through the entries in new jar and
// determine which files are candidates for implicit moves
// ( files that has the same filename and same content in old.jar
// and new.jar )
// and for files that cannot be implicitly moved, we will either
// find out whether it is moved or new (modified)
for (JarEntry newEntry : newJar) {
String newname = newEntry.getName();
// Return best match of contents, will return a name match if possible
String oldname = oldJar.getBestMatch(newJar, newEntry);
if (oldname == null) {
// New or modified entry
if (_debug) {
System.out.println("NEW: "+ newname);
}
newEntries.add(newname);
} else {
// Content already exist - need to do a move
// Should do implicit move? Yes, if names are the same, and
// no move command already exist from oldJar
if (oldname.equals(newname) && !moveSrc.contains(oldname)) {
if (_debug) {
System.out.println(newname + " added to implicit set!");
}
implicit.add(newname);
} else {
// The 1.0.1/1.0 JarDiffPatcher cannot handle
// multiple MOVE command with same src.
// The work around here is if we are going to generate
// a MOVE command with duplicate src, we will
// instead add the target as a new file. This way
// the jardiff can be applied by 1.0.1/1.0
// JarDiffPatcher also.
if (!minimal && (implicit.contains(oldname) ||
moveSrc.contains(oldname) )) {
// generate non-minimal jardiff
// for backward compatibility
if (_debug) {
System.out.println("NEW: "+ newname);
}
newEntries.add(newname);
} else {
// Use newname as key, since they are unique
if (_debug) {
System.err.println("moved.put " + newname + " " + oldname);
}
moved.put(newname, oldname);
moveSrc.add(oldname);
}
// Check if this disables an implicit 'move <oldname> <oldname>'
if (implicit.contains(oldname) && minimal) {
if (_debug) {
System.err.println("implicit.remove " + oldname);
System.err.println("moved.put " + oldname + " " + oldname);
}
implicit.remove(oldname);
moved.put(oldname, oldname);
moveSrc.add(oldname);
}
}
}
}
// SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
// <source of move commands> - <new or modified entries>
ArrayList<String> deleted = new ArrayList<>();
for (JarEntry oldEntry : oldJar) {
String oldName = oldEntry.getName();
if (!implicit.contains(oldName) && !moveSrc.contains(oldName)
&& !newEntries.contains(oldName)) {
if (_debug) {
System.err.println("deleted.add " + oldName);
}
deleted.add(oldName);
}
}
//DEBUG
if (_debug) {
//DEBUG: print out moved map
System.out.println("MOVED MAP!!!");
for (Map.Entry<String,String> entry : moved.entrySet()) {
System.out.println(entry);
}
//DEBUG: print out IMOVE map
System.out.println("IMOVE MAP!!!");
for (String newName : implicit) {
System.out.println("key is " + newName);
}
}
JarOutputStream jos = new JarOutputStream(os);
// Write out all the MOVEs and REMOVEs
createIndex(jos, deleted, moved);
// Put in New and Modified entries
for (String newName : newEntries) {
if (_debug) {
System.out.println("New File: " + newName);
}
writeEntry(jos, newJar.getEntryByName(newName), newJar);
}
jos.finish();
// jos.close();
}
}
/**
* Writes the index file out to <code>jos</code>.
* <code>oldEntries</code> gives the names of the files that were removed,
* <code>movedMap</code> maps from the new name to the old name.
*/
private static void createIndex (JarOutputStream jos, List<String> oldEntries,
Map<String,String> movedMap)
throws IOException
{
StringWriter writer = new StringWriter();
writer.write(VERSION_HEADER);
writer.write("\r\n");
// Write out entries that have been removed
for (String name : oldEntries) {
writer.write(REMOVE_COMMAND);
writer.write(" ");
writeEscapedString(writer, name);
writer.write("\r\n");
}
// And those that have moved
for (String newName : movedMap.keySet()) {
String oldName = movedMap.get(newName);
writer.write(MOVE_COMMAND);
writer.write(" ");
writeEscapedString(writer, oldName);
writer.write(" ");
writeEscapedString(writer, newName);
writer.write("\r\n");
}
jos.putNextEntry(new JarEntry(INDEX_NAME));
byte[] bytes = writer.toString().getBytes(UTF_8);
jos.write(bytes, 0, bytes.length);
}
protected static Writer writeEscapedString (Writer writer, String string)
throws IOException
{
int index = 0;
int last = 0;
char[] chars = null;
while ((index = string.indexOf(' ', index)) != -1) {
if (last != index) {
if (chars == null) {
chars = string.toCharArray();
}
writer.write(chars, last, index - last);
}
last = index;
index++;
writer.write('\\');
}
if (last != 0 && chars != null) {
writer.write(chars, last, chars.length - last);
}
else {
// no spaces
writer.write(string);
}
return writer;
}
private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file)
throws IOException
{
try (InputStream data = file.getJarFile().getInputStream(entry)) {
jos.putNextEntry(entry);
int size = data.read(newBytes);
while (size != -1) {
jos.write(newBytes, 0, size);
size = data.read(newBytes);
}
}
}
/**
* JarFile2 wraps a JarFile providing some convenience methods.
*/
private static class JarFile2 implements Iterable<JarEntry>, Closeable
{
private JarFile _jar;
private List<JarEntry> _entries;
private HashMap<String,JarEntry> _nameToEntryMap;
private HashMap<Long,LinkedList<JarEntry>> _crcToEntryMap;
public JarFile2 (String path) throws IOException {
_jar = new JarFile(new File(path));
index();
}
public JarFile getJarFile () {
return _jar;
}
// from interface Iterable<JarEntry>
@Override
public Iterator<JarEntry> iterator () {
return _entries.iterator();
}
public JarEntry getEntryByName (String name) {
return _nameToEntryMap.get(name);
}
/**
* Returns true if the two InputStreams differ.
*/
private static boolean differs (InputStream oldIS, InputStream newIS) throws IOException {
int newSize = 0;
int oldSize;
int total = 0;
boolean retVal = false;
while (newSize != -1) {
newSize = newIS.read(newBytes);
oldSize = oldIS.read(oldBytes);
if (newSize != oldSize) {
if (_debug) {
System.out.println("\tread sizes differ: " + newSize +
" " + oldSize + " total " + total);
}
retVal = true;
break;
}
if (newSize > 0) {
while (--newSize >= 0) {
total++;
if (newBytes[newSize] != oldBytes[newSize]) {
if (_debug) {
System.out.println("\tbytes differ at " +
total);
}
retVal = true;
break;
}
if ( retVal ) {
//Jump out
break;
}
newSize = 0;
}
}
}
return retVal;
}
public String getBestMatch (JarFile2 file, JarEntry entry) throws IOException {
// check for same name and same content, return name if found
if (contains(file, entry)) {
return (entry.getName());
}
// return name of same content file or null
return (hasSameContent(file,entry));
}
public boolean contains (JarFile2 f, JarEntry e) throws IOException {
JarEntry thisEntry = getEntryByName(e.getName());
// Look up name in 'this' Jar2File - if not exist return false
if (thisEntry == null)
return false;
// Check CRC - if no match - return false
if (thisEntry.getCrc() != e.getCrc())
return false;
// Check contents - if no match - return false
try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
InputStream newIS = f.getJarFile().getInputStream(e)) {
return !differs(oldIS, newIS);
}
}
public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException {
String thisName = null;
Long crcL = new Long(entry.getCrc());
// check if this jar contains files with the passed in entry's crc
if (_crcToEntryMap.containsKey(crcL)) {
// get the Linked List with files with the crc
LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
// go through the list and check for content match
ListIterator<JarEntry> li = ll.listIterator(0);
while (li.hasNext()) {
JarEntry thisEntry = li.next();
// check for content match
try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
InputStream newIS = file.getJarFile().getInputStream(entry)) {
if (!differs(oldIS, newIS)) {
thisName = thisEntry.getName();
return thisName;
}
}
}
}
return thisName;
}
private void index () throws IOException {
Enumeration<JarEntry> entries = _jar.entries();
_nameToEntryMap = new HashMap<>();
_crcToEntryMap = new HashMap<>();
_entries = new ArrayList<>();
if (_debug) {
System.out.println("indexing: " + _jar.getName());
}
if (entries != null) {
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
long crc = entry.getCrc();
Long crcL = new Long(crc);
if (_debug) {
System.out.println("\t" + entry.getName() + " CRC " + crc);
}
_nameToEntryMap.put(entry.getName(), entry);
_entries.add(entry);
// generate the CRC to entries map
if (_crcToEntryMap.containsKey(crcL)) {
// key exist, add the entry to the correcponding linked list
LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
ll.add(entry);
_crcToEntryMap.put(crcL, ll);
} else {
// create a new entry in the hashmap for the new key
LinkedList<JarEntry> ll = new LinkedList<JarEntry>();
ll.add(entry);
_crcToEntryMap.put(crcL, ll);
}
}
}
}
@Override
public void close() throws IOException {
_jar.close();
}
}
}
@@ -0,0 +1,24 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.tools;
/**
* Constants shared by {@link JarDiff} and {@link JarDiffPatcher}.
*/
public interface JarDiffCodes
{
/** The name of the jardiff control file. */
String INDEX_NAME = "META-INF/INDEX.JD";
/** The version header used in the control file. */
String VERSION_HEADER = "version 1.0";
/** A jardiff command to remove an entry. */
String REMOVE_COMMAND = "remove";
/** A jardiff command to move an entry. */
String MOVE_COMMAND = "move";
}
@@ -0,0 +1,292 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import com.threerings.getdown.util.ProgressObserver;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Applies a jardiff patch to a jar file.
*/
public class JarDiffPatcher implements JarDiffCodes
{
/**
* Patches the specified jar file using the supplied patch file and writing
* the new jar file to the supplied target.
*
* @param jarPath the path to the original jar file.
* @param diffPath the path to the jardiff patch file.
* @param target the output stream to which we will write the patched jar.
* @param observer an optional observer to be notified of patching progress.
*
* @throws IOException if any problem occurs during patching.
*/
public void patchJar (String jarPath, String diffPath, File target, ProgressObserver observer)
throws IOException
{
File oldFile = new File(jarPath), diffFile = new File(diffPath);
try (JarFile oldJar = new JarFile(oldFile);
JarFile jarDiff = new JarFile(diffFile);
JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
Set<String> ignoreSet = new HashSet<>();
Map<String, String> renameMap = new HashMap<>();
determineNameMapping(jarDiff, ignoreSet, renameMap);
// get all keys in renameMap
String[] keys = renameMap.keySet().toArray(new String[renameMap.size()]);
// Files to implicit move
Set<String> oldjarNames = new HashSet<>();
Enumeration<JarEntry> oldEntries = oldJar.entries();
if (oldEntries != null) {
while (oldEntries.hasMoreElements()) {
oldjarNames.add(oldEntries.nextElement().getName());
}
}
// size depends on the three parameters below, which is basically the
// counter for each loop that do the actual writes to the output file
// since oldjarNames.size() changes in the first two loop below, we
// need to adjust the size accordingly also when oldjarNames.size()
// changes
double size = oldjarNames.size() + keys.length + jarDiff.size();
double currentEntry = 0;
// Handle all remove commands
oldjarNames.removeAll(ignoreSet);
size -= ignoreSet.size();
// Add content from JARDiff
Enumeration<JarEntry> entries = jarDiff.entries();
if (entries != null) {
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (!INDEX_NAME.equals(entry.getName())) {
updateObserver(observer, currentEntry, size);
currentEntry++;
writeEntry(jos, entry, jarDiff);
// Remove entry from oldjarNames since no implicit move is
// needed
boolean wasInOld = oldjarNames.remove(entry.getName());
// Update progress counters. If it was in old, we do not
// need an implicit move, so adjust total size.
if (wasInOld) {
size--;
}
} else {
// no write is done, decrement size
size--;
}
}
}
// go through the renameMap and apply move for each entry
for (String newName : keys) {
// Apply move <oldName> <newName> command
String oldName = renameMap.get(newName);
// Get source JarEntry
JarEntry oldEntry = oldJar.getJarEntry(oldName);
if (oldEntry == null) {
String moveCmd = MOVE_COMMAND + oldName + " " + newName;
throw new IOException("error.badmove: " + moveCmd);
}
// Create dest JarEntry
JarEntry newEntry = new JarEntry(newName);
newEntry.setTime(oldEntry.getTime());
newEntry.setSize(oldEntry.getSize());
newEntry.setCompressedSize(oldEntry.getCompressedSize());
newEntry.setCrc(oldEntry.getCrc());
newEntry.setMethod(oldEntry.getMethod());
newEntry.setExtra(oldEntry.getExtra());
newEntry.setComment(oldEntry.getComment());
updateObserver(observer, currentEntry, size);
currentEntry++;
try (InputStream data = oldJar.getInputStream(oldEntry)) {
writeEntry(jos, newEntry, data);
}
// Remove entry from oldjarNames since no implicit move is needed
boolean wasInOld = oldjarNames.remove(oldName);
// Update progress counters. If it was in old, we do not need an
// implicit move, so adjust total size.
if (wasInOld) {
size--;
}
}
// implicit move
Iterator<String> iEntries = oldjarNames.iterator();
if (iEntries != null) {
while (iEntries.hasNext()) {
String name = iEntries.next();
JarEntry entry = oldJar.getJarEntry(name);
if (entry == null) {
// names originally retrieved from the JAR, so this should never happen
throw new AssertionError("JAR entry not found: " + name);
}
updateObserver(observer, currentEntry, size);
currentEntry++;
writeEntry(jos, entry, oldJar);
}
}
updateObserver(observer, currentEntry, size);
}
}
protected void updateObserver (ProgressObserver observer, double currentSize, double size)
{
if (observer != null) {
observer.progress((int)(100*currentSize/size));
}
}
protected void determineNameMapping (
JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
throws IOException
{
InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
if (is == null) {
throw new IOException("error.noindex");
}
LineNumberReader indexReader =
new LineNumberReader(new InputStreamReader(is, UTF_8));
String line = indexReader.readLine();
if (line == null || !line.equals(VERSION_HEADER)) {
throw new IOException("jardiff.error.badheader: " + line);
}
while ((line = indexReader.readLine()) != null) {
if (line.startsWith(REMOVE_COMMAND)) {
List<String> sub = getSubpaths(
line.substring(REMOVE_COMMAND.length()));
if (sub.size() != 1) {
throw new IOException("error.badremove: " + line);
}
ignoreSet.add(sub.get(0));
} else if (line.startsWith(MOVE_COMMAND)) {
List<String> sub = getSubpaths(
line.substring(MOVE_COMMAND.length()));
if (sub.size() != 2) {
throw new IOException("error.badmove: " + line);
}
// target of move should be the key
if (renameMap.put(sub.get(1), sub.get(0)) != null) {
// invalid move - should not move to same target twice
throw new IOException("error.badmove: " + line);
}
} else if (line.length() > 0) {
throw new IOException("error.badcommand: " + line);
}
}
}
protected List<String> getSubpaths (String path)
{
int index = 0;
int length = path.length();
ArrayList<String> sub = new ArrayList<>();
while (index < length) {
while (index < length && Character.isWhitespace
(path.charAt(index))) {
index++;
}
if (index < length) {
int start = index;
int last = start;
String subString = null;
while (index < length) {
char aChar = path.charAt(index);
if (aChar == '\\' && (index + 1) < length &&
path.charAt(index + 1) == ' ') {
if (subString == null) {
subString = path.substring(last, index);
} else {
subString += path.substring(last, index);
}
last = ++index;
} else if (Character.isWhitespace(aChar)) {
break;
}
index++;
}
if (last != index) {
if (subString == null) {
subString = path.substring(last, index);
} else {
subString += path.substring(last, index);
}
}
sub.add(subString);
}
}
return sub;
}
protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
throws IOException
{
try (InputStream data = file.getInputStream(entry)) {
writeEntry(jos, entry, data);
}
}
protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
throws IOException
{
jos.putNextEntry(new JarEntry(entry.getName()));
// Read the entry
int size = data.read(newBytes);
while (size != -1) {
jos.write(newBytes, 0, size);
size = data.read(newBytes);
}
}
protected static final int DEFAULT_READ_SIZE = 2048;
protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
protected static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
}
@@ -0,0 +1,205 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.ProgressObserver;
import com.threerings.getdown.util.StreamUtil;
import static com.threerings.getdown.Log.log;
/**
* Applies a unified patch file to an application directory, providing
* percentage completion feedback along the way. <em>Note:</em> the
* patcher is not thread safe. Create a separate patcher instance for each
* patching action that is desired.
*/
public class Patcher
{
/** A suffix appended to file names to indicate that a file should be newly created. */
public static final String CREATE = ".create";
/** A suffix appended to file names to indicate that a file should be patched. */
public static final String PATCH = ".patch";
/** A suffix appended to file names to indicate that a file should be deleted. */
public static final String DELETE = ".delete";
/**
* Applies the specified patch file to the application living in the
* specified application directory. The supplied observer, if
* non-null, will be notified of progress along the way.
*
* <p><em>Note:</em> this method runs on the calling thread, thus the
* caller may want to make use of a separate thread in conjunction
* with the patcher so that the user interface is not blocked for the
* duration of the patch.
*/
public void patch (File appdir, File patch, ProgressObserver obs)
throws IOException
{
// save this information for later
_obs = obs;
_plength = patch.length();
try (JarFile file = new JarFile(patch)) {
Enumeration<JarEntry> entries = file.entries(); // old skool!
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String path = entry.getName();
long elength = entry.getCompressedSize();
// depending on the suffix, we do The Right Thing (tm)
if (path.endsWith(CREATE)) {
path = strip(path, CREATE);
System.out.println("Creating " + path + "...");
createFile(file, entry, new File(appdir, path));
} else if (path.endsWith(PATCH)) {
path = strip(path, PATCH);
System.out.println("Patching " + path + "...");
patchFile(file, entry, appdir, path);
} else if (path.endsWith(DELETE)) {
path = strip(path, DELETE);
System.out.println("Removing " + path + "...");
File target = new File(appdir, path);
if (!FileUtil.deleteHarder(target)) {
System.err.println("Failure deleting '" + target + "'.");
}
} else {
System.err.println("Skipping bogus patch file entry: " + path);
}
// note that we've completed this entry
_complete += elength;
}
}
}
protected String strip (String path, String suffix)
{
return path.substring(0, path.length() - suffix.length());
}
protected void createFile (JarFile file, ZipEntry entry, File target)
{
// create our copy buffer if necessary
if (_buffer == null) {
_buffer = new byte[COPY_BUFFER_SIZE];
}
// make sure the file's parent directory exists
File pdir = target.getParentFile();
if (!pdir.exists() && !pdir.mkdirs()) {
log.warning("Failed to create parent for '" + target + "'.");
}
try (InputStream in = file.getInputStream(entry);
FileOutputStream fout = new FileOutputStream(target)) {
int total = 0, read;
while ((read = in.read(_buffer)) != -1) {
total += read;
fout.write(_buffer, 0, read);
updateProgress(total);
}
} catch (IOException ioe) {
System.err.println("Error creating '" + target + "': " + ioe);
}
}
protected void patchFile (JarFile file, ZipEntry entry,
File appdir, String path)
{
File target = new File(appdir, path);
File patch = new File(appdir, entry.getName());
File otarget = new File(appdir, path + ".old");
JarDiffPatcher patcher = null;
// make sure no stale old target is lying around to mess us up
FileUtil.deleteHarder(otarget);
// pipe the contents of the patch into a file
try (InputStream in = file.getInputStream(entry);
FileOutputStream fout = new FileOutputStream(patch)) {
StreamUtil.copy(in, fout);
StreamUtil.close(fout);
// move the current version of the jar to .old
if (!FileUtil.renameTo(target, otarget)) {
System.err.println("Failed to .oldify '" + target + "'.");
return;
}
// we'll need this to pass progress along to our observer
final long elength = entry.getCompressedSize();
ProgressObserver obs = new ProgressObserver() {
public void progress (int percent) {
updateProgress((int)(percent * elength / 100));
}
};
// now apply the patch to create the new target file
patcher = new JarDiffPatcher();
patcher.patchJar(otarget.getPath(), patch.getPath(), target, obs);
} catch (IOException ioe) {
if (patcher == null) {
System.err.println("Failed to write patch file '" + patch + "': " + ioe);
} else {
System.err.println("Error patching '" + target + "': " + ioe);
}
} finally {
// clean up our temporary files
FileUtil.deleteHarder(patch);
FileUtil.deleteHarder(otarget);
}
}
protected void updateProgress (int progress)
{
if (_obs != null) {
_obs.progress((int)(100 * (_complete + progress) / _plength));
}
}
public static void main (String[] args)
{
if (args.length != 2) {
System.err.println("Usage: Patcher appdir patch_file");
System.exit(-1);
}
Patcher patcher = new Patcher();
try {
patcher.patch(new File(args[0]), new File(args[1]), null);
} catch (IOException ioe) {
System.err.println("Error: " + ioe.getMessage());
System.exit(-1);
}
}
protected ProgressObserver _obs;
protected long _complete, _plength;
protected byte[] _buffer;
protected static final int COPY_BUFFER_SIZE = 4096;
}