/* * Copyright (c) 1999, 2024, 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. * * 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. */ /* @test @bug 4241361 4842702 4985614 6646605 5032358 6923692 6233323 8144977 8186464 4401122 8322830 @summary Make sure we can read a zip file. @modules jdk.zipfs @run junit ReadZip */ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.Collections; import java.util.HexFormat; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.junit.jupiter.api.Assertions.*; public class ReadZip { // ZIP file produced during tests private Path zip = Path.of("read-zip.zip"); /** * Create a sample ZIP file for use by tests * @param name name of the ZIP file to create * @return a sample ZIP file * @throws IOException if an unexpected IOException occurs */ private Path createZip(String name) throws IOException { Path zip = Path.of(name); try (OutputStream out = Files.newOutputStream(zip); ZipOutputStream zo = new ZipOutputStream(out)) { zo.putNextEntry(new ZipEntry("file.txt")); zo.write("hello".getBytes(StandardCharsets.UTF_8)); } return zip; } /** * Delete the ZIP file produced after each test method * @throws IOException if an unexpected IOException occurs */ @AfterEach public void cleanup() throws IOException { Files.deleteIfExists(zip); } /** * Make sure we throw NPE when calling getEntry or getInputStream with null params * * @throws IOException if an unexpected IOException occurs */ @Test public void nullPointerExceptionOnNullParams() throws IOException { zip = createZip("null-params.zip"); try (ZipFile zf = new ZipFile(zip.toFile())) { assertThrows(NullPointerException.class, () -> zf.getEntry(null)); assertThrows(NullPointerException.class, () -> zf.getInputStream(null)); // Sanity check that we can still read an entry ZipEntry ze = zf.getEntry("file.txt"); assertNotNull(ze, "cannot read from zip file"); } } /** * Read the zip file that has some garbage bytes padded at the end * @throws IOException if an unexpected IOException occurs */ @Test public void bytesPaddedAtEnd() throws IOException { zip = createZip("bytes-padded.zip"); // pad some bytes try (OutputStream os = Files.newOutputStream(zip, StandardOpenOption.APPEND)) { os.write(1); os.write(3); os.write(5); os.write(7); } try (ZipFile zf = new ZipFile(zip.toFile())) { ZipEntry ze = zf.getEntry("file.txt"); assertNotNull(ze, "cannot read from zip file"); } } /** * Verify that we can read a comment from the ZIP * file's 'End of Central Directory' header * @throws IOException if an unexpected IOException occurs */ @Test public void readZipFileComment() throws IOException { // Create a zip file with a comment in the 'End of Central Directory' header try (OutputStream out = Files.newOutputStream(zip); ZipOutputStream zos = new ZipOutputStream(out)) { ZipEntry ze = new ZipEntry("ZipEntry"); zos.putNextEntry(ze); zos.write(1); zos.write(2); zos.write(3); zos.write(4); zos.closeEntry(); zos.setComment("This is the comment for testing"); } // Read zip file comment try (ZipFile zf = new ZipFile(zip.toFile())) { ZipEntry ze = zf.getEntry("ZipEntry"); assertNotNull(ze, "cannot read entry from zip file"); assertEquals("This is the comment for testing", zf.getComment()); } } /** * Verify that a directory entry can be found using the * name 'directory/' as well as 'directory/' * * @throws IOException if an unexpected IOException occurs */ @Test public void readDirectoryEntries() throws IOException { // Create a ZIP containing some directory entries try (OutputStream fos = Files.newOutputStream(zip); ZipOutputStream zos = new ZipOutputStream(fos)) { // Add a META-INF directory with STORED compression type ZipEntry metaInf = new ZipEntry("META-INF/"); metaInf.setMethod(ZipEntry.STORED); metaInf.setSize(0); metaInf.setCrc(0); zos.putNextEntry(metaInf); // Add a regular directory ZipEntry dir = new ZipEntry("directory/"); zos.putNextEntry(dir); zos.closeEntry(); } // Verify directory lookups try (ZipFile zf = new ZipFile(zip.toFile())) { // Look up 'directory/' using the full name ZipEntry ze = zf.getEntry("directory/"); assertNotNull(ze, "read entry \"directory/\" failed"); assertTrue(ze.isDirectory(), "read entry \"directory/\" failed"); assertEquals("directory/", ze.getName()); try (InputStream is = zf.getInputStream(ze)) { is.available(); } catch (Exception x) { x.printStackTrace(); } // Look up 'directory/' without the trailing slash ze = zf.getEntry("directory"); assertNotNull(ze, "read entry \"directory\" failed"); assertTrue(ze.isDirectory(), "read entry \"directory\" failed"); assertEquals("directory/", ze.getName()); try (InputStream is = zf.getInputStream(ze)) { is.available(); } catch (Exception x) { x.printStackTrace(); } // Sanity check that also META-INF/ can be looked up with or without the trailing slash assertNotNull(zf.getEntry("META-INF")); assertNotNull(zf.getEntry("META-INF/")); assertEquals(zf.getEntry("META-INF").getName(), zf.getEntry("META-INF/").getName()); } } /** * Throw a NoSuchFileException exception when reading a non-existing zip file */ @Test public void nonExistingFile() { File nonExistingFile = new File("non-existing-file-f6804460f.zip"); assertThrows(NoSuchFileException.class, () -> new ZipFile(nonExistingFile)); } /** * Read a Zip file with a 'Zip64 End of Central Directory header' which was created * using ZipFileSystem with the 'forceZIP64End' option. * @throws IOException if an unexpected IOException occurs */ @Test public void readZip64EndZipFs() throws IOException { // Create zip file with Zip64 end Map env = Map.of("create", "true", "forceZIP64End", "true"); try (FileSystem fs = FileSystems.newFileSystem(zip, env)) { Files.write(fs.getPath("hello"), "hello".getBytes()); } // Read using ZipFile try (ZipFile zf = new ZipFile(zip.toFile())) { try (InputStream in = zf.getInputStream(zf.getEntry("hello"))) { assertEquals("hello", new String(in.readAllBytes(), StandardCharsets.US_ASCII)); } } // Read using ZipFileSystem try (FileSystem fs = FileSystems.newFileSystem(zip, Map.of())) { assertEquals("hello", new String(Files.readAllBytes(fs.getPath("hello")))); } } /** * Read a zip file created via Info-ZIP in streaming mode, * which includes a 'Zip64 End of Central Directory header'. * * @throws IOException if an unexpected IOException occurs * @throws InterruptedException if an unexpected InterruptedException occurs */ @Test public void readZip64EndInfoZIPStreaming() throws IOException, InterruptedException { // ZIP created using: "echo -n hello | zip zip64.zip -" // Hex encoded using: "cat zip64.zip | xxd -ps" byte[] zipBytes = HexFormat.of().parseHex(""" 504b03042d0000000000c441295886a61036ffffffffffffffff01001400 2d010010000500000000000000050000000000000068656c6c6f504b0102 1e032d0000000000c441295886a610360500000005000000010000000000 000001000000b011000000002d504b06062c000000000000001e032d0000 00000000000000010000000000000001000000000000002f000000000000 003800000000000000504b06070000000067000000000000000100000050 4b050600000000010001002f000000380000000000 """.replaceAll("\n","") ); Files.write(zip, zipBytes); try (ZipFile zf = new ZipFile(this.zip.toFile())) { try (InputStream in = zf.getInputStream(zf.getEntry("-"))) { String contents = new String(in.readAllBytes(), StandardCharsets.US_ASCII); assertEquals("hello", contents); } } } /** * Check that the available() method overriden by the input stream returned by * ZipFile.getInputStream correctly returns the number of remaining uncompressed bytes * * @throws IOException if an unexpected IOException occurs */ @Test public void availableShouldReturnRemainingUncompressedBytes() throws IOException { // The number of uncompressed bytes to write to the sample ZIP entry final int expectedBytes = 512; // Create a sample ZIP with deflated entry of a known uncompressed size try (ZipOutputStream zo = new ZipOutputStream(Files.newOutputStream(zip))) { zo.putNextEntry(new ZipEntry("file.txt")); zo.write(new byte[expectedBytes]); } // Verify the behavior of ZipFileInflaterInputStream.available() try (ZipFile zf = new ZipFile(zip.toFile())) { ZipEntry e = zf.getEntry("file.txt"); try (InputStream in = zf.getInputStream(e)) { // Initially, available() should return the full uncompressed size of the entry assertEquals(expectedBytes, in.available(), "wrong initial return value of available"); // Reading a few bytes should reduce the number of available bytes accordingly int bytesToRead = 10; in.read(new byte[bytesToRead]); assertEquals(expectedBytes - bytesToRead, in.available()); // Reading all remaining bytes should reduce the number of available bytes to zero in.transferTo(OutputStream.nullOutputStream()); assertEquals(0, in.available()); // available on a closed input stream should return zero in.close(); assertEquals(0, in.available()); } } } /** * Verify that reading an InputStream from a closed ZipFile * throws IOException as expected and does not crash the VM. * See bugs: 4528128 6846616 * * @throws IOException if an unexpected IOException occurs */ @Test public void readAfterClose() throws IOException { zip = createZip("read-after-close.zip"); InputStream in; try (ZipFile zf = new ZipFile(zip.toFile())) { ZipEntry zent = zf.getEntry("file.txt"); in = zf.getInputStream(zent); } // zf is closed at this point assertThrows(IOException.class, () -> { in.read(); }); assertThrows(IOException.class, () -> { in.read(new byte[10]); }); assertThrows(IOException.class, () -> { byte[] buf = new byte[10]; in.read(buf, 0, buf.length); }); assertThrows(IOException.class, () -> { in.readAllBytes(); }); } /** * Verify that ZipFile can open a ZIP file with zero entries * * @throws IOException if an unexpected IOException occurs */ @Test public void noEntries() throws IOException { // Create a ZIP file with no entries try (ZipOutputStream zo = new ZipOutputStream(Files.newOutputStream(zip))) { } // Open the "empty" ZIP file try (ZipFile zf = new ZipFile(zip.toFile())) { // Verify size assertEquals(0, zf.size()); // Verify entry lookup using ZipFile.getEntry() assertNull(zf.getEntry("file.txt")); // Verify iteration using ZipFile.entries() assertEquals(Collections.emptyList(), Collections.list(zf.entries())); // Verify iteration using ZipFile.stream() assertEquals(Collections.emptyList(), zf.stream().toList()); } } }