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.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  
25  import java.io.IOException;
26  import java.io.InputStream;
27  
28  import org.apache.commons.compress.AbstractTest;
29  import org.apache.commons.compress.archivers.ArchiveEntry;
30  import org.junit.jupiter.api.Test;
31  
32  /**
33   * JUnit test for a multi-volume ZIP file.
34   *
35   * Some tools (like 7-zip) allow users to split a large archives into 'volumes' with a given size to fit them into multiple cds, usb drives, or emails with an
36   * attachment size limit. It's basically the same file split into chunks of exactly 65536 bytes length. Concatenating volumes yields exactly the original file.
37   * There is no mechanism in the ZIP algorithm to accommodate for this. Before commons-compress used to enter an infinite loop on the last entry for such a file.
38   * This test is intended to prove that this error doesn't occur anymore. All entries but the last one are returned correctly, the last entry yields an
39   * exception.
40   */
41  class Maven221MultiVolumeTest extends AbstractTest {
42  
43      private static final String[] ENTRIES = {
44          // @formatter:off
45          "apache-maven-2.2.1/",
46          "apache-maven-2.2.1/LICENSE.txt",
47          "apache-maven-2.2.1/NOTICE.txt",
48          "apache-maven-2.2.1/README.txt",
49          "apache-maven-2.2.1/bin/",
50          "apache-maven-2.2.1/bin/m2.conf",
51          "apache-maven-2.2.1/bin/mvn",
52          "apache-maven-2.2.1/bin/mvn.bat",
53          "apache-maven-2.2.1/bin/mvnDebug",
54          "apache-maven-2.2.1/bin/mvnDebug.bat",
55          "apache-maven-2.2.1/boot/",
56          "apache-maven-2.2.1/boot/classworlds-1.1.jar",
57          "apache-maven-2.2.1/conf/",
58          "apache-maven-2.2.1/conf/settings.xml",
59          "apache-maven-2.2.1/lib/"
60          // @formatter:on
61      };
62  
63      private static final String LAST_ENTRY_NAME = "apache-maven-2.2.1/lib/maven-2.2.1-uber.jar";
64  
65      @Test
66      void testRead7ZipMultiVolumeArchiveForFile() {
67          assertThrows(IOException.class, () -> ZipFile.builder().setFile(getFile("apache-maven-2.2.1.zip.001")).get());
68      }
69  
70      @Test
71      void testRead7ZipMultiVolumeArchiveForStream() throws IOException {
72  
73          try (InputStream archive = newInputStream("apache-maven-2.2.1.zip.001");
74                  ZipArchiveInputStream zi = new ZipArchiveInputStream(archive, null, false)) {
75  
76              // these are the entries that are supposed to be processed
77              // correctly without any problems
78              for (final String element : ENTRIES) {
79                  assertEquals(element, zi.getNextEntry().getName());
80              }
81  
82              // this is the last entry that is truncated
83              final ArchiveEntry lastEntry = zi.getNextEntry();
84              assertEquals(LAST_ENTRY_NAME, lastEntry.getName());
85              final byte[] buffer = new byte[4096];
86  
87              // before the fix, we'd get 0 bytes on this read and all
88              // subsequent reads thus a client application might enter
89              // an infinite loop after the fix, we should get an
90              // exception
91              final IOException e1 = assertThrows(IOException.class, () -> {
92                  while (zi.read(buffer) > 0) {
93                      // empty
94                  }
95              }, "shouldn't be able to read from truncated entry");
96              assertEquals("Truncated ZIP file", e1.getMessage());
97  
98              final IOException e2 = assertThrows(IOException.class, () -> zi.read(buffer), "shouldn't be able to read from truncated entry after exception");
99              assertEquals("Truncated ZIP file", e2.getMessage());
100 
101             // and now we get another entry, which should also yield
102             // an exception
103             assertThrows(IOException.class, zi::getNextEntry, "shouldn't be able to read another entry from truncated file");
104         }
105     }
106 }