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  package org.apache.commons.compress.archivers.sevenz;
20  
21  import static java.nio.charset.StandardCharsets.UTF_16LE;
22  import static java.nio.charset.StandardCharsets.UTF_8;
23  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
24  import static org.junit.jupiter.api.Assertions.assertEquals;
25  import static org.junit.jupiter.api.Assertions.assertFalse;
26  import static org.junit.jupiter.api.Assertions.assertNotEquals;
27  import static org.junit.jupiter.api.Assertions.assertNotNull;
28  import static org.junit.jupiter.api.Assertions.assertNull;
29  import static org.junit.jupiter.api.Assertions.assertThrows;
30  import static org.junit.jupiter.api.Assertions.assertTrue;
31  
32  import java.io.ByteArrayOutputStream;
33  import java.io.File;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.nio.file.attribute.FileTime;
39  import java.security.NoSuchAlgorithmException;
40  import java.time.Instant;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.Collections;
44  import java.util.Date;
45  import java.util.HashMap;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Random;
50  import java.util.function.Function;
51  
52  import javax.crypto.Cipher;
53  
54  import org.apache.commons.compress.AbstractTest;
55  import org.apache.commons.compress.MemoryLimitException;
56  import org.apache.commons.compress.PasswordRequiredException;
57  import org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel;
58  import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
59  import org.apache.commons.io.IOUtils;
60  import org.junit.jupiter.api.Test;
61  
62  class SevenZFileTest extends AbstractTest {
63      private static final String TEST2_CONTENT = "<?xml version = '1.0'?>\r\n<!DOCTYPE connections>\r\n<meinxml>\r\n\t<leer />\r\n</meinxml>\n";
64  
65      private static boolean isStrongCryptoAvailable() throws NoSuchAlgorithmException {
66          return Cipher.getMaxAllowedKeyLength("AES/ECB/PKCS5Padding") >= 256;
67      }
68  
69      private void assertDate(final SevenZArchiveEntry entry, final String value, final Function<SevenZArchiveEntry, Boolean> hasValue,
70              final Function<SevenZArchiveEntry, FileTime> timeFunction, final Function<SevenZArchiveEntry, Date> dateFunction) {
71          if (value != null) {
72              assertTrue(hasValue.apply(entry));
73              final Instant parsedInstant = Instant.parse(value);
74              final FileTime parsedFileTime = FileTime.from(parsedInstant);
75              assertEquals(parsedFileTime, timeFunction.apply(entry));
76              assertEquals(Date.from(parsedInstant), dateFunction.apply(entry));
77          } else {
78              assertFalse(hasValue.apply(entry));
79              assertThrows(UnsupportedOperationException.class, () -> timeFunction.apply(entry));
80              assertThrows(UnsupportedOperationException.class, () -> dateFunction.apply(entry));
81          }
82      }
83  
84      private void assertDates(final SevenZArchiveEntry entry, final String modified, final String access, final String creation) {
85          assertDate(entry, modified, SevenZArchiveEntry::getHasLastModifiedDate, SevenZArchiveEntry::getLastModifiedTime,
86                  SevenZArchiveEntry::getLastModifiedDate);
87          assertDate(entry, access, SevenZArchiveEntry::getHasAccessDate, SevenZArchiveEntry::getAccessTime, SevenZArchiveEntry::getAccessDate);
88          assertDate(entry, creation, SevenZArchiveEntry::getHasCreationDate, SevenZArchiveEntry::getCreationTime, SevenZArchiveEntry::getCreationDate);
89      }
90  
91      private void checkHelloWorld(final String fileName) throws Exception {
92          try (SevenZFile sevenZFile = getSevenZFile(fileName)) {
93              final SevenZArchiveEntry entry = sevenZFile.getNextEntry();
94              assertEquals("Hello world.txt", entry.getName());
95              assertDates(entry, "2013-05-07T19:40:48Z", null, null);
96              final byte[] contents = new byte[(int) entry.getSize()];
97              int off = 0;
98              while (off < contents.length) {
99                  final int bytesRead = sevenZFile.read(contents, off, contents.length - off);
100                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
101                 off += bytesRead;
102             }
103             assertEquals("Hello, world!\n", new String(contents, UTF_8));
104             assertNull(sevenZFile.getNextEntry());
105         }
106     }
107 
108     private SevenZFile getSevenZFile(final String specialPath) throws IOException {
109         return SevenZFile.builder().setFile(getFile(specialPath)).get();
110     }
111 
112     private byte[] read(final SevenZFile sevenZFile, final SevenZArchiveEntry entry) throws IOException {
113         try (InputStream inputStream = sevenZFile.getInputStream(entry)) {
114             return IOUtils.toByteArray(inputStream);
115         }
116     }
117 
118     private byte[] readFully(final SevenZFile archive) throws IOException {
119         final byte[] buf = new byte[1024];
120         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
121         for (int len = 0; (len = archive.read(buf)) > 0;) {
122             baos.write(buf, 0, len);
123         }
124         return baos.toByteArray();
125     }
126 
127     @Test
128     void test7zDecryptUnarchive() throws Exception {
129         if (isStrongCryptoAvailable()) {
130             test7zUnarchive(getFile("bla.encrypted.7z"), SevenZMethod.LZMA, // stack LZMA + AES
131                     "foo".getBytes(UTF_16LE));
132         }
133     }
134 
135     @Test
136     void test7zDecryptUnarchiveUsingCharArrayPassword() throws Exception {
137         if (isStrongCryptoAvailable()) {
138             test7zUnarchive(getFile("bla.encrypted.7z"), SevenZMethod.LZMA, // stack LZMA + AES
139                     "foo".toCharArray());
140         }
141     }
142 
143     @Test
144     void test7zDeflate64Unarchive() throws Exception {
145         test7zUnarchive(getFile("bla.deflate64.7z"), SevenZMethod.DEFLATE64);
146     }
147 
148     @Test
149     void test7zDeflateUnarchive() throws Exception {
150         test7zUnarchive(getFile("bla.deflate.7z"), SevenZMethod.DEFLATE);
151     }
152 
153     @Test
154     void test7zMultiVolumeUnarchive() throws Exception {
155         try (@SuppressWarnings("deprecation")
156         SevenZFile sevenZFile = new SevenZFile(MultiReadOnlySeekableByteChannel.forFiles(getFile("bla-multi.7z.001"), getFile("bla-multi.7z.002")))) {
157             test7zUnarchive(sevenZFile, SevenZMethod.LZMA2);
158         }
159         try (SevenZFile sevenZFile = SevenZFile.builder()
160                 .setSeekableByteChannel(MultiReadOnlySeekableByteChannel.forFiles(getFile("bla-multi.7z.001"), getFile("bla-multi.7z.002"))).get()) {
161             test7zUnarchive(sevenZFile, SevenZMethod.LZMA2);
162         }
163     }
164 
165     @Test
166     void test7zUnarchive() throws Exception {
167         test7zUnarchive(getFile("bla.7z"), SevenZMethod.LZMA);
168     }
169 
170     private void test7zUnarchive(final File file, final SevenZMethod method) throws Exception {
171         test7zUnarchive(file, method, false);
172     }
173 
174     private void test7zUnarchive(final File file, final SevenZMethod method, final boolean tryToRecoverBrokenArchives) throws Exception {
175         test7zUnarchive(file, method, (char[]) null, tryToRecoverBrokenArchives);
176     }
177 
178     private void test7zUnarchive(final File file, final SevenZMethod method, final byte[] password) throws Exception {
179         try (@SuppressWarnings("deprecation")
180         SevenZFile sevenZFile = new SevenZFile(file, password)) {
181             test7zUnarchive(sevenZFile, method);
182         }
183         try (SevenZFile sevenZFile = SevenZFile.builder().setFile(file).setPassword(password).get()) {
184             test7zUnarchive(sevenZFile, method);
185         }
186     }
187 
188     private void test7zUnarchive(final File file, final SevenZMethod m, final char[] password) throws Exception {
189         test7zUnarchive(file, m, password, false);
190     }
191 
192     private void test7zUnarchive(final File file, final SevenZMethod m, final char[] password, final boolean tryToRecoverBrokenArchives) throws Exception {
193         try (@SuppressWarnings("deprecation")
194         SevenZFile sevenZFile = new SevenZFile(file, password,
195                 SevenZFileOptions.builder().withTryToRecoverBrokenArchives(tryToRecoverBrokenArchives).build())) {
196             test7zUnarchive(sevenZFile, m);
197         }
198         try (SevenZFile sevenZFile = SevenZFile.builder().setFile(file).setPassword(password).setTryToRecoverBrokenArchives(tryToRecoverBrokenArchives).get()) {
199             test7zUnarchive(sevenZFile, m);
200         }
201     }
202 
203     private void test7zUnarchive(final SevenZFile sevenZFile, final SevenZMethod m) throws Exception {
204         SevenZArchiveEntry entry = sevenZFile.getNextEntry();
205         assertEquals("test1.xml", entry.getName());
206         assertDates(entry, "2007-11-14T10:19:02Z", null, null);
207         assertEquals(m, entry.getContentMethods().iterator().next().getMethod());
208         entry = sevenZFile.getNextEntry();
209         assertEquals("test2.xml", entry.getName());
210         assertDates(entry, "2007-11-14T10:19:02Z", null, null);
211         assertEquals(m, entry.getContentMethods().iterator().next().getMethod());
212         final byte[] contents = new byte[(int) entry.getSize()];
213         int off = 0;
214         while (off < contents.length) {
215             final int bytesRead = sevenZFile.read(contents, off, contents.length - off);
216             assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
217             off += bytesRead;
218         }
219         assertEquals(TEST2_CONTENT, new String(contents, UTF_8));
220         assertNull(sevenZFile.getNextEntry());
221     }
222 
223     @Test
224     void test7zUnarchiveWithDefectHeader() throws Exception {
225         test7zUnarchive(getFile("bla.noendheaderoffset.7z"), SevenZMethod.LZMA, true);
226     }
227 
228     @Test
229     void test7zUnarchiveWithDefectHeaderFailsByDefault() throws Exception {
230         assertThrows(IOException.class, () -> test7zUnarchive(getFile("bla.noendheaderoffset.7z"), SevenZMethod.LZMA));
231     }
232 
233     @Test
234     void testAllEmptyFilesArchive() throws Exception {
235         try (SevenZFile archive = getSevenZFile("7z-empty-mhc-off.7z")) {
236             final SevenZArchiveEntry e = archive.getNextEntry();
237             assertNotNull(e);
238             assertEquals("empty", e.getName());
239             assertDates(e, "2013-05-14T17:50:19Z", null, null);
240             assertNull(archive.getNextEntry());
241         }
242     }
243 
244     /**
245      * @see "https://issues.apache.org/jira/browse/COMPRESS-256"
246      */
247     @Test
248     void testCompressedHeaderWithNonDefaultDictionarySize() throws Exception {
249         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
250             int count = 0;
251             while (sevenZFile.getNextEntry() != null) {
252                 count++;
253             }
254             assertEquals(446, count);
255         }
256     }
257 
258     @Test
259     void testEncryptedArchiveRequiresPassword() throws Exception {
260         final PasswordRequiredException ex = assertThrows(PasswordRequiredException.class, () -> getSevenZFile("bla.encrypted.7z").close(),
261                 "shouldn't decrypt without a password");
262         final String msg = ex.getMessage();
263         assertTrue(msg.startsWith("Cannot read encrypted content from "), "Should start with whining about being unable to decrypt");
264         assertTrue(msg.endsWith(" without a password."), "Should finish the sentence properly");
265         assertTrue(msg.contains("bla.encrypted.7z"), "Should contain archive's name");
266     }
267 
268     @Test
269     void testExtractNonExistSpecifiedFile() throws Exception {
270         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z");
271                 SevenZFile anotherSevenZFile = getSevenZFile("bla.7z")) {
272             for (final SevenZArchiveEntry nonExistEntry : anotherSevenZFile.getEntries()) {
273                 assertThrows(IllegalArgumentException.class, () -> sevenZFile.getInputStream(nonExistEntry));
274             }
275         }
276     }
277 
278     @Test
279     void testExtractSpecifiedFile() throws Exception {
280         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
281             final String testTxtContents = "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
282                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
283                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
284                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011";
285 
286             for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) {
287                 if (entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
288                     final byte[] contents = new byte[(int) entry.getSize()];
289                     int off = 0;
290                     final InputStream inputStream = sevenZFile.getInputStream(entry);
291                     while (off < contents.length) {
292                         final int bytesRead = inputStream.read(contents, off, contents.length - off);
293                         assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
294                         off += bytesRead;
295                     }
296                     assertEquals(testTxtContents, new String(contents, UTF_8));
297                     break;
298                 }
299             }
300         }
301     }
302 
303     @Test
304     void testExtractSpecifiedFileDeprecated() throws Exception {
305         try (@SuppressWarnings("deprecation")
306         SevenZFile sevenZFile = new SevenZFile(getFile("COMPRESS-256.7z"))) {
307             final String testTxtContents = "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
308                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
309                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
310                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011";
311 
312             for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) {
313                 if (entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
314                     final byte[] contents = new byte[(int) entry.getSize()];
315                     int off = 0;
316                     final InputStream inputStream = sevenZFile.getInputStream(entry);
317                     while (off < contents.length) {
318                         final int bytesRead = inputStream.read(contents, off, contents.length - off);
319                         assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
320                         off += bytesRead;
321                     }
322                     assertEquals(testTxtContents, new String(contents, UTF_8));
323                     break;
324                 }
325             }
326         }
327     }
328 
329     @Test
330     void testGetDefaultName() throws Exception {
331         try (SevenZFile sevenZFile = getSevenZFile("bla.deflate64.7z")) {
332             assertEquals("bla.deflate64", sevenZFile.getDefaultName());
333         }
334         try (SevenZFile sevenZFile = SevenZFile.builder().setSeekableByteChannel(Files.newByteChannel(getFile("bla.deflate64.7z").toPath())).get()) {
335             assertNull(sevenZFile.getDefaultName());
336         }
337         try (@SuppressWarnings("deprecation")
338         SevenZFile sevenZFile = new SevenZFile(Files.newByteChannel(getFile("bla.deflate64.7z").toPath()), "foo")) {
339             assertEquals("foo~", sevenZFile.getDefaultName());
340         }
341         try (SevenZFile sevenZFile = SevenZFile.builder().setSeekableByteChannel(Files.newByteChannel(getFile("bla.deflate64.7z").toPath()))
342                 .setDefaultName("foo").get()) {
343             assertEquals("foo~", sevenZFile.getDefaultName());
344         }
345         try (SevenZFile sevenZFile = SevenZFile.builder().setSeekableByteChannel(Files.newByteChannel(getFile("bla.deflate64.7z").toPath()))
346                 .setDefaultName(".foo").get()) {
347             assertEquals(".foo~", sevenZFile.getDefaultName());
348         }
349     }
350 
351     @Test
352     void testGetEntriesOfUnarchiveInMemoryTest() throws IOException {
353         final byte[] data = readAllBytes("bla.7z");
354         try (SevenZFile sevenZFile = SevenZFile.builder().setSeekableByteChannel(new SeekableInMemoryByteChannel(data)).get()) {
355             final Iterable<SevenZArchiveEntry> entries = sevenZFile.getEntries();
356             final Iterator<SevenZArchiveEntry> iter = entries.iterator();
357             SevenZArchiveEntry entry = iter.next();
358             assertEquals("test1.xml", entry.getName());
359             entry = iter.next();
360             assertEquals("test2.xml", entry.getName());
361             assertFalse(iter.hasNext());
362         }
363     }
364 
365     @Test
366     void testGetEntriesOfUnarchiveTest() throws IOException {
367         try (SevenZFile sevenZFile = getSevenZFile("bla.7z")) {
368             final Iterable<SevenZArchiveEntry> entries = sevenZFile.getEntries();
369             final Iterator<SevenZArchiveEntry> iter = entries.iterator();
370             SevenZArchiveEntry entry = iter.next();
371             assertEquals("test1.xml", entry.getName());
372             entry = iter.next();
373             assertEquals("test2.xml", entry.getName());
374             assertFalse(iter.hasNext());
375         }
376     }
377 
378     @Test
379     void testGivenNameWinsOverDefaultName() throws Exception {
380         try (@SuppressWarnings("deprecation")
381         SevenZFile sevenZFile = new SevenZFile(getFile("bla.7z"), SevenZFileOptions.builder().withUseDefaultNameForUnnamedEntries(true).build())) {
382             SevenZArchiveEntry ae = sevenZFile.getNextEntry();
383             assertNotNull(ae);
384             assertEquals("test1.xml", ae.getName());
385             ae = sevenZFile.getNextEntry();
386             assertNotNull(ae);
387             assertEquals("test2.xml", ae.getName());
388             assertNull(sevenZFile.getNextEntry());
389         }
390         try (SevenZFile sevenZFile = SevenZFile.builder().setFile(getFile("bla.7z")).setUseDefaultNameForUnnamedEntries(true).get()) {
391             SevenZArchiveEntry ae = sevenZFile.getNextEntry();
392             assertNotNull(ae);
393             assertEquals("test1.xml", ae.getName());
394             ae = sevenZFile.getNextEntry();
395             assertNotNull(ae);
396             assertEquals("test2.xml", ae.getName());
397             assertNull(sevenZFile.getNextEntry());
398         }
399     }
400 
401     /**
402      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-492">COMPRESS-492</a>
403      */
404     @Test
405     void testHandlesEmptyArchiveWithFilesInfo() throws Exception {
406         final File file = newTempFile("empty.7z");
407         try (SevenZOutputFile s = new SevenZOutputFile(file)) {
408             // do nothing
409         }
410         try (SevenZFile z = SevenZFile.builder().setFile(file).get()) {
411             assertFalse(z.getEntries().iterator().hasNext());
412             assertNull(z.getNextEntry());
413         }
414     }
415 
416     /**
417      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-492">COMPRESS-492</a>
418      */
419     @Test
420     void testHandlesEmptyArchiveWithoutFilesInfo() throws Exception {
421         try (SevenZFile z = getSevenZFile("COMPRESS-492.7z")) {
422             assertFalse(z.getEntries().iterator().hasNext());
423             assertNull(z.getNextEntry());
424         }
425     }
426 
427     @Test
428     void testHelloWorldHeaderCompressionOffCopy() throws Exception {
429         checkHelloWorld("7z-hello-mhc-off-copy.7z");
430     }
431 
432     @Test
433     void testHelloWorldHeaderCompressionOffLZMA2() throws Exception {
434         checkHelloWorld("7z-hello-mhc-off-lzma2.7z");
435     }
436 
437     @Test
438     void testLimitExtractionMemory() {
439         assertThrows(MemoryLimitException.class, () -> {
440             try (SevenZFile sevenZFile = SevenZFile.builder().setFile(getFile("bla.7z")).setMaxMemoryLimitKb(1).get()) {
441                 // Do nothing. Exception should be thrown
442             }
443         });
444         assertThrows(MemoryLimitException.class, () -> {
445             try (SevenZFile sevenZFile = SevenZFile.builder().setFile(getFile("bla.7z")).setMaxMemoryLimitKiB(1).get()) {
446                 // Do nothing. Exception should be thrown
447             }
448         });
449     }
450 
451     @Test
452     void testNoNameCanBeReplacedByDefaultName() throws Exception {
453         try (SevenZFile sevenZFile = SevenZFile.builder().setFile(getFile("bla-nonames.7z")).setUseDefaultNameForUnnamedEntries(true).get()) {
454             SevenZArchiveEntry ae = sevenZFile.getNextEntry();
455             assertNotNull(ae);
456             assertEquals("bla-nonames", ae.getName());
457             ae = sevenZFile.getNextEntry();
458             assertNotNull(ae);
459             assertEquals("bla-nonames", ae.getName());
460             assertNull(sevenZFile.getNextEntry());
461         }
462     }
463 
464     @Test
465     void testNoNameMeansNoNameByDefault() throws Exception {
466         try (SevenZFile sevenZFile = getSevenZFile("bla-nonames.7z")) {
467             SevenZArchiveEntry ae = sevenZFile.getNextEntry();
468             assertNotNull(ae);
469             assertNull(ae.getName());
470             ae = sevenZFile.getNextEntry();
471             assertNotNull(ae);
472             assertNull(ae.getName());
473             assertNull(sevenZFile.getNextEntry());
474         }
475     }
476 
477     @Test
478     void testNoOOMOnCorruptedHeader() throws IOException {
479         final List<Path> testFiles = new ArrayList<>();
480         testFiles.add(getPath("COMPRESS-542-1.7z"));
481         testFiles.add(getPath("COMPRESS-542-2.7z"));
482         testFiles.add(getPath("COMPRESS-542-endheadercorrupted.7z"));
483         testFiles.add(getPath("COMPRESS-542-endheadercorrupted2.7z"));
484         for (final Path file : testFiles) {
485             {
486                 final IOException e = assertThrows(IOException.class, () -> {
487                     try (@SuppressWarnings("deprecation")
488                     SevenZFile sevenZFile = new SevenZFile(Files.newByteChannel(file),
489                             SevenZFileOptions.builder().withTryToRecoverBrokenArchives(true).build())) {
490                         // do nothing
491                     }
492                 }, "Expected IOException: start header corrupt and unable to guess end header");
493                 assertEquals("Start header corrupt and unable to guess end header", e.getMessage());
494             }
495             {
496                 final IOException e = assertThrows(IOException.class, () -> {
497                     try (SevenZFile sevenZFile = SevenZFile.builder().setSeekableByteChannel(Files.newByteChannel(file)).setTryToRecoverBrokenArchives(true)
498                             .get()) {
499                         // do nothing
500                     }
501                 }, "Expected IOException: start header corrupt and unable to guess end header");
502                 assertEquals("Start header corrupt and unable to guess end header", e.getMessage());
503             }
504         }
505     }
506 
507     @Test
508     void testRandomAccessMultipleReadSameFile() throws Exception {
509         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
510             final String testTxtContents = "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
511                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
512                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
513                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011";
514 
515             SevenZArchiveEntry entry;
516             SevenZArchiveEntry testTxtEntry = null;
517             while ((entry = sevenZFile.getNextEntry()) != null) {
518                 if (entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
519                     testTxtEntry = entry;
520                     break;
521                 }
522             }
523 
524             assertNotNull(testTxtEntry, "testTxtEntry");
525             final byte[] contents = new byte[(int) testTxtEntry.getSize()];
526             int numberOfReads = 10;
527             while (numberOfReads-- > 0) {
528                 try (InputStream inputStream = sevenZFile.getInputStream(testTxtEntry)) {
529                     int off = 0;
530                     while (off < contents.length) {
531                         final int bytesRead = inputStream.read(contents, off, contents.length - off);
532                         assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
533                         off += bytesRead;
534                     }
535                     assertEquals(SevenZMethod.LZMA2, testTxtEntry.getContentMethods().iterator().next().getMethod());
536                     assertEquals(testTxtContents, new String(contents, UTF_8));
537                 }
538             }
539         }
540     }
541 
542     @Test
543     void testRandomAccessTogetherWithSequentialAccess() throws Exception {
544         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
545             // @formatter:off
546             final String testTxtContents = "111111111111111111111111111000101011\n"
547                     + "111111111111111111111111111000101011\n"
548                     + "111111111111111111111111111000101011\n"
549                     + "111111111111111111111111111000101011\n"
550                     + "111111111111111111111111111000101011\n"
551                     + "111111111111111111111111111000101011\n"
552                     + "111111111111111111111111111000101011\n"
553                     + "111111111111111111111111111000101011\n"
554                     + "111111111111111111111111111000101011\n"
555                     + "111111111111111111111111111000101011";
556             final String filesTxtContents = "0xxxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40xxxxxxxx50xxxxxxxx60xxxxxxxx70xxxxxxxx80xxxxxxxx90xxxxxxxx100"
557                     + "xxxxxxx110xxxxxxx120xxxxxxx130xxxxxxx -> 0yyyyyyyyy10yyyyyyyy20yyyyyyyy30yyyyyyyy40yyyyyyyy50yyyyyyyy60yyyyyyyy70yyyyyyyy80"
558                     + "yyyyyyyy90yyyyyyyy100yyyyyyy110yyyyyyy120yyyyyyy130yyyyyyy\n";
559             // @formatter:off
560             int off;
561             byte[] contents;
562 
563             // call getNextEntry and read before calling getInputStream
564             sevenZFile.getNextEntry();
565             SevenZArchiveEntry nextEntry = sevenZFile.getNextEntry();
566             contents = new byte[(int) nextEntry.getSize()];
567             off = 0;
568 
569             assertEquals(SevenZMethod.LZMA2, nextEntry.getContentMethods().iterator().next().getMethod());
570 
571             // just read them
572             while (off < contents.length) {
573                 final int bytesRead = sevenZFile.read(contents, off, contents.length - off);
574                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
575                 off += bytesRead;
576             }
577 
578             sevenZFile.getNextEntry();
579             sevenZFile.getNextEntry();
580 
581             for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) {
582                 // commons-compress-1.7-src/src/test/resources/test.txt
583                 if (entry.getName().equals("commons-compress-1.7-src/src/test/resources/longsymlink/files.txt")) {
584                     contents = new byte[(int) entry.getSize()];
585                     off = 0;
586                     final InputStream inputStream = sevenZFile.getInputStream(entry);
587                     while (off < contents.length) {
588                         final int bytesRead = inputStream.read(contents, off, contents.length - off);
589                         assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
590                         off += bytesRead;
591                     }
592                     assertEquals(SevenZMethod.LZMA2, entry.getContentMethods().iterator().next().getMethod());
593                     assertEquals(filesTxtContents, new String(contents, UTF_8));
594                     break;
595                 }
596             }
597 
598             // call getNextEntry after getInputStream
599             nextEntry = sevenZFile.getNextEntry();
600             while (!nextEntry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
601                 nextEntry = sevenZFile.getNextEntry();
602             }
603 
604             contents = new byte[(int) nextEntry.getSize()];
605             off = 0;
606             while (off < contents.length) {
607                 final int bytesRead = sevenZFile.read(contents, off, contents.length - off);
608                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
609                 off += bytesRead;
610             }
611             assertEquals(SevenZMethod.LZMA2, nextEntry.getContentMethods().iterator().next().getMethod());
612             assertEquals(testTxtContents, new String(contents, UTF_8));
613         }
614     }
615 
616     @Test
617     void testRandomAccessWhenJumpingBackwards() throws Exception {
618         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
619             final String testTxtContents = "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
620                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
621                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
622                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011";
623 
624             SevenZArchiveEntry entry;
625             SevenZArchiveEntry testTxtEntry = null;
626             while ((entry = sevenZFile.getNextEntry()) != null) {
627                 if (entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
628                     testTxtEntry = entry;
629                     break;
630                 }
631             }
632 
633             // read the next entry and jump back using random access
634             final SevenZArchiveEntry entryAfterTestTxtEntry = sevenZFile.getNextEntry();
635             final byte[] entryAfterTestTxtEntryContents = new byte[(int) entryAfterTestTxtEntry.getSize()];
636             int off = 0;
637             while (off < entryAfterTestTxtEntryContents.length) {
638                 final int bytesRead = sevenZFile.read(entryAfterTestTxtEntryContents, off, entryAfterTestTxtEntryContents.length - off);
639                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
640                 off += bytesRead;
641             }
642 
643             // jump backwards
644             assertNotNull(testTxtEntry, "testTxtEntry");
645             final byte[] contents = new byte[(int) testTxtEntry.getSize()];
646             try (InputStream inputStream = sevenZFile.getInputStream(testTxtEntry)) {
647                 off = 0;
648                 while (off < contents.length) {
649                     final int bytesRead = inputStream.read(contents, off, contents.length - off);
650                     assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
651                     off += bytesRead;
652                 }
653                 assertEquals(SevenZMethod.LZMA2, testTxtEntry.getContentMethods().iterator().next().getMethod());
654                 assertEquals(testTxtContents, new String(contents, UTF_8));
655             }
656 
657             // then read the next entry using getNextEntry
658             final SevenZArchiveEntry nextTestTxtEntry = sevenZFile.getNextEntry();
659             final byte[] nextTestContents = new byte[(int) nextTestTxtEntry.getSize()];
660             off = 0;
661             while (off < nextTestContents.length) {
662                 final int bytesRead = sevenZFile.read(nextTestContents, off, nextTestContents.length - off);
663                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
664                 off += bytesRead;
665             }
666 
667             assertEquals(nextTestTxtEntry.getName(), entryAfterTestTxtEntry.getName());
668             assertEquals(nextTestTxtEntry.getSize(), entryAfterTestTxtEntry.getSize());
669             assertArrayEquals(nextTestContents, entryAfterTestTxtEntryContents);
670         }
671     }
672 
673     @Test
674     void testRandomAccessWhenJumpingForwards() throws Exception {
675         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-256.7z")) {
676             final String testTxtContents = "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
677                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
678                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011\n"
679                     + "111111111111111111111111111000101011\n" + "111111111111111111111111111000101011";
680 
681             SevenZArchiveEntry testTxtEntry = null;
682             final Iterable<SevenZArchiveEntry> entries = sevenZFile.getEntries();
683             for (final SevenZArchiveEntry Entry : entries) {
684                 testTxtEntry = Entry;
685                 if (testTxtEntry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) {
686                     break;
687                 }
688             }
689             final SevenZArchiveEntry firstEntry = sevenZFile.getNextEntry();
690             // only read some of the data of the first entry
691             byte[] contents = new byte[(int) firstEntry.getSize() / 2];
692             sevenZFile.read(contents);
693 
694             // and the third entry
695             sevenZFile.getNextEntry();
696             final SevenZArchiveEntry thirdEntry = sevenZFile.getNextEntry();
697             contents = new byte[(int) thirdEntry.getSize() / 2];
698             sevenZFile.read(contents);
699 
700             // and then read a file after the first entry using random access
701             assertNotNull(testTxtEntry, "testTxtEntry");
702             contents = new byte[(int) testTxtEntry.getSize()];
703             int numberOfReads = 10;
704             while (numberOfReads-- > 0) {
705                 try (InputStream inputStream = sevenZFile.getInputStream(testTxtEntry)) {
706                     int off = 0;
707                     while (off < contents.length) {
708                         final int bytesRead = inputStream.read(contents, off, contents.length - off);
709                         assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
710                         off += bytesRead;
711                     }
712                     assertEquals(SevenZMethod.LZMA2, testTxtEntry.getContentMethods().iterator().next().getMethod());
713                     assertEquals(testTxtContents, new String(contents, UTF_8));
714                 }
715             }
716         }
717     }
718 
719     // https://issues.apache.org/jira/browse/COMPRESS-320
720     @Test
721     void testRandomlySkippingEntries() throws Exception {
722         // Read sequential reference.
723         final Map<String, byte[]> entriesByName = new HashMap<>();
724         try (SevenZFile archive = getSevenZFile("COMPRESS-320/Copy.7z")) {
725             SevenZArchiveEntry entry;
726             while ((entry = archive.getNextEntry()) != null) {
727                 if (entry.hasStream()) {
728                     entriesByName.put(entry.getName(), readFully(archive));
729                 }
730             }
731         }
732 
733         final String[] variants = { "BZip2-solid.7z", "BZip2.7z", "Copy-solid.7z", "Copy.7z", "Deflate-solid.7z", "Deflate.7z", "LZMA-solid.7z", "LZMA.7z",
734                 "LZMA2-solid.7z", "LZMA2.7z",
735                 // TODO: unsupported compression method.
736                 // "PPMd-solid.7z",
737                 // "PPMd.7z",
738         };
739 
740         // TODO: use randomized testing for predictable, but different, randomness.
741         final Random rnd = new Random(0xdeadbeef);
742         for (final String fileName : variants) {
743             try (SevenZFile archive = getSevenZFile("COMPRESS-320/" + fileName)) {
744 
745                 SevenZArchiveEntry entry;
746                 while ((entry = archive.getNextEntry()) != null) {
747                     // Sometimes skip reading entries.
748                     if (rnd.nextBoolean()) {
749                         continue;
750                     }
751 
752                     if (entry.hasStream()) {
753                         assertTrue(entriesByName.containsKey(entry.getName()));
754                         final byte[] content = readFully(archive);
755                         assertArrayEquals(content, entriesByName.get(entry.getName()), "Content mismatch on: " + fileName + "!" + entry.getName());
756                     }
757                 }
758 
759             }
760         }
761     }
762 
763     @Test
764     void testReadBigSevenZipFile() throws IOException {
765         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-592.7z")) {
766             SevenZArchiveEntry entry = sevenZFile.getNextEntry();
767             while (entry != null) {
768                 if (entry.hasStream()) {
769                     final byte[] content = new byte[(int) entry.getSize()];
770                     sevenZFile.read(content);
771                 }
772                 entry = sevenZFile.getNextEntry();
773             }
774         }
775     }
776 
777     /**
778      * @see "https://issues.apache.org/jira/browse/COMPRESS-348"
779      */
780     @Test
781     void testReadEntriesOfSize0() throws IOException {
782         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-348.7z")) {
783             int entries = 0;
784             SevenZArchiveEntry entry = sevenZFile.getNextEntry();
785             while (entry != null) {
786                 entries++;
787                 final int b = sevenZFile.read();
788                 if ("2.txt".equals(entry.getName()) || "5.txt".equals(entry.getName())) {
789                     assertEquals(-1, b);
790                 } else {
791                     assertNotEquals(-1, b);
792                 }
793                 entry = sevenZFile.getNextEntry();
794             }
795             assertEquals(5, entries);
796         }
797     }
798 
799     /**
800      * Test case for <a href="https://issues.apache.org/jira/browse/COMPRESS-681">COMPRESS-681</a>.
801      */
802     @Test
803     void testReadingArchiveProperties() throws IOException {
804         final String entryName = "COMPRESS-681.txt";
805         final String entryContent = "https://issues.apache.org/jira/browse/COMPRESS-681";
806         try (SevenZFile archive = getSevenZFile("COMPRESS-681.7z")) {
807             final SevenZArchiveEntry entry = archive.getNextEntry();
808             assertEquals(entryName, entry.getName());
809             final byte[] contents = new byte[(int) entry.getSize()];
810             int off = 0;
811             while (off < contents.length) {
812                 final int bytesRead = archive.read(contents, off, contents.length - off);
813                 assertTrue(bytesRead >= 0, "bytesRead exp: >=0, act: " + bytesRead);
814                 off += bytesRead;
815             }
816             assertEquals(entryContent, new String(contents, UTF_8));
817             assertNull(archive.getNextEntry());
818         }
819     }
820 
821     @Test
822     void testReadingBackDeltaDistance() throws Exception {
823         final File output = newTempFile("delta-distance.7z");
824         try (SevenZOutputFile outArchive = new SevenZOutputFile(output)) {
825             outArchive.setContentMethods(
826                     Arrays.asList(new SevenZMethodConfiguration(SevenZMethod.DELTA_FILTER, 32), new SevenZMethodConfiguration(SevenZMethod.LZMA2)));
827             final SevenZArchiveEntry entry = new SevenZArchiveEntry();
828             entry.setName("foo.txt");
829             outArchive.putArchiveEntry(entry);
830             outArchive.write(new byte[] { 'A' });
831             outArchive.closeArchiveEntry();
832         }
833 
834         try (SevenZFile archive = SevenZFile.builder().setFile(output).get()) {
835             final SevenZArchiveEntry entry = archive.getNextEntry();
836             final SevenZMethodConfiguration m = entry.getContentMethods().iterator().next();
837             assertEquals(SevenZMethod.DELTA_FILTER, m.getMethod());
838             assertEquals(32, m.getOptions());
839         }
840     }
841 
842     @Test
843     void testReadingBackLZMA2DictSize() throws Exception {
844         final File output = newTempFile("lzma2-dictsize.7z");
845         try (SevenZOutputFile outArchive = new SevenZOutputFile(output)) {
846             outArchive.setContentMethods(Arrays.asList(new SevenZMethodConfiguration(SevenZMethod.LZMA2, 1 << 20)));
847             final SevenZArchiveEntry entry = new SevenZArchiveEntry();
848             entry.setName("foo.txt");
849             outArchive.putArchiveEntry(entry);
850             outArchive.write(new byte[] { 'A' });
851             outArchive.closeArchiveEntry();
852         }
853 
854         try (SevenZFile archive = SevenZFile.builder().setFile(output).get()) {
855             final SevenZArchiveEntry entry = archive.getNextEntry();
856             final SevenZMethodConfiguration m = entry.getContentMethods().iterator().next();
857             assertEquals(SevenZMethod.LZMA2, m.getMethod());
858             assertEquals(1 << 20, m.getOptions());
859         }
860     }
861 
862     @Test
863     void testReadTimesFromFile() throws IOException {
864         try (SevenZFile sevenZFile = getSevenZFile("times.7z")) {
865             SevenZArchiveEntry entry = sevenZFile.getNextEntry();
866             assertNotNull(entry);
867             assertEquals("test", entry.getName());
868             assertTrue(entry.isDirectory());
869             assertDates(entry, "2022-03-21T14:50:46.2099751Z", "2022-03-21T14:50:46.2099751Z", "2022-03-16T10:19:24.1051115Z");
870 
871             entry = sevenZFile.getNextEntry();
872             assertNotNull(entry);
873             assertEquals("test/test-times.txt", entry.getName());
874             assertFalse(entry.isDirectory());
875             assertDates(entry, "2022-03-18T10:00:15Z", "2022-03-18T10:14:37.8130002Z", "2022-03-18T10:14:37.8110032Z");
876 
877             entry = sevenZFile.getNextEntry();
878             assertNotNull(entry);
879             assertEquals("test/test-times2.txt", entry.getName());
880             assertFalse(entry.isDirectory());
881             assertDates(entry, "2022-03-18T10:00:19Z", "2022-03-18T10:14:37.8170038Z", "2022-03-18T10:14:37.8140004Z");
882 
883             entry = sevenZFile.getNextEntry();
884             assertNull(entry);
885         }
886     }
887 
888     @Test
889     void testRetrieveInputStreamForAllEntriesMultipleTimes() throws IOException {
890         try (SevenZFile sevenZFile = getSevenZFile("bla.7z")) {
891             for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) {
892                 final byte[] firstRead = read(sevenZFile, entry);
893                 final byte[] secondRead = read(sevenZFile, entry);
894                 assertArrayEquals(firstRead, secondRead);
895             }
896         }
897     }
898 
899     @Test
900     void testRetrieveInputStreamForAllEntriesWithoutCRCMultipleTimes() throws IOException {
901         try (SevenZOutputFile out = new SevenZOutputFile(newTempFile("test.7z"))) {
902             final Path inputFile = Files.createTempFile("SevenZTestTemp", "");
903 
904             final SevenZArchiveEntry entry = out.createArchiveEntry(inputFile.toFile(), "test.txt");
905             out.putArchiveEntry(entry);
906             out.write("Test".getBytes(UTF_8));
907             out.closeArchiveEntry();
908 
909             Files.deleteIfExists(inputFile);
910         }
911 
912         try (SevenZFile sevenZFile = SevenZFile.builder().setFile(newTempFile("test.7z")).get()) {
913             for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) {
914                 final byte[] firstRead = read(sevenZFile, entry);
915                 final byte[] secondRead = read(sevenZFile, entry);
916                 assertArrayEquals(firstRead, secondRead);
917             }
918         }
919     }
920 
921     @Test
922     void testRetrieveInputStreamForShuffledEntries() throws IOException {
923         try (SevenZFile sevenZFile = getSevenZFile("COMPRESS-348.7z")) {
924             final List<SevenZArchiveEntry> entries = (List<SevenZArchiveEntry>) sevenZFile.getEntries();
925             Collections.shuffle(entries);
926             for (final SevenZArchiveEntry entry : entries) {
927                 read(sevenZFile, entry);
928             }
929         }
930     }
931 
932     @Test
933     void testSevenZWithEOS() throws IOException {
934         try (SevenZFile sevenZFile = getSevenZFile("lzma-with-eos.7z")) {
935             final List<SevenZArchiveEntry> entries = (List<SevenZArchiveEntry>) sevenZFile.getEntries();
936             for (final SevenZArchiveEntry entry : entries) {
937                 read(sevenZFile, entry);
938             }
939         }
940     }
941 
942     @Test
943     void testSignatureCheck() {
944         assertTrue(SevenZFile.matches(SevenZFile.sevenZSignature, SevenZFile.sevenZSignature.length));
945         assertTrue(SevenZFile.matches(SevenZFile.sevenZSignature, SevenZFile.sevenZSignature.length + 1));
946         assertFalse(SevenZFile.matches(SevenZFile.sevenZSignature, SevenZFile.sevenZSignature.length - 1));
947         assertFalse(SevenZFile.matches(new byte[] { 1, 2, 3, 4, 5, 6 }, 6));
948         assertTrue(SevenZFile.matches(new byte[] { '7', 'z', (byte) 0xBC, (byte) 0xAF, 0x27, 0x1C }, 6));
949         assertFalse(SevenZFile.matches(new byte[] { '7', 'z', (byte) 0xBC, (byte) 0xAF, 0x27, 0x1D }, 6));
950     }
951 }