/* * 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 8272746 * @modules java.base/jdk.internal.util * @summary Verify that ZipFile rejects a ZIP with a CEN size which does not fit in a Java byte array * @run junit CenSizeTooLarge */ import jdk.internal.util.ArraysSupport; import org.junit.Before; import org.junit.Test; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.time.LocalDateTime; 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.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class CenSizeTooLarge { // Entry names produced in this test are fixed-length public static final int NAME_LENGTH = 10; // 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 - NAME_LENGTH; // Data size (unsigned short) // Field size minus the leading header 'tag' and 'data size' fields (2 bytes each) static final short MAX_DATA_SIZE = (short) (MAX_EXTRA_FIELD_SIZE - 2 * Short.BYTES); // Tag for the 'unknown' field type, specified in APPNOTE.txt 'Third party mappings' static final short UNKNOWN_ZIP_TAG = (short) 0x9902; // Use a shared LocalDateTime on all entries to save processing time static final LocalDateTime TIME_LOCAL = LocalDateTime.now(); // The size of one CEN header, including the name and the extra field static final int CEN_HEADER_SIZE = ZipFile.CENHDR + NAME_LENGTH + MAX_EXTRA_FIELD_SIZE; // The number of entries needed to exceed the MAX_CEN_SIZE static final int NUM_ENTRIES = (MAX_CEN_SIZE / CEN_HEADER_SIZE) + 1; // Expected ZipException message when the CEN does not fit in a Java byte array private static final String CEN_TOO_LARGE_MESSAGE = "invalid END header (central directory size too large)"; // Zip file to create for testing private File hugeZipFile; private static final byte[] EXTRA_BYTES = makeLargeExtraField(); // Helps SparseOutputStream detect write of the last CEN entry private static final byte[] LAST_EXTRA_BYTES = makeLargeExtraField(); /** * Create a zip file with a CEN size which does not fit within a Java byte array */ @Before public void setup() throws IOException { hugeZipFile = new File("cen-too-large.zip"); hugeZipFile.deleteOnExit(); try (OutputStream out = new SparseOutputStream(new FileOutputStream(hugeZipFile)); ZipOutputStream zip = new ZipOutputStream(out)) { // Keep track of entries so we can update extra data before the CEN is written ZipEntry[] entries = new ZipEntry[NUM_ENTRIES]; // Add entries until MAX_CEN_SIZE is reached for (int i = 0; i < NUM_ENTRIES; i++) { // Create a fixed-length name for the entry String name = Integer.toString(i); name = "0".repeat(NAME_LENGTH - name.length()) + name; // Create and track the entry ZipEntry entry = entries[i] = new ZipEntry(name); // Use STORED for faster processing entry.setMethod(ZipEntry.STORED); entry.setSize(0); entry.setCrc(0); // Set the time/date field for faster processing entry.setTimeLocal(TIME_LOCAL); // Add the entry zip.putNextEntry(entry); } // Finish writing the last entry zip.closeEntry(); // Before the CEN headers are written, set the extra data on each entry for (ZipEntry entry : entries) { entry.setExtra(EXTRA_BYTES); } // Help SparseOutputSream detect the last entry entries[entries.length-1].setExtra(LAST_EXTRA_BYTES); } } /** * Validates that a ZipException is thrown with the expected message when * the ZipFile is initialized with a ZIP whose CEN exeeds {@link #MAX_CEN_SIZE} */ @Test public void centralDirectoryTooLargeToFitInByteArray() { ZipException ex = assertThrows(ZipException.class, () -> new ZipFile(hugeZipFile)); assertEquals(CEN_TOO_LARGE_MESSAGE, ex.getMessage()); } /** * We can reduce the number of written CEN headers by making each CEN header maximally large. * We do this by adding the extra field produced by this method to each CEN header. *

* The structure of an extra field is as follows: *

* Header ID (Two bytes, describes the type of the field, also called 'tag') * Data Size (Two byte short) * Data Block (Contents depend on field type) */ private static byte[] makeLargeExtraField() { // Make a maximally sized extra field byte[] extra = new byte[MAX_EXTRA_FIELD_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(MAX_DATA_SIZE); return extra; } /** * By writing sparse 'holes' until the last CEN is detected, we can save disk space * used by this test from ~2GB to ~4K. Instances of this class should be passed * directly to the ZipOutputStream constructor, without any buffering. Otherwise, * writes from ZipOutputStream may not be detected correctly. */ private static class SparseOutputStream extends FilterOutputStream { private final FileChannel channel; private boolean sparse = true; // True until the last CEN is written private long position = 0; public SparseOutputStream(FileOutputStream fos) { super(fos); this.channel = fos.getChannel(); } @Override public void write(byte[] b, int off, int len) throws IOException { position += len; if (sparse) { // Until finding the last CEN, we don't actually write anything, // but instead simply advance the position, creating a sparse file channel.position(position); // Check for last CEN record if (b == LAST_EXTRA_BYTES) { // From here on, write actual bytes sparse = false; } } else { // Regular write out.write(b, off, len); } } @Override public void write(int b) throws IOException { position++; if (sparse) { channel.position(position); } else { out.write(b); } } } }