1 /*
  2  * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import org.junit.jupiter.api.AfterEach;
 25 import org.junit.jupiter.api.BeforeEach;
 26 import org.junit.jupiter.api.Test;
 27 
 28 import java.io.ByteArrayOutputStream;
 29 import java.io.IOException;
 30 import java.nio.ByteBuffer;
 31 import java.nio.ByteOrder;
 32 import java.nio.charset.StandardCharsets;
 33 import java.nio.file.Files;
 34 import java.nio.file.Path;
 35 import java.util.zip.ZipEntry;
 36 import java.util.zip.ZipFile;
 37 import java.util.zip.ZipOutputStream;
 38 
 39 import static org.junit.jupiter.api.Assertions.assertEquals;
 40 
 41 
 42 /**
 43  * @test
 44  * @bug 8226530 8303891
 45  * @summary Verify that ZipFile reads size fields using the Zip64 extra
 46  * field when only the 'uncompressed size' field has the ZIP64 "magic value" 0xFFFFFFFF
 47  * @compile Zip64SizeTest.java
 48  * @run junit Zip64SizeTest
 49  */
 50 public class Zip64SizeTest {
 51     // ZIP file to create
 52     private static final Path ZIP_FILE = Path.of("Zip64SizeTest.zip");
 53     // Contents to write to ZIP entries
 54     private static final byte[] CONTENT = "Hello".getBytes(StandardCharsets.UTF_8);
 55     // This opaque tag will be ignored by ZipEntry.setExtra0
 56     private static final int UNKNOWN_TAG = 0x9902;
 57     // Tag used when converting the extra field to a real ZIP64 extra field
 58     private static final short ZIP64_TAG = 0x1;
 59     // Marker value to indicate that the actual value is stored in the ZIP64 extra field
 60     private static final int ZIP64_MAGIC_VALUE = 0xFFFFFFFF;
 61 
 62     /**
 63      * Validate that if the 'uncompressed size' of a ZIP CEN header is 0xFFFFFFFF, then the
 64      * actual size is retrieved from the corresponding ZIP64 Extended information field.
 65      *
 66      * @throws IOException if an unexpected IOException occurs
 67      */
 68     @Test
 69     public void validateZipEntrySizes() throws IOException {
 70         createZipFile();
 71         System.out.println("Validating Zip Entry Sizes");
 72         try (ZipFile zip = new ZipFile(ZIP_FILE.toFile())) {
 73             ZipEntry ze = zip.getEntry("first");
 74             System.out.printf("Entry: %s, size= %s%n", ze.getName(), ze.getSize());
 75             assertEquals(CONTENT.length, ze.getSize());
 76             ze = zip.getEntry("second");
 77             System.out.printf("Entry: %s, size= %s%n", ze.getName(), ze.getSize());
 78             assertEquals(CONTENT.length, ze.getSize());
 79         }
 80     }
 81 
 82     /**
 83      * Create a ZIP file with a CEN entry where the 'uncompressed size' is stored in
 84      * the ZIP64 field, but the 'compressed size' is in the CEN field. This makes the
 85      * ZIP64 data block 8 bytes long, which triggers the regression described in 8226530.
 86      *
 87      * The CEN entry for the "first" entry will have the following structure:
 88      * (Note the CEN 'Uncompressed Length' being 0xFFFFFFFF and the ZIP64
 89      * 'Uncompressed Size' being 5)
 90      *
 91      * 0081 CENTRAL HEADER #1     02014B50
 92      * 0085 Created Zip Spec      14 '2.0'
 93      * 0086 Created OS            00 'MS-DOS'
 94      * [...] Omitted for brevity
 95      * 0091 CRC                   F7D18982
 96      * 0095 Compressed Length     00000007
 97      * 0099 Uncompressed Length   FFFFFFFF
 98      * [...] Omitted for brevity
 99      * 00AF Filename              'first'
100      * 00B4 Extra ID #0001        0001 'ZIP64'
101      * 00B6   Length              0008
102      * 00B8   Uncompressed Size   0000000000000005
103      *
104      * @throws IOException if an error occurs creating the ZIP File
105      */
106     private static void createZipFile() throws IOException {
107         ByteArrayOutputStream baos = new ByteArrayOutputStream();
108         try (ZipOutputStream zos = new ZipOutputStream(baos)) {
109 
110             // The 'first' entry will store 'uncompressed size' in the Zip64 format
111             ZipEntry e1 = new ZipEntry("first");
112 
113             // Make an extra field with the correct size for an 8-byte 'uncompressed size'
114             // Zip64 field. Temporarily use the 'unknown' tag 0x9902 to make
115             // ZipEntry.setExtra0 skip parsing this as a Zip64.
116             // See APPNOTE.TXT, 4.6.1 Third Party Mappings
117             byte[] opaqueExtra = createBlankExtra((short) UNKNOWN_TAG, (short) Long.BYTES);
118             e1.setExtra(opaqueExtra);
119 
120             zos.putNextEntry(e1);
121             zos.write(CONTENT);
122 
123             // A second entry, not in Zip64 format
124             ZipEntry e2 = new ZipEntry("second");
125             zos.putNextEntry(e2);
126             zos.write(CONTENT);
127         }
128 
129         byte[] zip = baos.toByteArray();
130 
131         // Update the CEN of 'first' to use the Zip64 format
132         updateCENHeaderToZip64(zip);
133         Files.write(ZIP_FILE, zip);
134     }
135 
136     /**
137      * Update the CEN entry of the "first" entry to use ZIP64 format for the
138      * 'uncompressed size' field. The updated extra field will have the following
139      * structure:
140      *
141      * 00B4 Extra ID #0001        0001 'ZIP64'
142      * 00B6   Length              0008
143      * 00B8   Uncompressed Size   0000000000000005
144      *
145      * @param zip the ZIP file to update to ZIP64
146      */
147     private static void updateCENHeaderToZip64(byte[] zip) {
148         ByteBuffer buffer = ByteBuffer.wrap(zip).order(ByteOrder.LITTLE_ENDIAN);
149         // Find the offset of the first CEN header
150         int cenOffset = buffer.getInt(zip.length- ZipFile.ENDHDR + ZipFile.ENDOFF);
151         // Find the offset of the extra field
152         int nlen = buffer.getShort(cenOffset + ZipFile.CENNAM);
153         int extraOffset = cenOffset + ZipFile.CENHDR + nlen;
154 
155         // Change the header ID from 'unknown' to ZIP64
156         buffer.putShort(extraOffset, ZIP64_TAG);
157         // Update the 'uncompressed size' ZIP64 value to the actual uncompressed length
158         int fieldOffset = extraOffset
159                 + Short.BYTES // TAG
160                 + Short.BYTES; // data size
161         buffer.putLong(fieldOffset, CONTENT.length);
162 
163         // Set the 'uncompressed size' field of the CEN to 0xFFFFFFFF
164         buffer.putInt(cenOffset + ZipFile.CENLEN, ZIP64_MAGIC_VALUE);
165     }
166 
167     /**
168      * Create an extra field with the given tag and data block size, and a
169      * blank data block.
170      * @return an extra field with the specified tag and size
171      * @param tag the header id of the extra field
172      * @param blockSize the size of the extra field's data block
173      */
174     private static byte[] createBlankExtra(short tag, short blockSize) {
175         int size = Short.BYTES  // tag
176                 + Short.BYTES   // data block size
177                 + blockSize;   // data block;
178 
179         byte[] extra = new byte[size];
180         ByteBuffer.wrap(extra).order(ByteOrder.LITTLE_ENDIAN)
181                 .putShort(0, tag)
182                 .putShort(Short.BYTES, blockSize);
183         return extra;
184     }
185 
186     /**
187      * Make sure the needed test files do not exist prior to executing the test
188      * @throws IOException
189      */
190     @BeforeEach
191     public void setUp() throws IOException {
192         deleteFiles();
193     }
194 
195     /**
196      * Remove the files created for the test
197      * @throws IOException
198      */
199     @AfterEach
200     public void tearDown() throws IOException {
201         deleteFiles();
202     }
203 
204     /**
205      * Delete the files created for use by the test
206      * @throws IOException if an error occurs deleting the files
207      */
208     private static void deleteFiles() throws IOException {
209         Files.deleteIfExists(ZIP_FILE);
210     }
211 }