Now that the JDK is open source, we can just fork JarDiff and eliminate our
annoying dependency on javaws.jar (which no longer even includes JarDiff, now that lives in $JAVA_HOME/samples/jnlp/servlet/jardiff.jar). We had already forked JarDiffPatcher, so this completes the circle. Now I can also directly fix some of the annoying JarDiff bugs we were working around, which I'll do at some point.
This commit is contained in:
@@ -27,22 +27,6 @@
|
||||
<fileset dir="${deploy.dir}/lib" includes="*.jar"/>
|
||||
</path>
|
||||
|
||||
<!-- checks the availability of certain libraries -->
|
||||
<target name="check-available" depends="prepare">
|
||||
<available property="snark.present"
|
||||
classname="org.klomp.snark.Snark" classpathref="clazzpath"/>
|
||||
<echo message="Have Snark: ${snark.present}"/>
|
||||
<condition property="build.torrent">
|
||||
<isset property="snark.present"/>
|
||||
</condition>
|
||||
<available property="javaws.present"
|
||||
classname="com.sun.javaws.jardiff.JarDiff" classpathref="clazzpath"/>
|
||||
<echo message="Have Java Web Start: ${javaws.present}"/>
|
||||
<condition property="build.differ">
|
||||
<isset property="javaws.present"/>
|
||||
</condition>
|
||||
</target>
|
||||
|
||||
<!-- prepares the application directories -->
|
||||
<target name="prepare">
|
||||
<mkdir dir="${deploy.dir}/lib"/>
|
||||
@@ -61,9 +45,6 @@
|
||||
<copy todir="${deploy.dir}/lib" flatten="true">
|
||||
<fileset refid="${app.name}.libs"/>
|
||||
</copy>
|
||||
<!-- first look in libs.dir for javaws.jar then try java.home -->
|
||||
<copy todir="${deploy.dir}/lib" file="${libs.dir}/javaws.jar" failonerror="false"/>
|
||||
<copy todir="${deploy.dir}/lib" file="${java.home}/lib/javaws.jar" failonerror="false"/>
|
||||
</target>
|
||||
|
||||
<!-- cleans out the intermediate build files -->
|
||||
@@ -78,12 +59,9 @@
|
||||
<delete dir="${deploy.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="compile" depends="check-available" description="Compiles main classes.">
|
||||
<javac srcdir="${src.dir}" destdir="${deploy.dir}/classes"
|
||||
classpathref="clazzpath" includeAntRuntime="no"
|
||||
debug="on" deprecation="on" source="1.5" target="1.5">
|
||||
<exclude name="**/TorrentDownloader*" unless="build.torrent"/>
|
||||
<exclude name="**/tools/Differ*" unless="build.differ"/>
|
||||
<target name="compile" depends="prepare" description="Compiles main classes.">
|
||||
<javac srcdir="${src.dir}" destdir="${deploy.dir}/classes" includeAntRuntime="no"
|
||||
classpathref="clazzpath" debug="on" deprecation="on" source="1.5" target="1.5">
|
||||
<compilerarg value="-Xlint"/>
|
||||
<compilerarg value="-Xlint:-serial"/>
|
||||
</javac>
|
||||
@@ -117,10 +95,10 @@
|
||||
</target>
|
||||
|
||||
<!-- a target for rebuilding everything -->
|
||||
<target name="all" depends="clean,check-available,compile,javadoc,dist"/>
|
||||
<target name="all" depends="clean,compile,javadoc,dist"/>
|
||||
|
||||
<!-- builds our distribution files -->
|
||||
<target name="dist" depends="check-available,compile">
|
||||
<target name="dist" depends="compile">
|
||||
<jar destfile="${deploy.dir}/${app.name}.jar" manifest="lib/manifest.mf"
|
||||
basedir="${deploy.dir}/classes" excludes="**/netscape/**"/>
|
||||
</target>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
samskivert.jar
|
||||
ant.jar
|
||||
commons-codec.jar
|
||||
commons-io.jar
|
||||
javaws.jar
|
||||
snark.jar
|
||||
@@ -40,8 +40,6 @@ import java.util.zip.ZipEntry;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import com.sun.javaws.jardiff.JarDiff;
|
||||
|
||||
import com.samskivert.io.StreamUtil;
|
||||
|
||||
import com.threerings.getdown.data.Application;
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
* @(#)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 java.util.zip.*;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public static final String VERSION_HEADER = "version 1.0";
|
||||
public static final String INDEX_NAME = "META-INF/INDEX.JD";
|
||||
public static final String REMOVE_COMMAND = "remove";
|
||||
public static final String MOVE_COMMAND = "move";
|
||||
|
||||
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
|
||||
{
|
||||
JarFile2 oldJar = new JarFile2(oldPath);
|
||||
JarFile2 newJar = new JarFile2(newPath);
|
||||
|
||||
try {
|
||||
HashMap<String,String> moved = new HashMap<String,String>();
|
||||
HashSet<String> implicit = new HashSet<String>();
|
||||
HashSet<String> moveSrc = new HashSet<String>();
|
||||
HashSet<String> newEntries = new HashSet<String>();
|
||||
|
||||
// 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<String>();
|
||||
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 (String newName : moved.keySet()) {
|
||||
String oldName = moved.get(newName);
|
||||
System.out.println("key is " + newName + " value is " + oldName);
|
||||
}
|
||||
|
||||
//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();
|
||||
|
||||
} catch (IOException ioE){
|
||||
throw ioE;
|
||||
} finally {
|
||||
try {
|
||||
oldJar.getJarFile().close();
|
||||
} catch (IOException e1) {
|
||||
//ignore
|
||||
}
|
||||
try {
|
||||
newJar.getJarFile().close();
|
||||
} catch (IOException e1) {
|
||||
//ignore
|
||||
}
|
||||
} // finally
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
JarEntry je = new JarEntry(INDEX_NAME);
|
||||
byte[] bytes = writer.toString().getBytes("UTF-8");
|
||||
|
||||
writer.close();
|
||||
jos.putNextEntry(je);
|
||||
jos.write(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
private static void 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) {
|
||||
writer.write(chars, last, chars.length - last);
|
||||
}
|
||||
else {
|
||||
// no spaces
|
||||
writer.write(string);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file)
|
||||
throws IOException
|
||||
{
|
||||
writeEntry(jos, entry, file.getJarFile().getInputStream(entry));
|
||||
}
|
||||
|
||||
private static void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
|
||||
throws IOException
|
||||
{
|
||||
jos.putNextEntry(entry);
|
||||
|
||||
try {
|
||||
// Read the entry
|
||||
int size = data.read(newBytes);
|
||||
|
||||
while (size != -1) {
|
||||
jos.write(newBytes, 0, size);
|
||||
size = data.read(newBytes);
|
||||
}
|
||||
} catch(IOException ioE) {
|
||||
throw ioE;
|
||||
} finally {
|
||||
try {
|
||||
data.close();
|
||||
} catch(IOException e){
|
||||
//Ignore
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JarFile2 wraps a JarFile providing some convenience methods.
|
||||
*/
|
||||
private static class JarFile2 implements Iterable<JarEntry>
|
||||
{
|
||||
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>
|
||||
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;
|
||||
|
||||
try{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException ioE){
|
||||
throw ioE;
|
||||
} finally {
|
||||
try {
|
||||
oldIS.close();
|
||||
} catch(IOException e){
|
||||
//Ignore
|
||||
}
|
||||
try {
|
||||
newIS.close();
|
||||
} catch(IOException e){
|
||||
//Ignore
|
||||
}
|
||||
}
|
||||
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
|
||||
InputStream oldIS = getJarFile().getInputStream(thisEntry);
|
||||
InputStream newIS = f.getJarFile().getInputStream(e);
|
||||
boolean retValue = differs(oldIS, newIS);
|
||||
|
||||
return !retValue;
|
||||
}
|
||||
|
||||
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
|
||||
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<String,JarEntry>();
|
||||
_crcToEntryMap = new HashMap<Long,LinkedList<JarEntry>>();
|
||||
_entries = new ArrayList<JarEntry>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user