// // 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(); } } }