/* * Copyright Amazon.com Inc. 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 8253952 * @summary Test behaviour when copying ZipEntries between zip files. * @run junit CopyZipFile */ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Enumeration; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.*; import static org.junit.jupiter.api.Assertions.*; public class CopyZipFile { // ZIP file created in this test private Path zip = Path.of("first.zip"); // The content to put in each entry private static final byte[] TEST_STRING = "TestTestTest".getBytes(StandardCharsets.UTF_8); /** * Create the sample ZIP file used in this test, including a STORED entry * and DEFLATE entries with various compression levels. * @throws IOException if an unexpected IOException occurs */ @BeforeEach public void createZip() throws IOException { // By default, ZipOutputStream creates zip files with Local File Headers // without size, compressed size and crc values and an extra Data // Descriptor (see https://en.wikipedia.org/wiki/Zip_(file_format) // after the data belonging to that entry with these values if in the // corresponding ZipEntry one of the size, compressedSize or crc fields is // equal to '-1' (which is the default for newly created ZipEntries). try (OutputStream os = Files.newOutputStream(zip) ; ZipOutputStream zos = new ZipOutputStream(os)) { // First file will be compressed with DEFAULT_COMPRESSION (i.e. -1 or 6) zos.setLevel(Deflater.DEFAULT_COMPRESSION); zos.putNextEntry(new ZipEntry("DEFAULT_COMPRESSION.txt")); zos.write(TEST_STRING); // Second file won't be compressed at all (i.e. STORED) zos.setMethod(ZipOutputStream.STORED); ZipEntry ze = new ZipEntry("STORED.txt"); ze.setSize(TEST_STRING.length); ze.setCompressedSize(TEST_STRING.length); CRC32 crc = new CRC32(); crc.update(TEST_STRING); ze.setCrc(crc.getValue()); zos.putNextEntry(ze); zos.write(TEST_STRING); // Third file will be compressed with NO_COMPRESSION (i.e. 0) zos.setMethod(ZipOutputStream.DEFLATED); zos.setLevel(Deflater.NO_COMPRESSION); zos.putNextEntry(new ZipEntry("NO_COMPRESSION.txt")); zos.write(TEST_STRING); // Fourth file will be compressed with BEST_SPEED (i.e. 1) zos.setLevel(Deflater.BEST_SPEED); zos.putNextEntry(new ZipEntry("BEST_SPEED.txt")); zos.write(TEST_STRING); // Fifth file will be compressed with BEST_COMPRESSION (i.e. 9) zos.setLevel(Deflater.BEST_COMPRESSION); zos.putNextEntry(new ZipEntry("BEST_COMPRESSION.txt")); zos.write(TEST_STRING); } } /** * Delete the ZIP file produced by this test * @throws IOException if an unexpected IOException occurs */ @AfterEach public void cleanup() throws IOException { Files.deleteIfExists(zip); } /** * Read all entries using ZipInputStream.getNextEntry and copy them * to a new zip file using ZipOutputStream.putNextEntry. This only works * reliably because the input zip file has no values for the size, compressedSize * and crc values of streamed zip entries in the local file header and * therefore the ZipEntry objects created by ZipOutputStream.getNextEntry * will have all these fields set to '-1'. * * @throws IOException if an unexpected IOException occurs */ @Test public void copyFromZipInputStreamToZipOutputStream() throws IOException { try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip)); ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream())) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { // ZipInputStream.getNextEntry() only reads the Local File Header of a zip entry, // so for the zip file we've just generated the ZipEntry fields 'size', 'compressedSize` // and 'crc' for deflated entries should be uninitialized (i.e. '-1'). System.out.println( String.format("name=%s, clen=%d, len=%d, crc=%d", entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc())); if (entry.getMethod() == ZipEntry.DEFLATED) { // Expect size, compressed size and crc to not be initialized at this point assertEquals(-1, entry.getCompressedSize()); assertEquals(-1, entry.getSize()); assertEquals(-1, entry.getCrc()); } zos.putNextEntry(entry); zis.transferTo(zos); // After all the data belonging to a zip entry has been inflated (i.e. after ZipInputStream.read() // returned '-1'), it is guaranteed that the ZipInputStream will also have consumed the Data // Descriptor (if any) after the data and will have updated the 'size', 'compressedSize' and 'crc' // fields of the ZipEntry object. System.out.println( String.format("name=%s, clen=%d, len=%d, crc=%d\n", entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc())); // Expect size, compressed size and crc to be initialized at this point assertNotEquals(-1, entry.getCompressedSize()); assertNotEquals(-1, entry.getSize()); assertNotEquals(-1, entry.getCrc()); } } } /** * Read all entries using the ZipFile class and copy them to a new zip file * using ZipOutputStream.putNextEntry. * The ZipFile class reads all the zip entries from the Central * Directory, which has accurate information for size, compressedSize and crc. * This means that all ZipEntry objects returned from ZipFile will have correct * settings for these fields. * If the compression level was different in the input zip file (which we can't know * because the zip file format doesn't record this information), the * size of the re-compressed entry we are writing to the ZipOutputStream might differ * from the original compressed size recorded in the ZipEntry. This would result in an * "invalid entry compressed size" ZipException if ZipOutputStream wouldn't ignore * the implicitely set compressed size attribute of ZipEntries read from a ZipFile * or ZipInputStream. * @throws IOException if an unexpected IOException occurs */ @Test public void copyFromZipFileToZipOutputStream() throws IOException { try (ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream()); ZipFile zf = new ZipFile(zip.toFile())) { ZipEntry entry; Enumeration entries = zf.entries(); while (entries.hasMoreElements()) { entry = entries.nextElement(); System.out.println( String.format("name=%s, clen=%d, len=%d, crc=%d\n", entry.getName(), entry.getCompressedSize(), entry.getSize(), entry.getCrc())); // Expect size, compressed size and crc to be initialized at this point assertNotEquals(-1, entry.getCompressedSize()); assertNotEquals(-1, entry.getSize()); assertNotEquals(-1, entry.getCrc()); zos.putNextEntry(entry); try (InputStream is = zf.getInputStream(entry)) { is.transferTo(zos); } zos.closeEntry(); } } } /** * If the compressed size is set explicitly using ZipEntry.setCompressedSize(), * then the entry will be restreamed with a data descriptor and the compressed size * recomputed. If the source compression level was different from the target compression * level, the compressed sizes may differ and a ZipException will be thrown * when the entry is closed in ZipOutputStream.closeEntry * * @throws IOException if an unexpected IOException is thrown */ @Test public void explicitCompressedSizeWithDifferentCompressionLevels() throws IOException { try (ZipOutputStream zos = new ZipOutputStream(OutputStream.nullOutputStream()); ZipFile zf = new ZipFile(zip.toFile())) { // Be explicit about the default compression level zos.setLevel(Deflater.DEFAULT_COMPRESSION); Enumeration entries = zf.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); // Explicitly setting the compressed size will disable data descriptors // and enable validation that the compressed size in the ZipEntry matches the // actual compressed size written by ZipOutputStream entry.setCompressedSize(entry.getCompressedSize()); try (InputStream is = zf.getInputStream(entry)) { zos.putNextEntry(entry); is.transferTo(zos); // Some compression levels lead to unexpected recompressed sizes when closing the entry switch (entry.getName()) { case "DEFAULT_COMPRESSION.txt" -> { // DEFAULT_COMPRESSION matches expected size zos.closeEntry(); } case "STORED.txt" -> { // STORED should not throw zos.closeEntry(); } case "NO_COMPRESSION.txt", "BEST_SPEED.txt" -> { // NO_COMPRESSION and BEST_SPEED should lead to an unexpected recompressed size ZipException ze = assertThrows(ZipException.class, () -> { zos.closeEntry(); }); // Hack to fix and close the offending zip entry with the correct recompressed size. // The exception message is something like: // "invalid entry compressed size (expected 12 but got 7 bytes)" // and we need to extract the second integer. Pattern cSize = Pattern.compile("\\d+"); Matcher m = cSize.matcher(ze.getMessage()); m.find(); m.find(); entry.setCompressedSize(Integer.parseInt(m.group())); zos.closeEntry(); } case "BEST_COMPRESSION.txt" -> { // BEST_COMPRESSION produces the same compressed // size as DEFAULT_COMPRESSION for sample content zos.closeEntry(); } default -> { throw new IllegalArgumentException("Unexpected entry " + entry.getName()); } } } } } } }