/* * Copyright (c) 1996, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util.zip; import java.io.SequenceInputStream; import java.io.ByteArrayInputStream; import java.io.FilterInputStream; import java.io.InputStream; import java.io.IOException; import java.io.EOFException; import java.util.Objects; /** * This class implements a stream filter for decompressing GZIP file format data. * *
* This class is capable of reading a stream consisting of a series of members. *
* Reading from the stream may read and buffer bytes from the underlying stream. * This includes bytes that follow a member's trailer. Whether or not any additional bytes * have been read past a member's trailer, the read methods on this class yield decompressed * data from at most one member; data from multiple members is not combined in * a single read operation. * *
* If this method returns a nonzero integer n then {@code buf[off]} * through {@code buf[off+}n{@code -1]} contain the decompressed * data. The content of elements {@code buf[off+}n{@code ]} through * {@code buf[off+}len{@code -1]} is undefined, contrary to the * specification of the {@link java.io.InputStream InputStream} superclass, * so an implementation is free to modify these elements during the inflate * operation. If this method returns {@code -1} or throws an exception then * the content of {@code buf[off]} through {@code buf[off+}len{@code * -1]} is undefined. * * @param buf the buffer into which the data is read * @param off the start offset in the destination array {@code buf} * @param len the maximum number of bytes to read into {@code buf} * @return the actual number of bytes decompressed from a GZIP member, or -1 if the * end-of-stream is reached * * @throws NullPointerException If {@code buf} is {@code null}. * @throws IndexOutOfBoundsException If {@code off} is negative, * {@code len} is negative, or {@code len} is greater than * {@code buf.length - off} * @throws ZipException if the compressed input data is corrupt. * @throws IOException if the stream is closed or an I/O error has occurred. * * @see ##gzip_file_format GZIP file format */ @Override public int read(byte[] buf, int off, int len) throws IOException { ensureOpen(); if (eos) { return -1; } int n = super.read(buf, off, len); if (n == -1) { if (hasNoMoreMembers()) { eos = true; } else { // When a next member is available, hasNoMoreMembers() will read // its header and will position the stream at the next member's // deflated data. We now decompress and return that member's // decompressed data. return this.read(buf, off, len); } } else { crc.update(buf, off, n); } return n; } /** * Closes this input stream and releases any system resources associated * with the stream. * @throws IOException if an I/O error has occurred */ @Override public void close() throws IOException { if (!closed) { super.close(); eos = true; closed = true; } } /* * Reads GZIP member header and returns the total byte number * of this member header. * If failOnEOF is false and if the given InputStream has already * reached EOF when this method was invoked, then this method returns * -1 (indicating that there's no GZIP member header). * In all other cases of malformed header or EOF being detected * when reading the header, this method will throw an IOException. */ private int readHeader(InputStream stream, boolean failOnEOF) throws IOException { CheckedInputStream in = new CheckedInputStream(stream, crc); crc.reset(); int magic; if (!failOnEOF) { // read an unsigned short value representing the GZIP magic header. // this is the same as calling readUShort(in), except that here, // when reading the first byte, we don't raise an EOFException // if the stream has already reached EOF. // read unsigned byte int b = in.read(); if (b == -1) { // EOF crc.reset(); return -1; // represents no header bytes available } checkUnexpectedByte(b); // read the next unsigned byte to form the unsigned // short. we throw the usual EOFException/ZipException // from this point on if there is no more data or // the data doesn't represent a header. magic = (readUByte(in) << 8) | b; } else { magic = readUShort(in); } // Check header magic if (magic != GZIP_MAGIC) { throw new ZipException("Not in GZIP format"); } // Check compression method if (readUByte(in) != 8) { throw new ZipException("Unsupported compression method"); } // Read flags int flg = readUByte(in); // Skip MTIME, XFL, and OS fields skipBytes(in, 6); int n = 2 + 2 + 6; // Skip optional extra field if ((flg & FEXTRA) == FEXTRA) { int m = readUShort(in); skipBytes(in, m); n += m + 2; } // Skip optional file name if ((flg & FNAME) == FNAME) { do { n++; } while (readUByte(in) != 0); } // Skip optional file comment if ((flg & FCOMMENT) == FCOMMENT) { do { n++; } while (readUByte(in) != 0); } // Check optional header CRC if ((flg & FHCRC) == FHCRC) { int v = (int)crc.getValue() & 0xffff; if (readUShort(in) != v) { throw new ZipException("Corrupt GZIP header"); } n += 2; } crc.reset(); return n; } /** * Reads the current GZIP member's trailer and returns true if the end-of-stream is * reached. After reading the current member's trailer, if the stream has a subsequent * GZIP member, then this method reads that member's header and returns false indicating * that there is another member in the stream. */ private boolean hasNoMoreMembers() throws IOException { final int numRemainingInInflater = inf.getRemaining(); InputStream stream = this.in; if (numRemainingInInflater > 0) { stream = new SequenceInputStream( new ByteArrayInputStream(buf, len - numRemainingInInflater, numRemainingInInflater), new FilterInputStream(stream) { public void close() {} }); } // first read the current member's trailer readTrailer(stream); // decide whether to read next member's header final boolean readNextMember = alwaysReadNextMember || this.in.available() > 0 || numRemainingInInflater > 26; // current member's trailer == 8 bytes // + minimum of 10 bytes header for next member // + mandatory 8 bytes from next member's trailer // == at least 26 bytes needed for next member to // be present if (!readNextMember) { return true; // no need to read next member } // read next member's header int m = 8; // this.trailer try { int numNextHeaderBytes = readHeader(stream, false); // next.header (if available) if (numNextHeaderBytes == -1) { return true; // end of stream reached, no more members } m += numNextHeaderBytes; } catch (IOException ze) { return true; // ignore any malformed, consider it as no more members in the stream } inf.reset(); // reset the inflater for fresh input data from the next member if (numRemainingInInflater > m) { // position the inflater's input buffer to the start of next member's deflated data inf.setInput(buf, len - numRemainingInInflater + m, numRemainingInInflater - m); } return false; // next member exists } /** * Reads the current member's trailer * * @param stream the InputStream containing the trailer */ private void readTrailer(final InputStream stream) throws IOException { // Uses left-to-right evaluation order if ((readUInt(stream) != crc.getValue()) || // rfc1952; ISIZE is the input size modulo 2^32 (readUInt(stream) != (inf.getBytesWritten() & 0xffffffffL))) { throw new ZipException("Corrupt GZIP trailer"); } } /* * Reads unsigned integer in Intel byte order. */ private long readUInt(InputStream in) throws IOException { long s = readUShort(in); return ((long)readUShort(in) << 16) | s; } /* * Reads unsigned short in Intel byte order. */ private int readUShort(InputStream in) throws IOException { int b = readUByte(in); return (readUByte(in) << 8) | b; } /* * Reads unsigned byte. */ private int readUByte(InputStream in) throws IOException { int b = in.read(); if (b == -1) { throw new EOFException(); } checkUnexpectedByte(b); return b; } private void checkUnexpectedByte(final int b) throws IOException { if (b < -1 || b > 255) { // report the InputStream type which returned this unexpected byte throw new IOException(this.in.getClass().getName() + ".read() returned value out of range -1..255: " + b); } } /* * Skips bytes of input data blocking until all bytes are skipped. * Does not assume that the input stream is capable of seeking. */ private void skipBytes(InputStream in, int n) throws IOException { while (n > 0) { int len = in.read(tmpbuf, 0, n < tmpbuf.length ? n : tmpbuf.length); if (len == -1) { throw new EOFException(); } n -= len; } } }