Split Getdown into multiple (Maven) modules.

- core: the main Getdown logic code
- tools: code to create digest.txt & patch files
- launcher: the standalone launcher/updater app
- ant: the Ant task for creating digest.txt files

This paves the way for a proper Jigsaw-ification of the Getdown code.

I may further factor code out of getdown-launcher and into getdown-core to
enable the use-case where an app embeds Getdown completely and does not use the
launcher app/UI. That will also make it easier to create a JavaFX UI and retire
the old Swing UI.

This also moves the obsolete applet code into a separate applet module, which
is merely a temporary holding area. It will be deleted in the next commit.
This commit is contained in:
Michael Bayne
2018-09-04 09:11:29 -07:00
parent 8ff9bf4d68
commit 4c383f99c7
63 changed files with 386 additions and 264 deletions
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.threerings</groupId>
<artifactId>getdown</artifactId>
<version>1.7.2-SNAPSHOT</version>
</parent>
<artifactId>getdown-tools</artifactId>
<packaging>jar</packaging>
<name>Getdown App</name>
<description>Tools for creating digests and patch files</description>
<dependencies>
<dependency>
<groupId>com.threerings</groupId>
<artifactId>getdown-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,62 @@
//
// 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.BufferedInputStream;
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;
import com.threerings.getdown.data.Digest;
import com.threerings.getdown.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Produces a signed hash of the appbase, appname, and image path to ensure that signed copies of
* Getdown are not hijacked to run malicious code.
*/
public class AppletParamSigner
{
public static void main (String[] args)
{
if (args.length != 7) {
System.err.println("AppletParamSigner keystore storepass alias keypass " +
"appbase appname imgpath");
System.exit(255);
}
String keystore = args[0];
String storepass = args[1];
String alias = args[2];
String keypass = args[3];
String appbase = args[4];
String appname = args[5];
String imgpath = args[6];
String params = appbase + appname + imgpath;
try (FileInputStream fis = new FileInputStream(keystore);
BufferedInputStream bis = new BufferedInputStream(fis)) {
KeyStore store = KeyStore.getInstance("JKS");
store.load(bis, storepass.toCharArray());
PrivateKey key = (PrivateKey)store.getKey(alias, keypass.toCharArray());
Signature sig = Signature.getInstance(Digest.sigAlgorithm(Digest.VERSION));
sig.initSign(key);
sig.update(params.getBytes(UTF_8));
String signed = Base64.encodeToString(sig.sign(), Base64.DEFAULT);
System.out.println("<param name=\"appbase\" value=\"" + appbase + "\" />");
System.out.println("<param name=\"appname\" value=\"" + appname + "\" />");
System.out.println("<param name=\"bgimage\" value=\"" + imgpath + "\" />");
System.out.println("<param name=\"signature\" value=\"" + signed + "\" />");
} catch (Exception e) {
System.err.println("Failed to produce signature.");
e.printStackTrace();
}
}
}
@@ -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.samskivert.io.StreamUtil;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.FileUtil;
/**
* 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,206 @@
//
// 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.samskivert.io.StreamUtil;
import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.ProgressObserver;
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;
}
@@ -0,0 +1,32 @@
//
// 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.IOException;
import java.io.StringWriter;
import org.junit.Test;
import static com.threerings.getdown.tools.JarDiff.writeEscapedString;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link JarDiff}.
*/
public class JarDiffTest {
@Test
public void testWriteEscapedString () throws IOException
{
assertEquals("abc", writeEscapedString(new StringWriter(), "abc").toString());
assertEquals("abc\\ xyz", writeEscapedString(new StringWriter(), "abc xyz").toString());
assertEquals("\\ xyz", writeEscapedString(new StringWriter(), " xyz").toString());
assertEquals("abc\\ ", writeEscapedString(new StringWriter(), "abc ").toString());
assertEquals("\\ ", writeEscapedString(new StringWriter(), " ").toString());
assertEquals("", writeEscapedString(new StringWriter(), "").toString());
}
}