view src/org/tmatesoft/hg/internal/InflaterDataAccess.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents bcbcc318f250
children
line wrap: on
line source
/*
 * Copyright (c) 2011-2013 TMate Software Ltd
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@hg4j.com
 */
package org.tmatesoft.hg.internal;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.ZipException;

/**
 * DataAccess counterpart for InflaterInputStream.
 * XXX is it really needed to be subclass of FilterDataAccess? 
 *   
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public class InflaterDataAccess extends FilterDataAccess {

	private final Inflater inflater;
	private final byte[] inBuffer;
	private final ByteBuffer outBuffer;
	private int inflaterPos = 0;
	private int decompressedLength;

	public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength) {
		this(dataAccess, offset, compressedLength, -1, new Inflater(), new byte[512], null);
	}

	public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength) {
		this(dataAccess, offset, compressedLength, actualLength, new Inflater(), new byte[512], null);
	}

	public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength, Inflater inflater, byte[] inBuf, ByteBuffer outBuf) {
		super(dataAccess, offset, compressedLength);
		if (inflater == null || inBuf == null) {
			throw new IllegalArgumentException();
		}
		this.inflater = inflater;
		this.decompressedLength = actualLength;
		inBuffer = inBuf;
		outBuffer = outBuf == null ? ByteBuffer.allocate(inBuffer.length * 2) : outBuf;
		outBuffer.limit(0); // there's nothing to read in the buffer 
	}
	
	@Override
	public InflaterDataAccess reset() throws IOException {
		super.reset();
		inflater.reset();
		inflaterPos = 0;
		outBuffer.clear().limit(0); // or flip(), to indicate nothing to read
		return this;
	}
	
	@Override
	protected int available() throws IOException {
		return length() - decompressedPosition();
	}
	
	@Override
	public boolean isEmpty() throws IOException {
		// can't use super.available() <= 0 because even when 0 < super.count < 6(?)
		// decompressedPos might be already == length() 
		return available() <= 0;
	}
	
	@Override
	public int length() throws IOException {
		if (decompressedLength != -1) {
			return decompressedLength;
		}
		decompressedLength = 0; // guard to avoid endless loop in case length() would get invoked from below.
		final int oldPos = decompressedPosition();
		final int inflatedUpTo = inflaterPos;
		int inflatedMore = 0, c;
		do {
			outBuffer.limit(outBuffer.position()); // pretend the buffer is consumed
			c = fillOutBuffer();
			inflatedMore += c;
		} while (c == outBuffer.capacity()); // once we unpacked less than capacity, input is over
		decompressedLength = inflatedUpTo + inflatedMore;
		reset();
		seek(oldPos);
		return decompressedLength;
	}
	
	@Override
	public void seek(int localOffset) throws IOException {
		if (localOffset < 0 /* || localOffset >= length() */) {
			throw new IllegalArgumentException();
		}
		int currentPos = decompressedPosition();
		if (localOffset >= currentPos) {
			skip(localOffset - currentPos);
		} else {
			reset();
			skip(localOffset);
		}
	}
	
	@Override
	public void skip(final int bytesToSkip) throws IOException {
		int bytes = bytesToSkip;
		if (bytes < 0) {
			bytes += decompressedPosition();
			if (bytes < 0) {
				throw new IOException(String.format("Underflow. Rewind past start of the slice. To skip:%d, decPos:%d, decLen:%d. Left:%d", bytesToSkip, inflaterPos, decompressedLength, bytes));
			}
			reset();
			// fall-through
		}
		while (!isEmpty() && bytes > 0) {
			int fromBuffer = outBuffer.remaining();
			if (fromBuffer > 0) {
				if (fromBuffer >= bytes) {
					outBuffer.position(outBuffer.position() + bytes);
					bytes = 0;
					break;
				} else {
					bytes -= fromBuffer;
					outBuffer.limit(outBuffer.position()); // mark consumed
					// fall through to fill the buffer
				}
			}
			fillOutBuffer();
		}
		if (bytes != 0) {
			throw new IOException(String.format("Underflow. Rewind past end of the slice. To skip:%d, decPos:%d, decLen:%d. Left:%d", bytesToSkip, inflaterPos, decompressedLength, bytes));
		}
	}

	@Override
	public byte readByte() throws IOException {
		if (!outBuffer.hasRemaining()) {
			fillOutBuffer();
		}
		return outBuffer.get();
	}

	@Override
	public void readBytes(byte[] b, int off, int len) throws IOException {
		int fromBuffer;
		do {
			fromBuffer = outBuffer.remaining();
			if (fromBuffer > 0) {
				if (fromBuffer >= len) {
					outBuffer.get(b, off, len);
					return;
				} else {
					outBuffer.get(b, off, fromBuffer);
					off += fromBuffer;
					len -= fromBuffer;
					// fall-through
				}
			}
			fromBuffer = fillOutBuffer();
		} while (len > 0 && fromBuffer > 0);
		if (len > 0) {
			// prevent hang up in this cycle if no more data is available, see Issue 25
			throw new EOFException(String.format("No more compressed data is available to satisfy request for %d bytes. [finished:%b, needDict:%b, needInp:%b, available:%d", len, inflater.finished(), inflater.needsDictionary(), inflater.needsInput(), super.available()));
		}
	}
	
	@Override
	public void readBytes(ByteBuffer buf) throws IOException {
		int len = Math.min(available(), buf.remaining());
		while (len > 0) {
			if (outBuffer.remaining() >= len) {
				ByteBuffer slice = outBuffer.slice();
				slice.limit(len);
				buf.put(slice);
				outBuffer.position(outBuffer.position() + len);
				return;
			} else { 
				len -= outBuffer.remaining();
				buf.put(outBuffer);
			}
			fillOutBuffer();
		}
	}
	
	private int decompressedPosition() {
		assert outBuffer.remaining() <= inflaterPos; 
		return inflaterPos - outBuffer.remaining();
	}
	
	// after #fillOutBuffer(), outBuffer is ready for read
	private int fillOutBuffer() throws IOException {
		assert !outBuffer.hasRemaining();
		try {
			int inflatedBytes = 0;
		    outBuffer.clear();
		    int len = outBuffer.capacity();
		    int off = 0;
		    do {
			    int n;
			    while ((n = inflater.inflate(outBuffer.array(), off, len)) == 0) {
			    	// XXX few last bytes (checksum?) may be ignored by inflater, thus inflate may return 0 in
			    	// perfectly legal conditions (when all data already expanded, but there are still some bytes
			    	// in the input stream)
					int toRead = -1;
					if (inflater.needsInput() && (toRead = super.available()) > 0) {
						// fill
						if (toRead > inBuffer.length) {
							toRead = inBuffer.length;
						}
						super.readBytes(inBuffer, 0, toRead);
						inflater.setInput(inBuffer, 0, toRead);
					} else {
						// inflated nothing and doesn't want any more data (or no date available) - assume we're done 
						assert inflater.finished();
						assert toRead <= 0;
						break;
					}
			    }
				off += n;
				len -= n;
				inflatedBytes += n;
		    } while (len > 0 && !inflater.finished()); // either the buffer is filled or nothing more to unpack
		    inflaterPos += inflatedBytes;
		    outBuffer.limit(inflatedBytes);
		    assert outBuffer.position() == 0; // didn't change since #clear() above
		    return inflatedBytes;
		} catch (DataFormatException e) {
		    String s = e.getMessage();
		    throw new ZipException(s != null ? s : "Invalid ZLIB data format");
		}
    }
}