/* * Copyright (c) 2022, 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 8341595 * @modules java.base/jdk.internal.util * @summary Verify that ZipFile can read from a ZIP file with a maximally large CEN size * @run junit/othervm/manual -Xmx2500M CenSizeMaximum */ import jdk.internal.util.ArraysSupport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.EnumSet; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static org.junit.jupiter.api.Assertions.*; public class CenSizeMaximum { // Maximum allowed CEN size allowed by the ZipFile implementation static final int MAX_CEN_SIZE = ArraysSupport.SOFT_MAX_ARRAY_LENGTH; /** * From the APPNOTE.txt specification: * 4.4.10 file name length: (2 bytes) * 4.4.11 extra field length: (2 bytes) * 4.4.12 file comment length: (2 bytes) * * The length of the file name, extra field, and comment * fields respectively. The combined length of any * directory record and these three fields SHOULD NOT * generally exceed 65,535 bytes. *. * Create a maximum extra field which does not exceed 65,535 bytes */ static final int MAX_EXTRA_FIELD_SIZE = 65_535 - ZipFile.CENHDR; // Tag for the 'unknown' field type, specified in APPNOTE.txt 'Third party mappings' static final short UNKNOWN_ZIP_TAG = (short) 0x9902; // The size of one CEN header, including the name and the extra field static final int CEN_HEADER_SIZE = ZipFile.CENHDR + MAX_EXTRA_FIELD_SIZE; // The size of the extra data field header (tag id + data block length) static final int EXTRA_FIELD_HEADER_SIZE = 2 * Short.BYTES; // Zip file to create for testing private Path hugeZipFile = Path.of("cen-size-on-limit.zip"); /** * Clean up ZIP file created in this test */ @AfterEach public void cleanup() throws IOException { //Files.deleteIfExists(hugeZipFile); } /** * Validates that ZipFile opens a ZIP file with a CEN size close * to the {@link #MAX_CEN_SIZE} implementation limit. * * @throws IOException if an unexpected IO error occurs */ @Test public void maximumCenSize() throws IOException { int numCenHeaders = zipWithWithExactCenSize(MAX_CEN_SIZE, true, false); try (var zf = new ZipFile(hugeZipFile.toFile())) { assertEquals(numCenHeaders, zf.size()); } } /** * Validates that ZipFile rejects a ZIP where the last CEN record * overflows the CEN size and the END header CENTOT field is smaller * than the actual number of headers * * @throws IOException if an unexpected IO error occurs */ @Test public void lastCENHeaderBadSize() throws IOException { int numCenHeaders = zipWithWithExactCenSize(1024, true, true); ZipException zipException = assertThrows(ZipException.class, () -> { try (var zf = new ZipFile(hugeZipFile.toFile())) { assertEquals(numCenHeaders, zf.size()); } }); assertEquals("invalid CEN header (bad header size)", zipException.getMessage()); } /** * Produce a ZIP file with an exact CEN size. To minimize the number of CEN headers * written, maximally large, empty extra data blocks are written sparsely. * * @param cenSize the exact CEN size of the ZIP file to produce * @param invalidateEndTotal whether to decrement the END header's TOT field by one * @return the number of CEN headers produced * @throws IOException if an unexpected IO error occurs */ private int zipWithWithExactCenSize(long cenSize, boolean invalidateEndTotal, boolean overflowLastCEN) throws IOException { // Sanity check assertTrue(cenSize <= MAX_CEN_SIZE); // The number of CEN headers we need to write int numCenHeaders = (int) (cenSize / CEN_HEADER_SIZE) + 1; // Size if all extra data fields were of maximum size long overSized = numCenHeaders * (long) CEN_HEADER_SIZE; // Length to trim from the first CEN's extra data int negativPadding = (int) (overSized - cenSize); int firstExtraSize = MAX_EXTRA_FIELD_SIZE - negativPadding; // Sanity check long computedCenSize = (numCenHeaders -1L ) * CEN_HEADER_SIZE + ZipEntry.CENHDR + firstExtraSize; assertEquals(computedCenSize, cenSize); // A CEN header, followed by the four-bytes extra data header ByteBuffer cenHeader = createCENHeader(); // An END header ByteBuffer endHeader = createENDHeader(); // Update the END header if (invalidateEndTotal) { // To trigger countCENHeaders endHeader.putShort(ZipEntry.ENDTOT, (short) (numCenHeaders -1 & 0xFFFF)); } else { endHeader.putShort(ZipEntry.ENDTOT, (short) (numCenHeaders & 0xFFFF)); } // Update CEN size and offset fields endHeader.putInt(ZipEntry.ENDSIZ, (int) (cenSize & 0xFFFFFFFFL)); endHeader.putInt(ZipEntry.ENDOFF, 0); // When creating a sparse file, the file must not already exit Files.deleteIfExists(hugeZipFile); // Open a FileChannel for writing a sparse file EnumSet options = EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.SPARSE); try (FileChannel channel = FileChannel.open(hugeZipFile, options)) { // Write CEN headers for (int i = 0; i < numCenHeaders; i++) { // The first CEN header has trimmed extra data int extraSize = i == 0 ? firstExtraSize : MAX_EXTRA_FIELD_SIZE; if (overflowLastCEN && i == numCenHeaders - 1) { // make last CEN header overflow the CEN size cenHeader.putShort(ZipEntry.CENNAM, Short.MAX_VALUE); } // update elen field cenHeader.putShort(ZipEntry.CENEXT, (short) (extraSize & 0xFFFF)); // update data block len of the extra field header short dlen = (short) ((extraSize - EXTRA_FIELD_HEADER_SIZE) & 0xFFFF); cenHeader.putShort(ZipEntry.CENHDR + Short.BYTES, dlen); // Write the CEN header plus the four-byte extra header channel.write(cenHeader.rewind()); // Sparse "write" of the extra data block channel.position(channel.position() + extraSize - EXTRA_FIELD_HEADER_SIZE); } // Sanity check assertEquals(cenSize, channel.position()); // Write the END header channel.write(endHeader.rewind()); } return numCenHeaders; } // Creates a ByteBuffer representing a CEN header with a trailing extra field header private ByteBuffer createCENHeader() throws IOException { byte[] bytes = smallZipfile(); ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); int endOff = bytes.length - ZipEntry.ENDHDR; int cenSize = buf.getInt(endOff + ZipEntry.ENDSIZ); int cenOff = buf.getInt(endOff + ZipEntry.ENDOFF); return ByteBuffer.wrap( Arrays.copyOfRange(bytes, cenOff, cenOff + ZipEntry.CENHDR + EXTRA_FIELD_HEADER_SIZE) ).order(ByteOrder.LITTLE_ENDIAN); } // Creates a ByteBuffer representing an END header private ByteBuffer createENDHeader() throws IOException { byte[] bytes = smallZipfile(); ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); int endOff = bytes.length - ZipEntry.ENDHDR; return ByteBuffer.wrap( Arrays.copyOfRange(bytes, endOff, endOff + ZipEntry.ENDHDR) ).order(ByteOrder.LITTLE_ENDIAN); } // Create a byte array with a minimal ZIP file private static byte[] smallZipfile() throws IOException { var out = new ByteArrayOutputStream(); try (var zo = new ZipOutputStream(out)) { ZipEntry entry = new ZipEntry(""); entry.setExtra(makeDummyExtraField()); zo.putNextEntry(entry); } return out.toByteArray(); } // Create a minimally sized extra field private static byte[] makeDummyExtraField() { byte[] extra = new byte[EXTRA_FIELD_HEADER_SIZE]; // Little-endian ByteBuffer for updating the header fields ByteBuffer buffer = ByteBuffer.wrap(extra).order(ByteOrder.LITTLE_ENDIAN); // We use the 'unknown' tag, specified in APPNOTE.TXT, 4.6.1 Third party mappings' buffer.putShort(UNKNOWN_ZIP_TAG); // Size of the actual (empty) data buffer.putShort((short) 0); return extra; } }