/*
* Copyright (c) 1998-2010 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source 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; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source 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, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Nam Nguyen
*/
package com.caucho.quercus.lib.zlib;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
/**
* Similar to GZIPInputStream but with ability to read appended gzip.
*/
public class GZInputStream extends InputStream {
private PushbackInputStream _in;
private Inflater _inflater;
private CRC32 _crc;
private boolean _eof;
private boolean _isGzip;
private byte[] _readBuffer; //raw input data buffer
private byte[] _tbuffer; //temporary buffer
private int _readBufferSize; //amount of raw data read into _readBuffer
private int _inputSize; //decompressed bytes read so far
// for the current 'append' stream
private long _totalInputSize; //total decompressed bytes read
public GZInputStream(InputStream in)
throws IOException {
this(in, 512);
}
public GZInputStream(InputStream in, int size)
throws IOException {
// Need to use same buffer size for pushback and _readBuffer
// because will need to unread <= _readBuffer.length.
_in = new PushbackInputStream(in, size);
_inflater = new Inflater(true);
_crc = new CRC32();
_eof = false;
_readBuffer = new byte[size];
_tbuffer = new byte[128];
_totalInputSize = 0;
init();
}
/**
* Returns 0 if gzip EOF has been reached, 1 otherwise
*/
@Override
public int available()
throws IOException {
if (!_isGzip) {
return _in.available();
}
if (_eof == true) {
return 0;
}
return 1;
}
@Override
public void close()
throws IOException {
_inflater.end();
}
/**
* mark() and reset() are not supported by this class.
* @return false always
*/
@Override
public boolean markSupported() {
return false;
}
/**
* Returns the byte read, -1 if EOF
* @return number of bytes read, or -1 if EOF
*/
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int n = read(b);
if (n < 0) {
return -1;
}
return b[0];
}
/**
* Reads from the compressed stream and
* stores the resulting uncompressed data into the byte array.
* @return number of bytes read, or -1 upon EOF
*/
@Override
public int read(byte[] b)
throws IOException {
return read(b, 0, b.length);
}
/**
* Reads from the compressed stream and
* stores the resulting uncompressed data into the byte array.
* @return number of bytes read, or -1 upon EOF
*/
@Override
public int read(byte[] b, int off, int len)
throws IOException {
if (len <= 0 || off < 0 || off + len > b.length) {
return 0;
}
if (_eof) {
return -1;
}
// Read from uncompressed stream
if (!_isGzip) {
return _in.read(b, off, len);
}
try {
int sublen;
int length = 0;
while (length < len) {
if (_inflater.needsInput()) {
_readBufferSize = _in.read(_readBuffer, 0, _readBuffer.length);
if (_readBufferSize < 0) {
break;
}
_inflater.setInput(_readBuffer, 0, _readBufferSize);
}
sublen = _inflater.inflate(b, off + length, len - length);
_crc.update(b, off + length, sublen);
_inputSize += sublen;
_totalInputSize += sublen;
length += sublen;
// Unread gzip trailer and possibly beginning of appended gzip data.
if (_inflater.finished()) {
int remaining = _inflater.getRemaining();
_in.unread(_readBuffer, _readBufferSize - remaining, remaining);
readTrailer();
int secondPart = read(b, off + length, len - length);
return secondPart > 0 ? length + secondPart : length;
}
}
return length;
} catch (DataFormatException e) {
throw new IOException(e.getMessage());
}
}
/**
* Skips over and discards n bytes.
* @param n number of bytes to skip
* @return actual number of bytes skipped
*/
@Override
public long skip(long n)
throws IOException {
if (_eof || n <= 0) {
return 0;
}
long remaining = n;
while (remaining > 0) {
int length = (int) Math.min(_tbuffer.length, remaining);
int sublen = read(_tbuffer, 0, length);
if (sublen < 0) {
break;
}
remaining -= sublen;
}
return (n - remaining);
}
/**
* Inits/resets this class to be ready to read the start of a gzip stream.
*/
private void init()
throws IOException {
_inflater.reset();
_crc.reset();
_inputSize = 0;
_readBufferSize = 0;
byte flg;
int length = _in.read(_tbuffer, 0, 10);
if (length < 0) {
_isGzip = false;
return;
} else if (length != 10) {
_isGzip = false;
_in.unread(_tbuffer, 0, length);
return;
}
if (_tbuffer[0] != (byte) 0x1f || _tbuffer[1] != (byte) 0x8b) {
_isGzip = false;
_in.unread(_tbuffer, 0, length);
return;
}
flg = _tbuffer[3];
// Skip optional field
if ((flg & (byte) 0x04) > 0) {
length = _in.read(_tbuffer, 0, 2);
if (length != 2) {
throw new IOException("Bad GZIP (FEXTRA) header.");
}
length = (((int) _tbuffer[1]) << 4) | _tbuffer[0];
_in.skip(length);
}
int c;
// Skip optional field
if ((flg & (byte) 0x08) > 0) {
c = _in.read();
while (c != 0) {
if (c < 0) {
throw new IOException("Bad GZIP (FNAME) header.");
}
c = _in.read();
}
}
// Skip optional field
if ((flg & 0x10) > 0) {
c = _in.read();
while (c != 0) {
if (c < 0) {
throw new IOException("Bad GZIP (FCOMMENT) header.");
}
c = _in.read();
}
}
// Skip optional field
if ((flg & 0x02) > 0) {
length = _in.read(_tbuffer, 0, 2);
if (length != 2) {
throw new IOException("Bad GZIP (FHCRC) header.");
}
}
_isGzip = true;
}
/**
* Reads the trailer and prepare this class for the possibility
* of an appended gzip stream.
*/
private void readTrailer()
throws IOException {
int length = _in.read(_tbuffer, 0, 8);
if (length != 8) {
throw new IOException("Bad GZIP trailer.");
}
int refValue = _tbuffer[3] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[2] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[1] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[0] & 0xff;
int value = (int) _crc.getValue();
if (refValue != value) {
throw new IOException("Bad GZIP trailer (CRC32).");
}
refValue = _tbuffer[7] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[6] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[5] & 0xff;
refValue <<= 8;
refValue |= _tbuffer[4] & 0xff;
if (refValue != _inputSize) {
throw new IOException("Bad GZIP trailer (LENGTH).");
}
// Check to see if this gzip stream is appended with a valid gzip stream.
// If it is appended, then can continue reading from stream.
int c = _in.read();
if (c < 0) {
_eof = true;
} else {
_in.unread(c);
init();
if (!_isGzip) {
_eof = true;
}
}
}
/*
* Returns true if stream is in gzip format.
*/
public boolean isGzip() {
return _isGzip;
}
}