X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=getdown%2Fsrc%2Fgetdown%2Fcore%2Fsrc%2Fmain%2Fjava%2Fcom%2Fthreerings%2Fgetdown%2Ftools%2FJarDiff.java;fp=getdown%2Fsrc%2Fgetdown%2Fcore%2Fsrc%2Fmain%2Fjava%2Fcom%2Fthreerings%2Fgetdown%2Ftools%2FJarDiff.java;h=1cea0eacdeabae21bfea471aea5da3e164f76dd1;hb=8946f41687f4c822ac8d15ee8551f23f156735c4;hp=0000000000000000000000000000000000000000;hpb=f27f7be4c32780de615e2678f11a5e80702c5e25;p=jalview.git diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java new file mode 100644 index 0000000..1cea0ea --- /dev/null +++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java @@ -0,0 +1,449 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2018 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. + * + *

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 os. + */ + 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 moved = new HashMap<>(); + HashSet implicit = new HashSet<>(); + HashSet moveSrc = new HashSet<>(); + HashSet 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 ' + 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: = - - + // - + ArrayList 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 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 jos. + * oldEntries gives the names of the files that were removed, + * movedMap maps from the new name to the old name. + */ + private static void createIndex (JarOutputStream jos, List oldEntries, + Map 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, Closeable + { + private JarFile _jar; + private List _entries; + private HashMap _nameToEntryMap; + private HashMap> _crcToEntryMap; + + public JarFile2 (String path) throws IOException { + _jar = new JarFile(new File(path)); + index(); + } + + public JarFile getJarFile () { + return _jar; + } + + // from interface Iterable + @Override + public Iterator 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 = Long.valueOf(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 ll = _crcToEntryMap.get(crcL); + // go through the list and check for content match + ListIterator 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 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 = Long.valueOf(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 ll = _crcToEntryMap.get(crcL); + ll.add(entry); + _crcToEntryMap.put(crcL, ll); + + } else { + // create a new entry in the hashmap for the new key + LinkedList ll = new LinkedList(); + ll.add(entry); + _crcToEntryMap.put(crcL, ll); + } + } + } + } + + @Override + public void close() throws IOException { + _jar.close(); + } + } +}