| /* | |
| * 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. | |
| * <p> | |
| * The structure of an extra field is as follows: | |
| * <p> | |
| * 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); | |
| } | |
| } | |
| } | |
| } |