/* * Copyright (c) 2019, 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. */ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static org.junit.jupiter.api.Assertions.assertEquals; /** * @test * @bug 8226530 8303891 * @summary Verify that ZipFile reads size fields using the Zip64 extra * field when only the 'uncompressed size' field has the ZIP64 "magic value" 0xFFFFFFFF * @compile Zip64SizeTest.java * @run junit Zip64SizeTest */ public class Zip64SizeTest { // ZIP file to create private static final Path ZIP_FILE = Path.of("Zip64SizeTest.zip"); // Contents to write to ZIP entries private static final byte[] CONTENT = "Hello".getBytes(StandardCharsets.UTF_8); // This opaque tag will be ignored by ZipEntry.setExtra0 private static final int UNKNOWN_TAG = 0x9902; // Tag used when converting the extra field to a real ZIP64 extra field private static final short ZIP64_TAG = 0x1; // Marker value to indicate that the actual value is stored in the ZIP64 extra field private static final int ZIP64_MAGIC_VALUE = 0xFFFFFFFF; /** * Validate that if the 'uncompressed size' of a ZIP CEN header is 0xFFFFFFFF, then the * actual size is retrieved from the corresponding ZIP64 Extended information field. * * @throws IOException if an unexpected IOException occurs */ @Test public void validateZipEntrySizes() throws IOException { createZipFile(); System.out.println("Validating Zip Entry Sizes"); try (ZipFile zip = new ZipFile(ZIP_FILE.toFile())) { ZipEntry ze = zip.getEntry("first"); System.out.printf("Entry: %s, size= %s%n", ze.getName(), ze.getSize()); assertEquals(CONTENT.length, ze.getSize()); ze = zip.getEntry("second"); System.out.printf("Entry: %s, size= %s%n", ze.getName(), ze.getSize()); assertEquals(CONTENT.length, ze.getSize()); } } /** * Create a ZIP file with a CEN entry where the 'uncompressed size' is stored in * the ZIP64 field, but the 'compressed size' is in the CEN field. This makes the * ZIP64 data block 8 bytes long, which triggers the regression described in 8226530. * * The CEN entry for the "first" entry will have the following structure: * (Note the CEN 'Uncompressed Length' being 0xFFFFFFFF and the ZIP64 * 'Uncompressed Size' being 5) * * 0081 CENTRAL HEADER #1 02014B50 * 0085 Created Zip Spec 14 '2.0' * 0086 Created OS 00 'MS-DOS' * [...] Omitted for brevity * 0091 CRC F7D18982 * 0095 Compressed Length 00000007 * 0099 Uncompressed Length FFFFFFFF * [...] Omitted for brevity * 00AF Filename 'first' * 00B4 Extra ID #0001 0001 'ZIP64' * 00B6 Length 0008 * 00B8 Uncompressed Size 0000000000000005 * * @throws IOException if an error occurs creating the ZIP File */ private static void createZipFile() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { // The 'first' entry will store 'uncompressed size' in the Zip64 format ZipEntry e1 = new ZipEntry("first"); // Make an extra field with the correct size for an 8-byte 'uncompressed size' // Zip64 field. Temporarily use the 'unknown' tag 0x9902 to make // ZipEntry.setExtra0 skip parsing this as a Zip64. // See APPNOTE.TXT, 4.6.1 Third Party Mappings byte[] opaqueExtra = createBlankExtra((short) UNKNOWN_TAG, (short) Long.BYTES); e1.setExtra(opaqueExtra); zos.putNextEntry(e1); zos.write(CONTENT); // A second entry, not in Zip64 format ZipEntry e2 = new ZipEntry("second"); zos.putNextEntry(e2); zos.write(CONTENT); } byte[] zip = baos.toByteArray(); // Update the CEN of 'first' to use the Zip64 format updateCENHeaderToZip64(zip); Files.write(ZIP_FILE, zip); } /** * Update the CEN entry of the "first" entry to use ZIP64 format for the * 'uncompressed size' field. The updated extra field will have the following * structure: * * 00B4 Extra ID #0001 0001 'ZIP64' * 00B6 Length 0008 * 00B8 Uncompressed Size 0000000000000005 * * @param zip the ZIP file to update to ZIP64 */ private static void updateCENHeaderToZip64(byte[] zip) { ByteBuffer buffer = ByteBuffer.wrap(zip).order(ByteOrder.LITTLE_ENDIAN); // Find the offset of the first CEN header int cenOffset = buffer.getInt(zip.length- ZipFile.ENDHDR + ZipFile.ENDOFF); // Find the offset of the extra field int nlen = buffer.getShort(cenOffset + ZipFile.CENNAM); int extraOffset = cenOffset + ZipFile.CENHDR + nlen; // Change the header ID from 'unknown' to ZIP64 buffer.putShort(extraOffset, ZIP64_TAG); // Update the 'uncompressed size' ZIP64 value to the actual uncompressed length int fieldOffset = extraOffset + Short.BYTES // TAG + Short.BYTES; // data size buffer.putLong(fieldOffset, CONTENT.length); // Set the 'uncompressed size' field of the CEN to 0xFFFFFFFF buffer.putInt(cenOffset + ZipFile.CENLEN, ZIP64_MAGIC_VALUE); } /** * Create an extra field with the given tag and data block size, and a * blank data block. * @return an extra field with the specified tag and size * @param tag the header id of the extra field * @param blockSize the size of the extra field's data block */ private static byte[] createBlankExtra(short tag, short blockSize) { int size = Short.BYTES // tag + Short.BYTES // data block size + blockSize; // data block; byte[] extra = new byte[size]; ByteBuffer.wrap(extra).order(ByteOrder.LITTLE_ENDIAN) .putShort(0, tag) .putShort(Short.BYTES, blockSize); return extra; } /** * Make sure the needed test files do not exist prior to executing the test * @throws IOException */ @BeforeEach public void setUp() throws IOException { deleteFiles(); } /** * Remove the files created for the test * @throws IOException */ @AfterEach public void tearDown() throws IOException { deleteFiles(); } /** * Delete the files created for use by the test * @throws IOException if an error occurs deleting the files */ private static void deleteFiles() throws IOException { Files.deleteIfExists(ZIP_FILE); } }