/* * * Copyright (c) 2001 Torgeir Veimo * Copyright (c) 2002 Nicolas PERIDONT * Bug Fixes: Daniel Morrione dan@morrione.net * Copyright (c) 2006 Heiko Klein * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * * Change Log: * iiimmddyyn nnnnn Description * ---------- ----- ------------------------------------------------------- * gls100603a Fixes from Torgeir Veimo and Dan Morrione * gls110603a Stream not being closed thus preventing a file from * being subsequently deleted. * gls031504a Error being written to stderr rather than throwing exception */ package cpw.mods.fml.repackage.com.nothome.delta; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; /** * Class for computing deltas against a source. * The source file is read by blocks and a hash is computed per block. * Then the target is scanned for matching blocks. * <p/> * This class is not thread safe. Use one instance per thread. * <p/> * This class should support files over 4GB in length, although you must * use a larger checksum size, such as 1K, as all checksums use "int" indexing. * Newer versions may eventually support paging in/out of checksums. */ public class Delta { /** * Debug flag. */ final static boolean debug = false; /** * Default size of 16. * For "Lorem ipsum" text files (see the tests) the ideal size is about 14. * Any smaller and the patch size becomes actually be larger. * <p> * Use a size like 64 or 128 for large files. */ public static final int DEFAULT_CHUNK_SIZE = 1<<4; /** * Chunk Size. */ private int S; private SourceState source; private TargetState target; private DiffWriter output; /** * Constructs a new Delta. * In the future, additional constructor arguments will set the algorithm details. */ public Delta() { setChunkSize(DEFAULT_CHUNK_SIZE); } /** * Sets the chunk size used. * Larger chunks are faster and use less memory, but create larger patches * as well. * * @param size */ public void setChunkSize(int size) { if (size <= 0) throw new IllegalArgumentException("Invalid size"); S = size; } /** * Compares the source bytes with target bytes, writing to output. */ public void compute(byte source[], byte target[], OutputStream output) throws IOException { compute(new ByteBufferSeekableSource(source), new ByteArrayInputStream(target), new GDiffWriter(output)); } /** * Compares the source bytes with target bytes, returning output. */ public byte[] compute(byte source[], byte target[]) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(); compute(source, target, os); return os.toByteArray(); } /** * Compares the source bytes with target input, writing to output. */ public void compute(byte[] sourceBytes, InputStream inputStream, DiffWriter diffWriter) throws IOException { compute(new ByteBufferSeekableSource(sourceBytes), inputStream, diffWriter); } /** * Compares the source file with a target file, writing to output. * * @param output will be closed */ public void compute(File sourceFile, File targetFile, DiffWriter output) throws IOException { RandomAccessFileSeekableSource source = new RandomAccessFileSeekableSource(new RandomAccessFile(sourceFile, "r")); InputStream is = new BufferedInputStream(new FileInputStream(targetFile)); try { compute(source, is, output); } finally { source.close(); is.close(); } } /** * Compares the source with a target, writing to output. * * @param output will be closed */ public void compute(SeekableSource seekSource, InputStream targetIS, DiffWriter output) throws IOException { if (debug) { debug("using match length S = " + S); } source = new SourceState(seekSource); target = new TargetState(targetIS); this.output = output; if (debug) debug("checksums " + source.checksum); while (!target.eof()) { debug("!target.eof()"); int index = target.find(source); if (index != -1) { if (debug) debug("found hash " + index); long offset = (long)index * S; source.seek(offset); int match = target.longestMatch(source); if (match >= S) { if (debug) debug("output.addCopy("+offset+","+match+")"); output.addCopy(offset, match); } else { // move the position back according to how much we can't copy target.tbuf.position(target.tbuf.position() - match); addData(); } } else { addData(); } } output.close(); } private void addData() throws IOException { int i = target.read(); if (debug) debug("addData " + Integer.toHexString(i)); if (i == -1) return; output.addData((byte)i); } class SourceState { private Checksum checksum; private SeekableSource source; public SourceState(SeekableSource source) throws IOException { checksum = new Checksum(source, S); this.source = source; source.seek(0); } public void seek(long index) throws IOException { source.seek(index); } /** * Returns a debug <code>String</code>. */ @Override public String toString() { return "Source"+ " checksum=" + this.checksum + " source=" + this.source + ""; } } class TargetState { private ReadableByteChannel c; private ByteBuffer tbuf = ByteBuffer.allocate(blocksize()); private ByteBuffer sbuf = ByteBuffer.allocate(blocksize()); private long hash; private boolean hashReset = true; private boolean eof; TargetState(InputStream targetIS) throws IOException { c = Channels.newChannel(targetIS); tbuf.limit(0); } private int blocksize() { return Math.min(1024 * 16, S * 4); } /** * Returns the index of the next N bytes of the stream. */ public int find(SourceState source) throws IOException { if (eof) return -1; sbuf.clear(); sbuf.limit(0); if (hashReset) { debug("hashReset"); while (tbuf.remaining() < S) { tbuf.compact(); int read = c.read(tbuf); tbuf.flip(); if (read == -1) { debug("target ending"); return -1; } } hash = Checksum.queryChecksum(tbuf, S); hashReset = false; } if (debug) debug("hash " + hash + " " + dump()); return source.checksum.findChecksumIndex(hash); } public boolean eof() { return eof; } /** * Reads a byte. * @throws IOException */ public int read() throws IOException { if (tbuf.remaining() <= S) { readMore(); if (!tbuf.hasRemaining()) { eof = true; return -1; } } byte b = tbuf.get(); if (tbuf.remaining() >= S) { byte nchar = tbuf.get( tbuf.position() + S -1 ); hash = Checksum.incrementChecksum(hash, b, nchar, S); } else { debug("out of char"); } return b & 0xFF; } /** * Returns the longest match length at the source location. */ public int longestMatch(SourceState source) throws IOException { debug("longestMatch"); int match = 0; hashReset = true; while (true) { if (!sbuf.hasRemaining()) { sbuf.clear(); int read = source.source.read(sbuf); sbuf.flip(); if (read == -1) return match; } if (!tbuf.hasRemaining()) { readMore(); if (!tbuf.hasRemaining()) { debug("target ending"); eof = true; return match; } } if (sbuf.get() != tbuf.get()) { tbuf.position(tbuf.position() - 1); return match; } match++; } } private void readMore() throws IOException { if (debug) debug("readMore " + tbuf); tbuf.compact(); c.read(tbuf); tbuf.flip(); } void hash() { hash = Checksum.queryChecksum(tbuf, S); } /** * Returns a debug <code>String</code>. */ @Override public String toString() { return "Target[" + " targetBuff=" + dump() + // this.tbuf + " sourceBuff=" + this.sbuf + " hashf=" + this.hash + " eof=" + this.eof + "]"; } private String dump() { return dump(tbuf); } private String dump(ByteBuffer bb) { return getTextDump(bb); } private void append(StringBuffer sb, int value) { char b1 = (char)((value >> 4) & 0x0F); char b2 = (char)((value) & 0x0F); sb.append( Character.forDigit(b1, 16) ); sb.append( Character.forDigit(b2, 16) ); } public String getTextDump(ByteBuffer bb) { StringBuffer sb = new StringBuffer(bb.remaining() * 2); bb.mark(); while (bb.hasRemaining()) { int val = bb.get(); if (val > 32 && val < 127) sb.append(" ").append((char)val); else append(sb, val); } bb.reset(); return sb.toString(); } } /** * Creates a patch using file names. */ public static void main(String argv[]) throws Exception { if (argv.length != 3) { System.err.println("usage Delta [-d] source target [output]"); System.err.println("either -d or an output filename must be specified."); System.err.println("aborting.."); return; } DiffWriter output = null; File sourceFile = null; File targetFile = null; if (argv[0].equals("-d")) { sourceFile = new File(argv[1]); targetFile = new File(argv[2]); output = new DebugDiffWriter(); } else { sourceFile = new File(argv[0]); targetFile = new File(argv[1]); output = new GDiffWriter( new DataOutputStream( new BufferedOutputStream( new FileOutputStream(new File(argv[2]))))); } if (sourceFile.length() > Integer.MAX_VALUE || targetFile.length() > Integer.MAX_VALUE) { System.err.println( "source or target is too large, max length is " + Integer.MAX_VALUE); System.err.println("aborting.."); output.close(); return; } Delta d = new Delta(); d.compute(sourceFile, targetFile, output); output.flush(); output.close(); if (debug) //gls031504a System.out.println("finished generating delta"); } private void debug(String s) { if (debug) System.err.println(s); } }