diff --git a/src/java.base/share/classes/java/util/zip/GZIPInputStream.java b/src/java.base/share/classes/java/util/zip/GZIPInputStream.java index 72fb8036f08..88d08386e8c 100644 --- a/src/java.base/share/classes/java/util/zip/GZIPInputStream.java +++ b/src/java.base/share/classes/java/util/zip/GZIPInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -34,17 +34,55 @@ import java.io.EOFException; import java.util.Objects; /** - * This class implements a stream filter for reading compressed data in - * the GZIP file format. + * 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 uncompressed - * data. The content of elements {@code buf[off+}n{@code ]} through + * 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 @@ -131,18 +173,20 @@ public class GZIPInputStream extends InflaterInputStream { * * @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 read - * @return the actual number of bytes inflated, or -1 if the end of the - * compressed input stream is reached + * @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 an I/O error has occurred. + * @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) { @@ -165,6 +209,7 @@ public class GZIPInputStream extends InflaterInputStream { * with the stream. * @throws IOException if an I/O error has occurred */ + @Override public void close() throws IOException { if (!closed) { super.close(); @@ -173,20 +218,6 @@ public class GZIPInputStream extends InflaterInputStream { } } - /** - * GZIP header magic number. - */ - public static final int GZIP_MAGIC = 0x8b1f; - - /* - * File header flags. - */ - private static final int FTEXT = 1; // Extra text - private static final int FHCRC = 2; // Header CRC - private static final int FEXTRA = 4; // Extra field - private static final int FNAME = 8; // File name - private static final int FCOMMENT = 16; // File comment - /* * Reads GZIP member header and returns the total byte number * of this member header. @@ -309,8 +340,6 @@ public class GZIPInputStream extends InflaterInputStream { return b; } - private byte[] tmpbuf = new byte[128]; - /* * Skips bytes of input data blocking until all bytes are skipped. * Does not assume that the input stream is capable of seeking. diff --git a/test/jdk/java/util/zip/GZIP/BasicGZIPInputStreamTest.java b/test/jdk/java/util/zip/GZIP/BasicGZIPInputStreamTest.java index 1f52d44146e..642b0bafa66 100644 --- a/test/jdk/java/util/zip/GZIP/BasicGZIPInputStreamTest.java +++ b/test/jdk/java/util/zip/GZIP/BasicGZIPInputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 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 @@ -22,15 +22,21 @@ */ import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /* * @test @@ -51,7 +57,7 @@ public class BasicGZIPInputStreamTest { @ParameterizedTest @MethodSource("npeFromConstructors") public void testNPEFromConstructors(final Executable constructor) { - Assertions.assertThrows(NullPointerException.class, constructor, + assertThrows(NullPointerException.class, constructor, "GZIPInputStream constructor did not throw NullPointerException"); } @@ -71,7 +77,7 @@ public class BasicGZIPInputStreamTest { @ParameterizedTest @MethodSource("iaeFromConstructors") public void testIAEFromConstructors(final Executable constructor) { - Assertions.assertThrows(IllegalArgumentException.class, constructor, + assertThrows(IllegalArgumentException.class, constructor, "GZIPInputStream constructor did not throw IllegalArgumentException"); } @@ -89,7 +95,29 @@ public class BasicGZIPInputStreamTest { @ParameterizedTest @MethodSource("ioeFromConstructors") public void testIOEFromConstructors(final Executable constructor) { - Assertions.assertThrows(IOException.class, constructor, + assertThrows(IOException.class, constructor, "GZIPInputStream constructor did not throw IOException"); } + + /* + * Verifies that GZIPInputStream.read() throws IOException when invoked on a closed + * stream + */ + @Test + void testClosedStreamRead() throws Exception { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) { + gzos.write(new byte[] {0x42, 0x42}); // GZIP compress these input bytes + } + final byte[] gzipCompressed = baos.toByteArray(); + // create the GZIPInputStream to test + final GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(gzipCompressed)); + in.close(); + final IOException ioe = assertThrows(IOException.class, () -> in.read(new byte[1], 0, 1)); + final String exMessage = ioe.getMessage(); + if (exMessage == null || !exMessage.contains("Stream closed")) { + // unexpected exception message, propagate the original exception + throw ioe; + } + } } diff --git a/test/jdk/java/util/zip/GZIP/GZIPInputStreamRead.java b/test/jdk/java/util/zip/GZIP/GZIPInputStreamRead.java index 56bd58e1aaf..9cc13b29f34 100644 --- a/test/jdk/java/util/zip/GZIP/GZIPInputStreamRead.java +++ b/test/jdk/java/util/zip/GZIP/GZIPInputStreamRead.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 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 @@ -21,81 +21,201 @@ * questions. */ -/* @test +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test * @bug 4691425 * @summary Test the read and write of GZIPInput/OutputStream, including * concatenated .gz inputstream * @key randomness + * @library /test/lib + * @build jdk.test.lib.RandomFactory + * @run junit ${test.main.class} */ +class GZIPInputStreamRead { -import java.io.*; -import java.util.*; -import java.util.zip.*; + private static final Random random = RandomFactory.getRandom(); -public class GZIPInputStreamRead { - public static void main(String[] args) throws Throwable { - Random rnd = new Random(); - for (int i = 1; i < 100; i++) { - int members = rnd.nextInt(10) + 1; + /* + * Generates GZIP content containing multiple members and then verifies + * that using GZIPInputStream to decompress that content generates the correct + * expected decompressed data. + */ + @Test + void testMultipleMembers() throws Exception { + final int numMembers = random.nextInt(10) + 1; + final ByteArrayOutputStream rawUncompressedBaos = new ByteArrayOutputStream(); + final ByteArrayOutputStream gzipCompressedBaos = new ByteArrayOutputStream(); + // generate GZIP content with multiple members + for (int j = 0; j < numMembers; j++) { + byte[] src = new byte[random.nextInt(8192) + 1]; + random.nextBytes(src); + rawUncompressedBaos.write(src); - ByteArrayOutputStream srcBAOS = new ByteArrayOutputStream(); - ByteArrayOutputStream dstBAOS = new ByteArrayOutputStream(); - for (int j = 0; j < members; j++) { - byte[] src = new byte[rnd.nextInt(8192) + 1]; - rnd.nextBytes(src); - srcBAOS.write(src); - - try (GZIPOutputStream gzos = new GZIPOutputStream(dstBAOS)) { - gzos.write(src); - } - } - byte[] srcBytes = srcBAOS.toByteArray(); - byte[] dstBytes = dstBAOS.toByteArray(); - // try different size of buffer to read the - // GZIPInputStream - /* just for fun when running manually - for (int j = 1; j < 10; j++) { - test(srcBytes, dstBytes, j); - } - */ - for (int j = 0; j < 10; j++) { - int readBufSZ = rnd.nextInt(2048) + 1; - test(srcBytes, - dstBytes, - readBufSZ, - 512); // the defualt buffer size - test(srcBytes, - dstBytes, - readBufSZ, - rnd.nextInt(4096) + 1); + try (GZIPOutputStream gzos = new GZIPOutputStream(gzipCompressedBaos)) { + gzos.write(src); } } + final byte[] uncompressedRawBytes = rawUncompressedBaos.toByteArray(); + final byte[] gzipCompressedBytes = gzipCompressedBaos.toByteArray(); + // decompress using GZIPInputStream and verify the decompressed output. + // use different input buffer size for GZIPInputStream when running the verification. + for (int j = 0; j < 10; j++) { + final int readBufSZ = random.nextInt(2048) + 1; + verifyDecompressed(uncompressedRawBytes, + gzipCompressedBytes, + readBufSZ, + 512); // the default input buffer size + verifyDecompressed(uncompressedRawBytes, + gzipCompressedBytes, + readBufSZ, + random.nextInt(4096) + 1); + } } - private static void test(byte[] src, byte[] dst, - int readBufSize, int gzisBufSize) - throws Throwable - { - try (ByteArrayInputStream bais = new ByteArrayInputStream(dst); - GZIPInputStream gzis = new GZIPInputStream(bais, gzisBufSize)) - { - byte[] result = new byte[src.length + 10]; + /* + * Generates GZIP content containing one member followed by some arbitrary non-member data. + * The test then verifies that using GZIPInputStream to decompress that content generates + * the correct expected decompressed data. + */ + @Test + void testNonMemberAfterTrailer() throws Exception { + final byte[] rawUncompressed = new byte[random.nextInt(1234)]; + random.nextBytes(rawUncompressed); + final ByteArrayOutputStream gzipCompressedPlusExtra = new ByteArrayOutputStream(); + // generate a valid GZIP member + try (GZIPOutputStream gzos = new GZIPOutputStream(gzipCompressedPlusExtra)) { + gzos.write(rawUncompressed); // GZIP compress + } + final int numCompressedBytes = gzipCompressedPlusExtra.size(); + // past the GZIP trailer, write some additional bytes that doesn't represent a GZIP member + final byte[] notGZIPMagic = ByteBuffer.allocate(Integer.BYTES). + putInt(GZIPInputStream.GZIP_MAGIC + 42) + .array(); + gzipCompressedPlusExtra.write(notGZIPMagic); + assertEquals(numCompressedBytes + notGZIPMagic.length, gzipCompressedPlusExtra.size(), + "unexpected number of compressed + extra bytes"); + // now use GZIPInputStream to decompress the compressed plus extra bytes and verify + // that the extra bytes don't cause unexpected decompressed output + final ByteArrayOutputStream decompressedBaos = new ByteArrayOutputStream(); + int n = 0; + try (ByteArrayInputStream bais = new ByteArrayInputStream(gzipCompressedPlusExtra.toByteArray()); + GZIPInputStream gzipIn = new GZIPInputStream(bais)) { + + final byte[] tmpBuf = new byte[42]; + while ((n = gzipIn.read(tmpBuf)) != -1) { + decompressedBaos.write(tmpBuf, 0, n); + } + final byte[] decompressed = decompressedBaos.toByteArray(); + // verify the decompressed content + assertEquals(rawUncompressed.length, decompressed.length, + "unexpected number of decompressed bytes"); + assertArrayEquals(rawUncompressed, decompressed, "unexpected decompressed data"); + // make sure additional calls to read still return EOF + assertEquals(-1, gzipIn.read(), "unexpected return from read(), expected EOF"); + assertEquals(-1, gzipIn.read(new byte[10]), "unexpected return from read(), expected EOF"); + } + } + + /* + * Verifies that the InputStream.available() method is invoked on the underlying InputStream + * to determine presence of additional GZIP members in the stream. + */ + @Test + void testInputStreamAvailableCalled() throws Exception { + final byte[] rawUncompressedMember1 = new byte[random.nextInt(111)]; + random.nextBytes(rawUncompressedMember1); + System.err.println("GZIP member 1 has " + rawUncompressedMember1.length + " bytes"); + + final byte[] rawUncompressedMember2 = new byte[random.nextInt(33)]; + random.nextBytes(rawUncompressedMember2); + System.err.println("GZIP member 2 has " + rawUncompressedMember2.length + " bytes"); + + final ByteArrayOutputStream twoMemberGzipCompressedBaos = new ByteArrayOutputStream(); + // generate GZIP format data with 2 valid GZIP members + try (GZIPOutputStream gzos = new GZIPOutputStream(twoMemberGzipCompressedBaos)) { + gzos.write(rawUncompressedMember1); // GZIP compress + gzos.write(rawUncompressedMember2); // GZIP compress + } + final byte[] gzipCompressed = twoMemberGzipCompressedBaos.toByteArray(); + final AtomicBoolean availableInvoked = new AtomicBoolean(); + // an InputStream which tracks the calls to available() + final ByteArrayInputStream underlying = new ByteArrayInputStream(gzipCompressed) { + @Override + public int available() { + availableInvoked.set(true); + return super.available(); + } + }; + // now use GZIPInputStream to decompress the compressed data and expect the decompressed + // data to be correct and also expect the InputStream.available() to have been invoked + final ByteArrayOutputStream decompressedBaos = new ByteArrayOutputStream(); + int n = 0; + try (GZIPInputStream gzipIn = new GZIPInputStream(underlying)) { + + final byte[] tmpBuf = new byte[1024]; + while ((n = gzipIn.read(tmpBuf)) != -1) { + decompressedBaos.write(tmpBuf, 0, n); + } + assertTrue(availableInvoked.get(), "InputStream.available() wasn't invoked"); + final byte[] decompressed = decompressedBaos.toByteArray(); + // verify the decompressed content, it should represent the two GZIP members + assertEquals(rawUncompressedMember1.length + rawUncompressedMember2.length, + decompressed.length, "unexpected number of decompressed bytes"); + + assertArrayEquals(rawUncompressedMember1, + Arrays.copyOfRange(decompressed, 0, rawUncompressedMember1.length), + "unexpected decompressed data of first member"); + + assertArrayEquals(rawUncompressedMember2, + Arrays.copyOfRange(decompressed, rawUncompressedMember1.length, decompressed.length), + "unexpected decompressed data of second member"); + + // make sure additional calls to read still return EOF + assertEquals(-1, gzipIn.read(), "unexpected return from read(), expected EOF"); + assertEquals(-1, gzipIn.read(new byte[42]), "unexpected return from read(), expected EOF"); + } + } + + // verify that decompressing the gzipCompressed data using GZIPInputStream + // generates the expected output + private static void verifyDecompressed(final byte[] rawUncompressed, + final byte[] gzipCompressed, + final int readBufSize, final int gzisBufSize) + throws IOException { + try (ByteArrayInputStream bais = new ByteArrayInputStream(gzipCompressed); + GZIPInputStream gzis = new GZIPInputStream(bais, gzisBufSize)) { + + byte[] result = new byte[rawUncompressed.length + 10]; byte[] buf = new byte[readBufSize]; int n = 0; - int off = 0; - + int numDecompressed = 0; while ((n = gzis.read(buf, 0, buf.length)) != -1) { - System.arraycopy(buf, 0, result, off, n); - off += n; + System.arraycopy(buf, 0, result, numDecompressed, n); + numDecompressed += n; // no range check, if overflow, let it fail } - if (off != src.length || gzis.available() != 0 || - !Arrays.equals(src, Arrays.copyOf(result, off))) { - throw new RuntimeException( - "GZIPInputStream reading failed! " + - ", src.len=" + src.length + - ", read=" + off); - } + assertEquals(rawUncompressed.length, numDecompressed, + "unexpected number of decompressed bytes"); + assertEquals(0, gzis.available(), + "unexpected additional bytes available in the GZIPInputStream"); + assertArrayEquals(rawUncompressed, Arrays.copyOf(result, numDecompressed), + "unexpected decompressed data"); } } }