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