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:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user