tikhomirov@51: /* tikhomirov@576: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@157: * tikhomirov@157: * This program is free software; you can redistribute it and/or modify tikhomirov@157: * it under the terms of the GNU General Public License as published by tikhomirov@157: * the Free Software Foundation; version 2 of the License. tikhomirov@157: * tikhomirov@157: * This program is distributed in the hope that it will be useful, tikhomirov@157: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@157: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@157: * GNU General Public License for more details. tikhomirov@157: * tikhomirov@157: * For information on how to redistribute this software under tikhomirov@157: * the terms of a license other than GNU General Public License tikhomirov@157: * contact TMate Software at support@hg4j.com tikhomirov@51: */ tikhomirov@157: package org.tmatesoft.hg.internal; tikhomirov@51: tikhomirov@584: import java.io.EOFException; tikhomirov@51: import java.io.IOException; tikhomirov@576: import java.nio.ByteBuffer; tikhomirov@51: import java.util.zip.DataFormatException; tikhomirov@51: import java.util.zip.Inflater; tikhomirov@51: import java.util.zip.ZipException; tikhomirov@51: tikhomirov@51: /** tikhomirov@51: * DataAccess counterpart for InflaterInputStream. tikhomirov@51: * XXX is it really needed to be subclass of FilterDataAccess? tikhomirov@157: * tikhomirov@157: * @author Artem Tikhomirov tikhomirov@157: * @author TMate Software Ltd. tikhomirov@51: */ tikhomirov@51: public class InflaterDataAccess extends FilterDataAccess { tikhomirov@51: tikhomirov@51: private final Inflater inflater; tikhomirov@576: private final byte[] inBuffer; tikhomirov@576: private final ByteBuffer outBuffer; tikhomirov@576: private int inflaterPos = 0; tikhomirov@158: private int decompressedLength; tikhomirov@51: tikhomirov@420: public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength) { tikhomirov@263: this(dataAccess, offset, compressedLength, -1, new Inflater(), new byte[512]); tikhomirov@51: } tikhomirov@51: tikhomirov@420: public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength) { tikhomirov@263: this(dataAccess, offset, compressedLength, actualLength, new Inflater(), new byte[512]); tikhomirov@158: } tikhomirov@158: tikhomirov@420: public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength, Inflater inflater, byte[] buf) { tikhomirov@158: super(dataAccess, offset, compressedLength); tikhomirov@263: if (inflater == null || buf == null) { tikhomirov@263: throw new IllegalArgumentException(); tikhomirov@263: } tikhomirov@51: this.inflater = inflater; tikhomirov@158: this.decompressedLength = actualLength; tikhomirov@576: inBuffer = buf; tikhomirov@576: outBuffer = ByteBuffer.allocate(inBuffer.length * 2); tikhomirov@576: outBuffer.limit(0); // there's nothing to read in the buffer tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@157: public InflaterDataAccess reset() throws IOException { tikhomirov@51: super.reset(); tikhomirov@51: inflater.reset(); tikhomirov@576: inflaterPos = 0; tikhomirov@576: outBuffer.clear().limit(0); // or flip(), to indicate nothing to read tikhomirov@157: return this; tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@421: protected int available() throws IOException { tikhomirov@576: return length() - decompressedPosition(); tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@421: public boolean isEmpty() throws IOException { tikhomirov@158: // can't use super.available() <= 0 because even when 0 < super.count < 6(?) tikhomirov@158: // decompressedPos might be already == length() tikhomirov@158: return available() <= 0; tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@421: public int length() throws IOException { tikhomirov@51: if (decompressedLength != -1) { tikhomirov@51: return decompressedLength; tikhomirov@51: } tikhomirov@576: decompressedLength = 0; // guard to avoid endless loop in case length() would get invoked from below. tikhomirov@576: final int oldPos = decompressedPosition(); tikhomirov@576: final int inflatedUpTo = inflaterPos; tikhomirov@576: int inflatedMore = 0, c; tikhomirov@576: do { tikhomirov@576: outBuffer.limit(outBuffer.position()); // pretend the buffer is consumed tikhomirov@576: c = fillOutBuffer(); tikhomirov@576: inflatedMore += c; tikhomirov@576: } while (c == outBuffer.capacity()); // once we unpacked less than capacity, input is over tikhomirov@576: decompressedLength = inflatedUpTo + inflatedMore; tikhomirov@421: reset(); tikhomirov@421: seek(oldPos); tikhomirov@421: return decompressedLength; tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@158: public void seek(int localOffset) throws IOException { tikhomirov@51: if (localOffset < 0 /* || localOffset >= length() */) { tikhomirov@51: throw new IllegalArgumentException(); tikhomirov@51: } tikhomirov@576: int currentPos = decompressedPosition(); tikhomirov@576: if (localOffset >= currentPos) { tikhomirov@576: skip(localOffset - currentPos); tikhomirov@51: } else { tikhomirov@51: reset(); tikhomirov@420: skip(localOffset); tikhomirov@51: } tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@391: public void skip(final int bytesToSkip) throws IOException { tikhomirov@391: int bytes = bytesToSkip; tikhomirov@51: if (bytes < 0) { tikhomirov@576: bytes += decompressedPosition(); tikhomirov@51: if (bytes < 0) { tikhomirov@576: 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)); tikhomirov@51: } tikhomirov@51: reset(); tikhomirov@51: // fall-through tikhomirov@51: } tikhomirov@51: while (!isEmpty() && bytes > 0) { tikhomirov@576: int fromBuffer = outBuffer.remaining(); tikhomirov@576: if (fromBuffer > 0) { tikhomirov@576: if (fromBuffer >= bytes) { tikhomirov@576: outBuffer.position(outBuffer.position() + bytes); tikhomirov@576: bytes = 0; tikhomirov@576: break; tikhomirov@576: } else { tikhomirov@576: bytes -= fromBuffer; tikhomirov@576: outBuffer.limit(outBuffer.position()); // mark consumed tikhomirov@576: // fall through to fill the buffer tikhomirov@576: } tikhomirov@576: } tikhomirov@576: fillOutBuffer(); tikhomirov@51: } tikhomirov@51: if (bytes != 0) { tikhomirov@576: 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)); tikhomirov@51: } tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@51: public byte readByte() throws IOException { tikhomirov@576: if (!outBuffer.hasRemaining()) { tikhomirov@576: fillOutBuffer(); tikhomirov@576: } tikhomirov@576: return outBuffer.get(); tikhomirov@51: } tikhomirov@51: tikhomirov@51: @Override tikhomirov@51: public void readBytes(byte[] b, int off, int len) throws IOException { tikhomirov@584: int fromBuffer; tikhomirov@576: do { tikhomirov@584: fromBuffer = outBuffer.remaining(); tikhomirov@576: if (fromBuffer > 0) { tikhomirov@576: if (fromBuffer >= len) { tikhomirov@576: outBuffer.get(b, off, len); tikhomirov@576: return; tikhomirov@576: } else { tikhomirov@576: outBuffer.get(b, off, fromBuffer); tikhomirov@576: off += fromBuffer; tikhomirov@576: len -= fromBuffer; tikhomirov@576: // fall-through tikhomirov@576: } tikhomirov@576: } tikhomirov@584: fromBuffer = fillOutBuffer(); tikhomirov@584: } while (len > 0 && fromBuffer > 0); tikhomirov@584: if (len > 0) { tikhomirov@584: // prevent hang up in this cycle if no more data is available, see Issue 25 tikhomirov@584: 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())); tikhomirov@584: } tikhomirov@576: } tikhomirov@576: tikhomirov@576: @Override tikhomirov@576: public void readBytes(ByteBuffer buf) throws IOException { tikhomirov@576: int len = Math.min(available(), buf.remaining()); tikhomirov@576: while (len > 0) { tikhomirov@576: if (outBuffer.remaining() >= len) { tikhomirov@576: ByteBuffer slice = outBuffer.slice(); tikhomirov@576: slice.limit(len); tikhomirov@576: buf.put(slice); tikhomirov@576: outBuffer.position(outBuffer.position() + len); tikhomirov@576: return; tikhomirov@576: } else { tikhomirov@576: len -= outBuffer.remaining(); tikhomirov@576: buf.put(outBuffer); tikhomirov@576: } tikhomirov@576: fillOutBuffer(); tikhomirov@576: } tikhomirov@576: } tikhomirov@576: tikhomirov@576: private int decompressedPosition() { tikhomirov@576: assert outBuffer.remaining() <= inflaterPos; tikhomirov@576: return inflaterPos - outBuffer.remaining(); tikhomirov@576: } tikhomirov@576: tikhomirov@576: // after #fillOutBuffer(), outBuffer is ready for read tikhomirov@576: private int fillOutBuffer() throws IOException { tikhomirov@576: assert !outBuffer.hasRemaining(); tikhomirov@51: try { tikhomirov@576: int inflatedBytes = 0; tikhomirov@576: outBuffer.clear(); tikhomirov@576: int len = outBuffer.capacity(); tikhomirov@576: int off = 0; tikhomirov@576: do { tikhomirov@576: int n; tikhomirov@576: while ((n = inflater.inflate(outBuffer.array(), off, len)) == 0) { tikhomirov@399: // XXX few last bytes (checksum?) may be ignored by inflater, thus inflate may return 0 in tikhomirov@157: // perfectly legal conditions (when all data already expanded, but there are still some bytes tikhomirov@399: // in the input stream) tikhomirov@399: int toRead = -1; tikhomirov@399: if (inflater.needsInput() && (toRead = super.available()) > 0) { tikhomirov@576: // fill tikhomirov@576: if (toRead > inBuffer.length) { tikhomirov@576: toRead = inBuffer.length; tikhomirov@51: } tikhomirov@576: super.readBytes(inBuffer, 0, toRead); tikhomirov@576: inflater.setInput(inBuffer, 0, toRead); tikhomirov@399: } else { tikhomirov@576: // inflated nothing and doesn't want any more data (or no date available) - assume we're done tikhomirov@576: assert inflater.finished(); tikhomirov@576: assert toRead <= 0; tikhomirov@576: break; tikhomirov@51: } tikhomirov@51: } tikhomirov@51: off += n; tikhomirov@51: len -= n; tikhomirov@576: inflatedBytes += n; tikhomirov@576: } while (len > 0 && !inflater.finished()); // either the buffer is filled or nothing more to unpack tikhomirov@576: inflaterPos += inflatedBytes; tikhomirov@576: outBuffer.limit(inflatedBytes); tikhomirov@576: assert outBuffer.position() == 0; // didn't change since #clear() above tikhomirov@576: return inflatedBytes; tikhomirov@51: } catch (DataFormatException e) { tikhomirov@51: String s = e.getMessage(); tikhomirov@51: throw new ZipException(s != null ? s : "Invalid ZLIB data format"); tikhomirov@51: } tikhomirov@51: } tikhomirov@51: }