View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.commons.compress.archivers.zip;
21  
22  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
23  import static org.junit.jupiter.api.Assertions.assertEquals;
24  import static org.junit.jupiter.api.Assertions.assertFalse;
25  import static org.junit.jupiter.api.Assertions.assertNotNull;
26  import static org.junit.jupiter.api.Assertions.assertNull;
27  import static org.junit.jupiter.api.Assertions.assertThrows;
28  import static org.junit.jupiter.api.Assertions.assertTrue;
29  
30  import java.io.BufferedInputStream;
31  import java.io.ByteArrayInputStream;
32  import java.io.ByteArrayOutputStream;
33  import java.io.EOFException;
34  import java.io.File;
35  import java.io.FileInputStream;
36  import java.io.FileOutputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.nio.channels.Channels;
41  import java.nio.channels.SeekableByteChannel;
42  import java.nio.charset.StandardCharsets;
43  import java.nio.file.Files;
44  import java.nio.file.Path;
45  import java.nio.file.Paths;
46  import java.time.Instant;
47  import java.util.zip.ZipEntry;
48  import java.util.zip.ZipException;
49  
50  import org.apache.commons.compress.AbstractTest;
51  import org.apache.commons.compress.archivers.ArchiveEntry;
52  import org.apache.commons.compress.archivers.ArchiveInputStream;
53  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
54  import org.apache.commons.compress.utils.ByteUtils;
55  import org.apache.commons.io.IOUtils;
56  import org.apache.commons.lang3.ArrayFill;
57  import org.junit.jupiter.api.Test;
58  import org.junit.jupiter.params.ParameterizedTest;
59  import org.junit.jupiter.params.provider.ValueSource;
60  
61  import io.airlift.compress.zstd.ZstdInputStream;
62  
63  class ZipArchiveInputStreamTest extends AbstractTest {
64  
65      private static final class AirliftZipArchiveInputStream extends ZipArchiveInputStream {
66  
67          private boolean used;
68  
69          private AirliftZipArchiveInputStream(final InputStream inputStream) {
70              super(inputStream);
71          }
72  
73          @Override
74          protected InputStream createZstdInputStream(final InputStream bis) throws IOException {
75              return new ZstdInputStream(bis) {
76                  @Override
77                  public int read(final byte[] outputBuffer, final int outputOffset, final int outputLength) throws IOException {
78                      used = true;
79                      return super.read(outputBuffer, outputOffset, outputLength);
80                  }
81              };
82          }
83  
84          public boolean isUsed() {
85              return used;
86          }
87      }
88  
89      private static void nameSource(final String archive, final String entry, int entryNo, final ZipArchiveEntry.NameSource expected) throws Exception {
90          try (ZipArchiveInputStream zis = new ZipArchiveInputStream(Files.newInputStream(getFile(archive).toPath()))) {
91              ZipArchiveEntry ze;
92              do {
93                  ze = zis.getNextZipEntry();
94              } while (--entryNo > 0);
95              assertEquals(entry, ze.getName());
96              assertEquals(expected, ze.getNameSource());
97          }
98      }
99  
100     private static void nameSource(final String archive, final String entry, final ZipArchiveEntry.NameSource expected) throws Exception {
101         nameSource(archive, entry, 1, expected);
102     }
103 
104     private static byte[] readEntry(final ZipArchiveInputStream zip, final ZipArchiveEntry zae) throws IOException {
105         final int len = (int) zae.getSize();
106         final byte[] buff = new byte[len];
107         zip.read(buff, 0, len);
108 
109         return buff;
110     }
111 
112     private void extractZipInputStream(final ZipArchiveInputStream inputStream) throws IOException {
113         ZipArchiveEntry zae = inputStream.getNextZipEntry();
114         while (zae != null) {
115             if (zae.getName().endsWith(".zip")) {
116                 try (ZipArchiveInputStream innerInputStream = new ZipArchiveInputStream(inputStream)) {
117                     extractZipInputStream(innerInputStream);
118                 }
119             }
120             zae = inputStream.getNextZipEntry();
121         }
122     }
123 
124     /**
125      * Forge a ZIP archive in memory, using STORED and Data Descriptor, and without signature of Data Descriptor.
126      *
127      * @return the input stream of the generated zip
128      * @throws IOException there are problems
129      */
130     private InputStream forgeZipInputStream() throws IOException {
131         try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
132                 ZipArchiveOutputStream zo = new ZipArchiveOutputStream(byteArrayOutputStream)) {
133 
134             final ZipArchiveEntry entryA = new ZipArchiveEntry("foo");
135             entryA.setMethod(ZipEntry.STORED);
136             entryA.setSize(4);
137             entryA.setCrc(0xb63cfbcdL);
138             // Ensure we won't write extra fields. They are not compatible with the manual edits below.
139             entryA.setTime(Instant.parse("2022-12-26T17:01:00Z").toEpochMilli());
140             zo.putArchiveEntry(entryA);
141             zo.write(new byte[] { 1, 2, 3, 4 });
142             zo.closeArchiveEntry();
143             zo.close();
144 
145             final byte[] zipContent = byteArrayOutputStream.toByteArray();
146             final byte[] zipContentWithDataDescriptor = new byte[zipContent.length + 12];
147             System.arraycopy(zipContent, 0, zipContentWithDataDescriptor, 0, 37);
148             // modify the general purpose bit flag
149             zipContentWithDataDescriptor[6] = 8;
150 
151             // copy the crc-32, compressed size and uncompressed size to the data descriptor
152             System.arraycopy(zipContent, 14, zipContentWithDataDescriptor, 37, 12);
153 
154             // and copy the rest of the ZIP content
155             System.arraycopy(zipContent, 37, zipContentWithDataDescriptor, 49, zipContent.length - 37);
156 
157             return new ByteArrayInputStream(zipContentWithDataDescriptor);
158         }
159     }
160 
161     private void fuzzingTest(final int[] bytes) throws Exception {
162         final int len = bytes.length;
163         final byte[] input = new byte[len];
164         for (int i = 0; i < len; i++) {
165             input[i] = (byte) bytes[i];
166         }
167         try (ArchiveInputStream<?> ais = ArchiveStreamFactory.DEFAULT.createArchiveInputStream("zip", new ByteArrayInputStream(input))) {
168             ais.getNextEntry();
169             IOUtils.toByteArray(ais);
170         }
171     }
172 
173     private void getAllZipEntries(final ZipArchiveInputStream zipInputStream) throws IOException {
174         while (zipInputStream.getNextZipEntry() != null) {
175             // noop
176         }
177     }
178 
179     private void multiByteReadConsistentlyReturnsMinusOneAtEof(final File file) throws Exception {
180         final byte[] buf = new byte[2];
181         try (InputStream in = newInputStream("bla.zip");
182                 ZipArchiveInputStream archive = new ZipArchiveInputStream(in)) {
183             assertEquals(-1, archive.getCompressedCount());
184             assertNotNull(archive.getNextEntry());
185             IOUtils.toByteArray(archive);
186             assertEquals(-1, archive.read(buf));
187             assertEquals(-1, archive.read(buf));
188         }
189     }
190 
191     private void singleByteReadConsistentlyReturnsMinusOneAtEof(final File file) throws Exception {
192         try (InputStream in = Files.newInputStream(file.toPath());
193                 ZipArchiveInputStream archive = new ZipArchiveInputStream(in)) {
194             assertNotNull(archive.getNextEntry());
195             IOUtils.toByteArray(archive);
196             assertEquals(-1, archive.read());
197             assertEquals(-1, archive.read());
198         }
199     }
200 
201     @Test
202     void testGetCompressedCountEmptyZip() throws IOException {
203         try (ZipArchiveInputStream zin = new ZipArchiveInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY))) {
204             assertEquals(-1, zin.getCompressedCount());
205         }
206     }
207 
208     @Test
209     void testGetFirstEntryEmptyZip() throws IOException {
210         try (ZipArchiveInputStream zin = new ZipArchiveInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY))) {
211             final ZipArchiveEntry entry = zin.getNextEntry();
212             assertNull(entry);
213         }
214     }
215 
216     @Test
217     void testGetUncompressedCountEmptyZip() throws IOException {
218         try (ZipArchiveInputStream zin = new ZipArchiveInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY))) {
219             assertEquals(0, zin.getUncompressedCount());
220         }
221     }
222 
223     /**
224      * Test case for <a href="https://issues.apache.org/jira/browse/COMPRESS-351">COMPRESS-351</a>.
225      */
226     @Test
227     void testMessageWithCorruptFileName() throws Exception {
228         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("COMPRESS-351.zip"))) {
229             final EOFException ex = assertThrows(EOFException.class, () -> {
230                 ZipArchiveEntry ze = in.getNextZipEntry();
231                 while (ze != null) {
232                     ze = in.getNextZipEntry();
233                 }
234             }, "expected EOFException");
235             final String m = ex.getMessage();
236             assertTrue(m.startsWith("Truncated ZIP entry: ?2016")); // the first character is not printable
237         }
238     }
239 
240     @Test
241     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingBzip2() throws Exception {
242         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("bzip2-zip.zip"));
243     }
244 
245     @Test
246     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingDeflate() throws Exception {
247         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("bla.zip"));
248     }
249 
250     @Test
251     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingDeflate64() throws Exception {
252         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("COMPRESS-380/COMPRESS-380.zip"));
253     }
254 
255     @Test
256     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingExplode() throws Exception {
257         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("imploding-8Kdict-3trees.zip"));
258     }
259 
260     @Test
261     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingStore() throws Exception {
262         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("COMPRESS-264.zip"));
263     }
264 
265     @Test
266     void testMultiByteReadConsistentlyReturnsMinusOneAtEofUsingUnshrink() throws Exception {
267         multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("SHRUNK.ZIP"));
268     }
269 
270     @Test
271     void testMultiByteReadThrowsAtEofForCorruptedStoredEntry() throws Exception {
272         final byte[] content = readAllBytes("COMPRESS-264.zip");
273         // make size much bigger than entry's real size
274         for (int i = 17; i < 26; i++) {
275             content[i] = (byte) 0xff;
276         }
277         final byte[] buf = new byte[2];
278         try (ByteArrayInputStream in = new ByteArrayInputStream(content);
279                 ZipArchiveInputStream archive = new ZipArchiveInputStream(in)) {
280             assertNotNull(archive.getNextEntry());
281             final IOException ex1 = assertThrows(IOException.class, () -> IOUtils.toByteArray(archive), "expected exception");
282             assertEquals("Truncated ZIP file", ex1.getMessage());
283             final IOException ex2 = assertThrows(IOException.class, () -> archive.read(buf), "expected exception");
284             assertEquals("Truncated ZIP file", ex2.getMessage());
285             final IOException ex3 = assertThrows(IOException.class, () -> archive.read(buf), "expected exception");
286             assertEquals("Truncated ZIP file", ex3.getMessage());
287         }
288     }
289 
290     @Test
291     void testNameSourceDefaultsToName() throws Exception {
292         nameSource("bla.zip", "test1.xml", ZipArchiveEntry.NameSource.NAME);
293     }
294 
295     @Test
296     void testNameSourceIsSetToEFS() throws Exception {
297         nameSource("utf8-7zip-test.zip", "\u20AC_for_Dollar.txt", 3, ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG);
298     }
299 
300     @Test
301     void testNameSourceIsSetToUnicodeExtraField() throws Exception {
302         nameSource("utf8-winzip-test.zip", "\u20AC_for_Dollar.txt", ZipArchiveEntry.NameSource.UNICODE_EXTRA_FIELD);
303     }
304 
305     /**
306      * Test correct population of header and data offsets.
307      */
308     @Test
309     void testOffsets() throws Exception {
310         // mixed.zip contains both inflated and stored files
311         try (InputStream archiveStream = ZipArchiveInputStream.class.getResourceAsStream("/mixed.zip");
312                 ZipArchiveInputStream zipStream = new ZipArchiveInputStream(archiveStream)) {
313             final ZipArchiveEntry inflatedEntry = zipStream.getNextZipEntry();
314             assertEquals("inflated.txt", inflatedEntry.getName());
315             assertEquals(0x0000, inflatedEntry.getLocalHeaderOffset());
316             assertEquals(0x0046, inflatedEntry.getDataOffset());
317             final ZipArchiveEntry storedEntry = zipStream.getNextZipEntry();
318             assertEquals("stored.txt", storedEntry.getName());
319             assertEquals(0x5892, storedEntry.getLocalHeaderOffset());
320             assertEquals(0x58d6, storedEntry.getDataOffset());
321             assertNull(zipStream.getNextZipEntry());
322         }
323     }
324 
325     @Test
326     void testProperlyMarksEntriesAsUnreadableIfUncompressedSizeIsUnknown() throws Exception {
327         // we never read any data
328         try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY))) {
329             final ZipArchiveEntry e = new ZipArchiveEntry("test");
330             e.setMethod(ZipMethod.DEFLATED.getCode());
331             assertTrue(zis.canReadEntryData(e));
332             e.setMethod(ZipMethod.ENHANCED_DEFLATED.getCode());
333             assertTrue(zis.canReadEntryData(e));
334             e.setMethod(ZipMethod.BZIP2.getCode());
335             assertFalse(zis.canReadEntryData(e));
336         }
337     }
338 
339     @Test
340     void testProperlyReadsStoredEntries() throws IOException {
341         try (InputStream fs = newInputStream("bla-stored.zip");
342                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs)) {
343             ZipArchiveEntry e = archive.getNextZipEntry();
344             assertNotNull(e);
345             assertEquals("test1.xml", e.getName());
346             assertEquals(610, e.getCompressedSize());
347             assertEquals(610, e.getSize());
348             byte[] data = IOUtils.toByteArray(archive);
349             assertEquals(610, data.length);
350             e = archive.getNextZipEntry();
351             assertNotNull(e);
352             assertEquals("test2.xml", e.getName());
353             assertEquals(82, e.getCompressedSize());
354             assertEquals(82, e.getSize());
355             data = IOUtils.toByteArray(archive);
356             assertEquals(82, data.length);
357             assertNull(archive.getNextEntry());
358         }
359     }
360 
361     @Test
362     void testProperlyReadsStoredEntryWithDataDescriptorWithoutSignature() throws IOException {
363         try (InputStream fs = newInputStream("bla-stored-dd-nosig.zip");
364                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs, StandardCharsets.UTF_8.name(), true, true)) {
365             final ZipArchiveEntry e = archive.getNextZipEntry();
366             assertNotNull(e);
367             assertEquals("test1.xml", e.getName());
368             assertEquals(-1, e.getCompressedSize());
369             assertEquals(-1, e.getSize());
370             final byte[] data = IOUtils.toByteArray(archive);
371             assertEquals(610, data.length);
372             assertEquals(610, e.getCompressedSize());
373             assertEquals(610, e.getSize());
374         }
375     }
376 
377     @Test
378     void testProperlyReadsStoredEntryWithDataDescriptorWithSignature() throws IOException {
379         try (InputStream fs = newInputStream("bla-stored-dd.zip");
380                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs, StandardCharsets.UTF_8.name(), true, true)) {
381             final ZipArchiveEntry e = archive.getNextZipEntry();
382             assertNotNull(e);
383             assertEquals("test1.xml", e.getName());
384             assertEquals(-1, e.getCompressedSize());
385             assertEquals(-1, e.getSize());
386             final byte[] data = IOUtils.toByteArray(archive);
387             assertEquals(610, data.length);
388             assertEquals(610, e.getCompressedSize());
389             assertEquals(610, e.getSize());
390         }
391     }
392 
393     /**
394      * @see "https://issues.apache.org/jira/browse/COMPRESS-189"
395      */
396     @Test
397     void testProperUseOfInflater() throws Exception {
398         try (ZipFile zf = ZipFile.builder().setFile(getFile("COMPRESS-189.zip")).get()) {
399             final ZipArchiveEntry zae = zf.getEntry("USD0558682-20080101.ZIP");
400             try (ZipArchiveInputStream in = new ZipArchiveInputStream(new BufferedInputStream(zf.getInputStream(zae)))) {
401                 ZipArchiveEntry innerEntry;
402                 while ((innerEntry = in.getNextZipEntry()) != null) {
403                     if (innerEntry.getName().endsWith("XML")) {
404                         assertTrue(0 < in.read());
405                     }
406                 }
407             }
408         }
409     }
410 
411     /**
412      * @see "https://issues.apache.org/jira/browse/COMPRESS-380"
413      */
414     @Test
415     void testReadDeflate64CompressedStream() throws Exception {
416         final byte[] orig = readAllBytes("COMPRESS-380/COMPRESS-380-input");
417         final File archive = getFile("COMPRESS-380/COMPRESS-380.zip");
418         try (ZipArchiveInputStream zin = new ZipArchiveInputStream(Files.newInputStream(archive.toPath()))) {
419             assertNotNull(zin.getNextZipEntry());
420             final byte[] fromZip = IOUtils.toByteArray(zin);
421             assertArrayEquals(orig, fromZip);
422         }
423     }
424 
425     @Test
426     void testReadDeflate64CompressedStreamWithDataDescriptor() throws Exception {
427         // this is a copy of bla.jar with META-INF/MANIFEST.MF's method manually changed to ENHANCED_DEFLATED
428         final File archive = getFile("COMPRESS-380/COMPRESS-380-dd.zip");
429         try (ZipArchiveInputStream zin = new ZipArchiveInputStream(Files.newInputStream(archive.toPath()))) {
430             final ZipArchiveEntry e = zin.getNextZipEntry();
431             assertEquals(-1, e.getSize());
432             assertEquals(ZipMethod.ENHANCED_DEFLATED.getCode(), e.getMethod());
433             final byte[] fromZip = IOUtils.toByteArray(zin);
434             final byte[] expected = { 'M', 'a', 'n', 'i', 'f', 'e', 's', 't', '-', 'V', 'e', 'r', 's', 'i', 'o', 'n', ':', ' ', '1', '.', '0', '\r', '\n', '\r',
435                     '\n' };
436             assertArrayEquals(expected, fromZip);
437             zin.getNextZipEntry();
438             assertEquals(25, e.getSize());
439         }
440     }
441 
442     /**
443      * Test case for <a href="https://issues.apache.org/jira/browse/COMPRESS-264">COMPRESS-264</a>.
444      */
445     @Test
446     void testReadingOfFirstStoredEntry() throws Exception {
447 
448         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("COMPRESS-264.zip"))) {
449             final ZipArchiveEntry ze = in.getNextZipEntry();
450             assertEquals(5, ze.getSize());
451             assertArrayEquals(new byte[] { 'd', 'a', 't', 'a', '\n' }, IOUtils.toByteArray(in));
452         }
453     }
454 
455     @Test
456     void testRejectsStoredEntriesWithDataDescriptorByDefault() throws IOException {
457         try (InputStream fs = newInputStream("bla-stored-dd.zip");
458                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs)) {
459             final ZipArchiveEntry e = archive.getNextZipEntry();
460             assertNotNull(e);
461             assertEquals("test1.xml", e.getName());
462             assertEquals(-1, e.getCompressedSize());
463             assertEquals(-1, e.getSize());
464             assertThrows(UnsupportedZipFeatureException.class, () -> IOUtils.toByteArray(archive));
465         }
466     }
467 
468     @Test
469     void testShouldConsumeArchiveCompletely() throws Exception {
470         try (InputStream is = ZipArchiveInputStreamTest.class.getResourceAsStream("/archive_with_trailer.zip");
471                 ZipArchiveInputStream zip = new ZipArchiveInputStream(is)) {
472             getAllZipEntries(zip);
473             final byte[] expected = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n' };
474             final byte[] actual = new byte[expected.length];
475             is.read(actual);
476             assertArrayEquals(expected, actual);
477         }
478     }
479 
480     /**
481      * @see "https://issues.apache.org/jira/browse/COMPRESS-219"
482      */
483     @Test
484     void testShouldReadNestedZip() throws IOException {
485         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("COMPRESS-219.zip"))) {
486             extractZipInputStream(in);
487         }
488     }
489 
490     @Test
491     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingBzip2() throws Exception {
492         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("bzip2-zip.zip"));
493     }
494 
495     @Test
496     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingDeflate() throws Exception {
497         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("bla.zip"));
498     }
499 
500     @Test
501     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingDeflate64() throws Exception {
502         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("COMPRESS-380/COMPRESS-380.zip"));
503     }
504 
505     @Test
506     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingExplode() throws Exception {
507         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("imploding-8Kdict-3trees.zip"));
508     }
509 
510     @Test
511     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingStore() throws Exception {
512         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("COMPRESS-264.zip"));
513     }
514 
515     @Test
516     void testSingleByteReadConsistentlyReturnsMinusOneAtEofUsingUnshrink() throws Exception {
517         singleByteReadConsistentlyReturnsMinusOneAtEof(getFile("SHRUNK.ZIP"));
518     }
519 
520     @Test
521     void testSingleByteReadThrowsAtEofForCorruptedStoredEntry() throws Exception {
522         final byte[] content = readAllBytes("COMPRESS-264.zip");
523         // make size much bigger than entry's real size
524         for (int i = 17; i < 26; i++) {
525             content[i] = (byte) 0xff;
526         }
527         try (ByteArrayInputStream in = new ByteArrayInputStream(content);
528                 ZipArchiveInputStream archive = new ZipArchiveInputStream(in)) {
529             assertNotNull(archive.getNextEntry());
530             final IOException ex1 = assertThrows(IOException.class, () -> IOUtils.toByteArray(archive), "expected exception");
531             assertEquals("Truncated ZIP file", ex1.getMessage());
532             final IOException ex2 = assertThrows(IOException.class, archive::read, "expected exception");
533             assertEquals("Truncated ZIP file", ex2.getMessage());
534             final IOException ex3 = assertThrows(IOException.class, archive::read, "expected exception");
535             assertEquals("Truncated ZIP file", ex3.getMessage());
536         }
537     }
538 
539     @Test
540     void testSplitZipCreatedByWinrar() throws IOException {
541         final File lastFile = getFile("COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.zip");
542         try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile);
543                 InputStream inputStream = Channels.newInputStream(channel);
544                 ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, StandardCharsets.UTF_8.name(), true, false, true)) {
545 
546             final File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_winrar/zip_to_compare_created_by_winrar.zip");
547             try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(Files.newInputStream(fileToCompare.toPath()),
548                     StandardCharsets.UTF_8.name(), true, false, true)) {
549 
550                 ArchiveEntry entry;
551                 while ((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) {
552                     if (entry.isDirectory()) {
553                         continue;
554                     }
555                     assertArrayEquals(IOUtils.toByteArray(splitInputStream), IOUtils.toByteArray(inputStreamToCompare));
556                 }
557             }
558         }
559     }
560 
561     @Test
562     void testSplitZipCreatedByZip() throws IOException {
563         final File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip");
564         try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile);
565                 InputStream inputStream = Channels.newInputStream(channel);
566                 ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, StandardCharsets.UTF_8.name(), true, false, true)) {
567 
568             final Path fileToCompare = getPath("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip");
569             try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(Files.newInputStream(fileToCompare), StandardCharsets.UTF_8.name(),
570                     true, false, true)) {
571 
572                 ArchiveEntry entry;
573                 while ((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) {
574                     if (entry.isDirectory()) {
575                         continue;
576                     }
577                     assertArrayEquals(IOUtils.toByteArray(splitInputStream), IOUtils.toByteArray(inputStreamToCompare));
578                 }
579             }
580         }
581     }
582 
583     @Test
584     void testSplitZipCreatedByZipOfZip64() throws IOException {
585         final File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.zip");
586         try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile);
587                 InputStream inputStream = Channels.newInputStream(channel);
588                 ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, StandardCharsets.UTF_8.name(), true, false, true)) {
589 
590             final Path fileToCompare = getPath("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip_zip64.zip");
591             try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(Files.newInputStream(fileToCompare), StandardCharsets.UTF_8.name(),
592                     true, false, true)) {
593 
594                 ArchiveEntry entry;
595                 while ((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) {
596                     if (entry.isDirectory()) {
597                         continue;
598                     }
599                     assertArrayEquals(IOUtils.toByteArray(splitInputStream), IOUtils.toByteArray(inputStreamToCompare));
600                 }
601             }
602         }
603     }
604 
605     @Test
606     void testSplitZipCreatedByZipThrowsException() throws IOException {
607         final File zipSplitFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01");
608         try (ZipArchiveInputStream inputStream = new ZipArchiveInputStream(Files.newInputStream(zipSplitFile.toPath()), StandardCharsets.UTF_8.name(), true,
609                 false, true)) {
610 
611             assertThrows(EOFException.class, () -> {
612                 ArchiveEntry entry = inputStream.getNextEntry();
613                 while (entry != null) {
614                     entry = inputStream.getNextEntry();
615                 }
616             });
617         }
618     }
619 
620     /**
621      * {@code getNextZipEntry()} should throw a {@code ZipException} rather than return {@code null} when an unexpected structure is encountered.
622      */
623     @Test
624     void testThrowOnInvalidEntry() throws Exception {
625         try (ZipArchiveInputStream zip = new ZipArchiveInputStream(ZipArchiveInputStreamTest.class.getResourceAsStream("/invalid-zip.zip"))) {
626             final ZipException expected = assertThrows(ZipException.class, zip::getNextZipEntry, "IOException expected");
627             assertTrue(expected.getMessage().contains("Cannot find zip signature"));
628         }
629     }
630 
631     @Test
632     void testThrowsIfStoredDDIsDifferentFromLengthRead() throws IOException {
633         try (InputStream fs = newInputStream("bla-stored-dd-contradicts-actualsize.zip");
634                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs, StandardCharsets.UTF_8.name(), true, true)) {
635             final ZipArchiveEntry e = archive.getNextZipEntry();
636             assertNotNull(e);
637             assertEquals("test1.xml", e.getName());
638             assertEquals(-1, e.getCompressedSize());
639             assertEquals(-1, e.getSize());
640             assertThrows(ZipException.class, () -> IOUtils.toByteArray(archive));
641         }
642     }
643 
644     @Test
645     void testThrowsIfStoredDDIsInconsistent() throws IOException {
646         try (InputStream fs = newInputStream("bla-stored-dd-sizes-differ.zip");
647                 ZipArchiveInputStream archive = new ZipArchiveInputStream(fs, StandardCharsets.UTF_8.name(), true, true)) {
648             final ZipArchiveEntry e = archive.getNextZipEntry();
649             assertNotNull(e);
650             assertEquals("test1.xml", e.getName());
651             assertEquals(-1, e.getCompressedSize());
652             assertEquals(-1, e.getSize());
653             assertThrows(ZipException.class, () -> IOUtils.toByteArray(archive));
654         }
655     }
656 
657     /**
658      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-523">COMPRESS-523</a>
659      */
660     @Test
661     void testThrowsIfThereIsNoEocd() {
662         assertThrows(IOException.class, () -> fuzzingTest(new int[] { 0x50, 0x4b, 0x01, 0x02, 0x14, 0x00, 0x14, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
663                 0x00, 0x43, 0xbe, 0x00, 0x00, 0x00, 0xb7, 0xe8, 0x07, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 }));
664     }
665 
666     /**
667      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-518">COMPRESS-518</a>
668      */
669     @Test
670     void testThrowsIfZip64ExtraCouldNotBeUnderstood() {
671         assertThrows(IOException.class,
672                 () -> fuzzingTest(new int[] { 0x50, 0x4b, 0x03, 0x04, 0x2e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x84, 0xb6, 0xba, 0x46, 0x72, 0xb6, 0xfe, 0x77, 0x63,
673                         0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x1c, 0x00, 0x62, 0x62, 0x62, 0x01, 0x00, 0x09, 0x00, 0x03, 0xe7, 0xce, 0x64,
674                         0x55, 0xf3, 0xce, 0x64, 0x55, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0x5c, 0xf9, 0x01, 0x00, 0x04, 0x88, 0x13, 0x00, 0x00 }));
675     }
676 
677     @Test
678     void testThrowsIOExceptionIfThereIsCorruptedZip64Extra() throws IOException {
679         try (InputStream fis = newInputStream("COMPRESS-546.zip");
680                 ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(fis)) {
681             assertThrows(IOException.class, () -> getAllZipEntries(zipInputStream));
682         }
683     }
684 
685     @Test
686     void testUnshrinkEntry() throws Exception {
687         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("SHRUNK.ZIP"))) {
688             ZipArchiveEntry entry = in.getNextZipEntry();
689             assertEquals(ZipMethod.UNSHRINKING.getCode(), entry.getMethod(), "method");
690             assertTrue(in.canReadEntryData(entry));
691 
692             try (InputStream original = newInputStream("test1.xml")) {
693                 try {
694                     assertArrayEquals(IOUtils.toByteArray(original), IOUtils.toByteArray(in));
695                 } finally {
696                     original.close();
697                 }
698 
699                 entry = in.getNextZipEntry();
700                 assertEquals(ZipMethod.UNSHRINKING.getCode(), entry.getMethod(), "method");
701                 assertTrue(in.canReadEntryData(entry));
702             }
703 
704             assertArrayEquals(readAllBytes("test2.xml"), IOUtils.toByteArray(in));
705         }
706     }
707 
708     @Test
709     void testUnzipBZip2CompressedEntry() throws Exception {
710 
711         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("bzip2-zip.zip"))) {
712             final ZipArchiveEntry ze = in.getNextZipEntry();
713             assertEquals(42, ze.getSize());
714             final byte[] expected = ArrayFill.fill(new byte[42], (byte) 'a');
715             assertArrayEquals(expected, IOUtils.toByteArray(in));
716         }
717     }
718 
719     /**
720      * @see "https://issues.apache.org/jira/browse/COMPRESS-176"
721      */
722     @Test
723     void testWinzipBackSlashWorkaround() throws Exception {
724         try (ZipArchiveInputStream in = new ZipArchiveInputStream(newInputStream("test-winzip.zip"))) {
725             ZipArchiveEntry zae = in.getNextZipEntry();
726             zae = in.getNextZipEntry();
727             zae = in.getNextZipEntry();
728             assertEquals("\u00e4/", zae.getName());
729         }
730     }
731 
732     /**
733      * Test case for <a href="https://issues.apache.org/jira/browse/COMPRESS-364">COMPRESS-364</a>.
734      */
735     @Test
736     void testWithBytesAfterData() throws Exception {
737         final int expectedNumEntries = 2;
738         try (InputStream is = ZipArchiveInputStreamTest.class.getResourceAsStream("/archive_with_bytes_after_data.zip");
739                 ZipArchiveInputStream zip = new ZipArchiveInputStream(is)) {
740             int actualNumEntries = 0;
741             ZipArchiveEntry zae = zip.getNextZipEntry();
742             while (zae != null) {
743                 actualNumEntries++;
744                 readEntry(zip, zae);
745                 zae = zip.getNextZipEntry();
746             }
747             assertEquals(expectedNumEntries, actualNumEntries);
748         }
749     }
750 
751     /**
752      * Tests COMPRESS-689.
753      */
754     @Test
755     void testWriteZipWithLinks() throws IOException {
756         try (OutputStream output = new FileOutputStream("target/zipWithLinks.zip");
757                 ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(output)) {
758             zipOutputStream.putArchiveEntry(new ZipArchiveEntry("original"));
759             zipOutputStream.write("original content".getBytes());
760             zipOutputStream.closeArchiveEntry();
761             final ZipArchiveEntry entry = new ZipArchiveEntry("link");
762             entry.setUnixMode(UnixStat.LINK_FLAG | 0444);
763             assertEquals(ZipArchiveEntry.PLATFORM_UNIX, entry.getPlatform());
764             assertTrue(entry.isUnixSymlink());
765             zipOutputStream.putArchiveEntry(entry);
766             zipOutputStream.write("original".getBytes());
767             zipOutputStream.closeArchiveEntry();
768         }
769         // Reads the central directory
770         try (ZipFile zipFile = ZipFile.builder().setFile("target/zipWithLinks.zip").get()) {
771             assertTrue(zipFile.getEntry("link").isUnixSymlink(), "'link' detected but it's not sym link");
772             assertFalse(zipFile.getEntry("original").isUnixSymlink(), "'original' detected but it's not sym link");
773         }
774         // Doesn't reads the central directory
775         try (ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(new FileInputStream("target/zipWithLinks.zip"))) {
776             ZipArchiveEntry entry;
777             int entriesCount = 0;
778             while ((entry = zipInputStream.getNextEntry()) != null) {
779                 if ("link".equals(entry.getName())) {
780                     // This information is only set in the central directory
781                     // assertTrue(entry.isUnixSymlink(), "'link' detected but it's not sym link");
782                 } else {
783                     assertFalse(entry.isUnixSymlink(), "'original' detected but it's sym link and should be regular file");
784                 }
785                 entriesCount++;
786             }
787             assertEquals(2, entriesCount);
788         }
789     }
790 
791     @Test
792     void testZipArchiveInputStreamSubclassReplacement() throws IOException {
793         try (InputStream fs = newInputStream("COMPRESS-692/compress-692.zip");
794                 AirliftZipArchiveInputStream archive = new AirliftZipArchiveInputStream(fs)) {
795             assertFalse(archive.isUsed());
796             ZipArchiveEntry e = archive.getNextEntry();
797             assertNotNull(e);
798             assertEquals(ZipMethod.ZSTD.getCode(), e.getMethod());
799             assertEquals("dolor.txt", e.getName());
800             assertEquals(635, e.getCompressedSize());
801             assertEquals(6066, e.getSize());
802             byte[] data = IOUtils.toByteArray(archive);
803             assertEquals(6066, data.length);
804             assertTrue(archive.isUsed());
805             e = archive.getNextEntry();
806             assertNotNull(e);
807             assertEquals(ZipMethod.ZSTD.getCode(), e.getMethod());
808             assertEquals("ipsum.txt", e.getName());
809             assertEquals(636, e.getCompressedSize());
810             assertEquals(6072, e.getSize());
811             data = IOUtils.toByteArray(archive);
812             assertEquals(6072, data.length);
813             assertNotNull(archive.getNextEntry());
814         }
815     }
816 
817     @ParameterizedTest
818     @ValueSource(booleans = { true, false })
819     void testZipInputStream(final boolean allowStoredEntriesWithDataDescriptor) {
820         try (ZipArchiveInputStream zIn = new ZipArchiveInputStream(Files.newInputStream(Paths.get("src/test/resources/COMPRESS-647/test.zip")),
821                 StandardCharsets.UTF_8.name(), false, allowStoredEntriesWithDataDescriptor)) {
822             ZipArchiveEntry zae = zIn.getNextEntry();
823             while (zae != null) {
824                 zae = zIn.getNextEntry();
825             }
826         } catch (final IOException e) {
827             // Ignore expected exception
828         }
829     }
830 
831     @Test
832     void testZipUsingStoredWithDDAndNoDDSignature() throws IOException {
833         try (InputStream inputStream = forgeZipInputStream();
834                 ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(inputStream, StandardCharsets.UTF_8.name(), true, true)) {
835             getAllZipEntries(zipInputStream);
836         }
837     }
838 
839     @Test
840     void testZipWithBadExtraFields() throws IOException {
841         try (InputStream fis = newInputStream("COMPRESS-548.zip");
842                 ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream(fis)) {
843             getAllZipEntries(zipInputStream);
844         }
845     }
846 
847     @Test
848     void testZipWithLongerBeginningGarbage() throws IOException {
849         final Path path = createTempPath("preamble", ".zip");
850 
851         try (OutputStream fos = Files.newOutputStream(path)) {
852             fos.write("#!/usr/bin/env some-program with quite a few arguments to make it longer than the local header\n".getBytes(StandardCharsets.UTF_8));
853             try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos)) {
854                 final ZipArchiveEntry entry = new ZipArchiveEntry("file-1.txt");
855                 entry.setMethod(ZipEntry.DEFLATED);
856                 zos.putArchiveEntry(entry);
857                 zos.writeUtf8("entry-content\n");
858                 zos.closeArchiveEntry();
859             }
860         }
861 
862         try (InputStream is = Files.newInputStream(path);
863                 ZipArchiveInputStream zis = new ZipArchiveInputStream(is)) {
864             final ZipArchiveEntry entry = zis.getNextEntry();
865             assertEquals("file-1.txt", entry.getName());
866             final byte[] content = IOUtils.toByteArray(zis);
867             assertArrayEquals("entry-content\n".getBytes(StandardCharsets.UTF_8), content);
868         }
869     }
870 
871     @Test
872     void testZipWithShortBeginningGarbage() throws IOException {
873         final Path path = createTempPath("preamble", ".zip");
874 
875         try (OutputStream fos = Files.newOutputStream(path)) {
876             fos.write("#!/usr/bin/unzip\n".getBytes(StandardCharsets.UTF_8));
877             try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos)) {
878                 final ZipArchiveEntry entry = new ZipArchiveEntry("file-1.txt");
879                 entry.setMethod(ZipEntry.DEFLATED);
880                 zos.putArchiveEntry(entry);
881                 zos.writeUtf8("entry-content\n");
882                 zos.closeArchiveEntry();
883             }
884         }
885 
886         try (InputStream is = Files.newInputStream(path);
887                 ZipArchiveInputStream zis = new ZipArchiveInputStream(is)) {
888             final ZipArchiveEntry entry = zis.getNextEntry();
889             assertEquals("file-1.txt", entry.getName());
890             final byte[] content = IOUtils.toByteArray(zis);
891             assertArrayEquals("entry-content\n".getBytes(StandardCharsets.UTF_8), content);
892         }
893     }
894 }