|
| 1 | +/* |
| 2 | + * Copyright (c) 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 | +/* |
| 25 | + * @test |
| 26 | + * @bug 8303866 |
| 27 | + * @summary ZipInputStream should read 8-byte data descriptors if the LOC has |
| 28 | + * a ZIP64 extended information extra field |
| 29 | + * @run junit Zip64DataDescriptor |
| 30 | + */ |
| 31 | + |
| 32 | + |
| 33 | +import org.junit.jupiter.api.Test; |
| 34 | +import org.junit.jupiter.api.BeforeEach; |
| 35 | + |
| 36 | +import java.io.ByteArrayInputStream; |
| 37 | +import java.io.ByteArrayOutputStream; |
| 38 | +import java.io.IOException; |
| 39 | +import java.io.OutputStream; |
| 40 | +import java.nio.ByteBuffer; |
| 41 | +import java.nio.ByteOrder; |
| 42 | +import java.nio.charset.StandardCharsets; |
| 43 | +import java.util.HexFormat; |
| 44 | +import java.util.zip.*; |
| 45 | + |
| 46 | +import static org.junit.jupiter.api.Assertions.*; |
| 47 | + |
| 48 | +public class Zip64DataDescriptor { |
| 49 | + |
| 50 | + // A byte array holding a small-sized Zip64 ZIP file, described below |
| 51 | + private byte[] zip64File; |
| 52 | + |
| 53 | + // A byte array holding a ZIP used for testing invalid Zip64 extra fields |
| 54 | + private byte[] invalidZip64; |
| 55 | + |
| 56 | + @BeforeEach |
| 57 | + public void setup() throws IOException { |
| 58 | + /* |
| 59 | + * Structure of the ZIP64 file used below . Note the presence |
| 60 | + * of a Zip64 extended information extra field and the |
| 61 | + * Data Descriptor having 8-byte values for csize and size. |
| 62 | + * |
| 63 | + * The file was produced using the zip command on MacOS |
| 64 | + * (zip 3.0, by Info-ZIP), in streamming mode (to enable Zip64), |
| 65 | + * using the -fd option (to force the use of data descriptors) |
| 66 | + * |
| 67 | + * The following command was used: |
| 68 | + * <pre>echo hello | zip -fd > hello.zip</pre> |
| 69 | + * |
| 70 | + * ------ Local File Header ------ |
| 71 | + * 000000 signature 0x04034b50 |
| 72 | + * 000004 version 45 |
| 73 | + * 000006 flags 0x0008 |
| 74 | + * 000008 method 8 Deflated |
| 75 | + * 000010 time 0xb180 22:12 |
| 76 | + * 000012 date 0x565c 2023-02-28 |
| 77 | + * 000014 crc 0x00000000 |
| 78 | + * 000018 csize -1 |
| 79 | + * 000022 size -1 |
| 80 | + * 000026 nlen 1 |
| 81 | + * 000028 elen 20 |
| 82 | + * 000030 name 1 bytes '-' |
| 83 | + * 000031 ext id 0x0001 Zip64 extended information extra field |
| 84 | + * 000033 ext size 16 |
| 85 | + * 000035 z64 size 0 |
| 86 | + * 000043 z64 csize 0 |
| 87 | + * |
| 88 | + * ------ File Data ------ |
| 89 | + * 000051 data 8 bytes |
| 90 | + * |
| 91 | + * ------ Data Desciptor ------ |
| 92 | + * 000059 signature 0x08074b50 |
| 93 | + * 000063 crc 0x363a3020 |
| 94 | + * 000067 csize 8 |
| 95 | + * 000075 size 6 |
| 96 | + * 000083 ... |
| 97 | + */ |
| 98 | + |
| 99 | + String hex = """ |
| 100 | + 504b03042d000800080080b15c5600000000ffffffffffffffff01001400 |
| 101 | + 2d0100100000000000000000000000000000000000cb48cdc9c9e7020050 |
| 102 | + 4b070820303a3608000000000000000600000000000000504b01021e032d |
| 103 | + 000800080080b15c5620303a360800000006000000010000000000000001 |
| 104 | + 000000b011000000002d504b050600000000010001002f00000053000000 |
| 105 | + 0000"""; |
| 106 | + |
| 107 | + zip64File = HexFormat.of().parseHex(hex.replaceAll("\n", "")); |
| 108 | + |
| 109 | + // Create the ZIP file used for testing that invalid Zip64 extra fields are ignored |
| 110 | + // This ZIP has the regular 4-bit data descriptor |
| 111 | + |
| 112 | + byte[] extra = new byte[Long.BYTES + Long.BYTES + Short.BYTES * 2]; // Size of a regular Zip64 extra field |
| 113 | + ByteBuffer buffer = ByteBuffer.wrap(extra).order(ByteOrder.LITTLE_ENDIAN); |
| 114 | + buffer.putShort(0, (short) 123); // Not processed by ZipEntry.setExtra |
| 115 | + buffer.putShort(Short.BYTES, (short) (extra.length - 4)); |
| 116 | + |
| 117 | + ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| 118 | + try (ZipOutputStream zo = new ZipOutputStream(baos)) { |
| 119 | + ZipEntry ze = new ZipEntry("-"); |
| 120 | + ze.setExtra(extra); |
| 121 | + zo.putNextEntry(ze); |
| 122 | + zo.write("hello\n".getBytes(StandardCharsets.UTF_8)); |
| 123 | + } |
| 124 | + |
| 125 | + invalidZip64 = baos.toByteArray(); |
| 126 | + |
| 127 | + // Set Zip64 magic values on compressed and uncompressed size fields |
| 128 | + ByteBuffer.wrap(invalidZip64).order(ByteOrder.LITTLE_ENDIAN) |
| 129 | + .putInt(ZipFile.LOCSIZ, 0xFFFFFFFF) |
| 130 | + .putInt(ZipFile.LOCLEN, 0xFFFFFFFF); |
| 131 | + |
| 132 | + // Set the Zip64 header ID 0x1 on the extra field in the invalid file |
| 133 | + setExtraHeaderId((short) 0x1); |
| 134 | + } |
| 135 | + |
| 136 | + /* |
| 137 | + * Verify that small-sized Zip64 entries can be parsed by ZipInputStream |
| 138 | + */ |
| 139 | + @Test |
| 140 | + public void shouldReadZip64Descriptor() throws IOException { |
| 141 | + readZipInputStream(zip64File); |
| 142 | + } |
| 143 | + |
| 144 | + /* |
| 145 | + * For maximal backward compatibility when reading Zip64 descriptors, invalid |
| 146 | + * Zip64 extra data sizes should be ignored |
| 147 | + */ |
| 148 | + @Test |
| 149 | + public void shouldIgnoreInvalidExtraSize() throws IOException { |
| 150 | + setExtraSize((short) 42); |
| 151 | + readZipInputStream(invalidZip64); |
| 152 | + } |
| 153 | + |
| 154 | + /* |
| 155 | + * Files with Zip64 magic values but no Zip64 field should be ignored |
| 156 | + * when considering 8 byte data descriptors |
| 157 | + */ |
| 158 | + @Test |
| 159 | + public void shouldIgnoreNoZip64Header() throws IOException { |
| 160 | + setExtraSize((short) 123); |
| 161 | + readZipInputStream(invalidZip64); |
| 162 | + } |
| 163 | + |
| 164 | + /* |
| 165 | + * Theoretically, ZIP files may exist with ZIP64 format, but with 4-byte |
| 166 | + * data descriptors. Such files will fail to parse, as demonstrated by this test. |
| 167 | + */ |
| 168 | + @Test |
| 169 | + public void shouldFailParsingZip64With4ByteDataDescriptor() throws IOException { |
| 170 | + ZipException ex = assertThrows(ZipException.class, () -> { |
| 171 | + readZipInputStream(invalidZip64); |
| 172 | + }); |
| 173 | + |
| 174 | + String msg = String.format("Expected exeption message to contain 'invalid entry size', was %s", |
| 175 | + ex.getMessage()); |
| 176 | + assertTrue(ex.getMessage().contains("invalid entry size"), msg); |
| 177 | + } |
| 178 | + |
| 179 | + /* |
| 180 | + * Validate that an extra data size exceeding the length of the extra field is ignored |
| 181 | + */ |
| 182 | + @Test |
| 183 | + public void shouldIgnoreExcessiveExtraSize() throws IOException { |
| 184 | + |
| 185 | + setExtraSize(Short.MAX_VALUE); |
| 186 | + |
| 187 | + |
| 188 | + readZipInputStream(invalidZip64); |
| 189 | + } |
| 190 | + |
| 191 | + /* |
| 192 | + * Validate that the Data Descriptor is read with 32-bit fields if neither the |
| 193 | + * LOC's 'uncompressed size' or 'compressed size' fields have the Zip64 magic value, |
| 194 | + * even when there is a Zip64 field in the extra field. |
| 195 | + */ |
| 196 | + @Test |
| 197 | + public void shouldIgnoreNoMagicMarkers() throws IOException { |
| 198 | + // Set compressed and uncompressed size fields to zero |
| 199 | + ByteBuffer.wrap(invalidZip64).order(ByteOrder.LITTLE_ENDIAN) |
| 200 | + .putInt(ZipFile.LOCSIZ, 0) |
| 201 | + .putInt(ZipFile.LOCLEN, 0); |
| 202 | + |
| 203 | + |
| 204 | + readZipInputStream(invalidZip64); |
| 205 | + } |
| 206 | + |
| 207 | + /* |
| 208 | + * Validate that an extra data size exceeding the length of the extra field is ignored |
| 209 | + */ |
| 210 | + @Test |
| 211 | + public void shouldIgnoreTrucatedZip64Extra() throws IOException { |
| 212 | + |
| 213 | + truncateZip64(); |
| 214 | + |
| 215 | + readZipInputStream(invalidZip64); |
| 216 | + } |
| 217 | + |
| 218 | + /** |
| 219 | + * Update the Extra field header ID of the invalid file |
| 220 | + */ |
| 221 | + private void setExtraHeaderId(short id) { |
| 222 | + // Set the header ID on the extra field |
| 223 | + ByteBuffer buffer = ByteBuffer.wrap(invalidZip64).order(ByteOrder.LITTLE_ENDIAN); |
| 224 | + int nlen = buffer.getShort(ZipFile.LOCNAM); |
| 225 | + buffer.putShort(ZipFile.LOCHDR + nlen, id); |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * Updates the 16-bit 'data size' field of the Zip64 extended information field, |
| 230 | + * potentially to an invalid value. |
| 231 | + * @param size the value to set in the 'data size' field. |
| 232 | + */ |
| 233 | + private void setExtraSize(short size) { |
| 234 | + ByteBuffer buffer = ByteBuffer.wrap(invalidZip64).order(ByteOrder.LITTLE_ENDIAN); |
| 235 | + // Compute the offset to the Zip64 data block size field |
| 236 | + short nlen = buffer.getShort(ZipFile.LOCNAM); |
| 237 | + int dataSizeOffset = ZipFile.LOCHDR + nlen + Short.BYTES; |
| 238 | + buffer.putShort(dataSizeOffset, size); |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * Puts a truncated Zip64 field (just the tag) at the end of the LOC extra field. |
| 243 | + * The beginning of the extra field is filled with a generic extra field containing |
| 244 | + * just zeros. |
| 245 | + */ |
| 246 | + private void truncateZip64() { |
| 247 | + ByteBuffer buffer = ByteBuffer.wrap(invalidZip64).order(ByteOrder.LITTLE_ENDIAN); |
| 248 | + // Get the LOC name and extra sizes |
| 249 | + short nlen = buffer.getShort(ZipFile.LOCNAM); |
| 250 | + short elen = buffer.getShort(ZipFile.LOCEXT); |
| 251 | + int cenOffset = ZipFile.LOCHDR + nlen + elen; |
| 252 | + |
| 253 | + // Zero out the extra field |
| 254 | + int estart = ZipFile.LOCHDR + nlen; |
| 255 | + buffer.put(estart, new byte[elen]); |
| 256 | + // Put a generic extra field in the start |
| 257 | + buffer.putShort(estart, (short) 42); |
| 258 | + buffer.putShort(estart + Short.BYTES, (short) (elen - 4 - 2)); |
| 259 | + // Put a truncated (just the tag) Zip64 field at the end |
| 260 | + buffer.putShort(cenOffset - Short.BYTES, (short) 0x0001); |
| 261 | + } |
| 262 | + |
| 263 | + /* |
| 264 | + * Consume and verify the ZIP file using ZipInputStream |
| 265 | + */ |
| 266 | + private void readZipInputStream(byte[] zip) throws IOException { |
| 267 | + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zip))) { |
| 268 | + // Read the ZIP entry, this calls readLOC |
| 269 | + ZipEntry e = in.getNextEntry(); |
| 270 | + |
| 271 | + // Sanity check the zip entry |
| 272 | + assertNotNull(e, "Missing zip entry"); |
| 273 | + assertEquals("-", e.getName()); |
| 274 | + |
| 275 | + // Read the entry data, this causes readEND to parse the data descriptor |
| 276 | + assertEquals("hello\n", new String(in.readAllBytes(), StandardCharsets.UTF_8)); |
| 277 | + |
| 278 | + // There should only be a single zip entry |
| 279 | + assertNull(in.getNextEntry(), "Unexpected additional zip entry"); |
| 280 | + } |
| 281 | + } |
| 282 | +} |
0 commit comments