View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.compress.archivers.sevenz;
18  
19  import static java.nio.charset.StandardCharsets.UTF_16LE;
20  
21  import java.io.BufferedInputStream;
22  import java.io.ByteArrayInputStream;
23  import java.io.Closeable;
24  import java.io.DataInputStream;
25  import java.io.EOFException;
26  import java.io.File;
27  import java.io.FilterInputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.nio.ByteBuffer;
31  import java.nio.ByteOrder;
32  import java.nio.channels.Channels;
33  import java.nio.channels.SeekableByteChannel;
34  import java.nio.file.Files;
35  import java.nio.file.OpenOption;
36  import java.nio.file.Path;
37  import java.nio.file.StandardOpenOption;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.BitSet;
41  import java.util.EnumSet;
42  import java.util.LinkedHashMap;
43  import java.util.LinkedList;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Objects;
47  import java.util.zip.CRC32;
48  import java.util.zip.CheckedInputStream;
49  
50  import org.apache.commons.compress.MemoryLimitException;
51  import org.apache.commons.compress.utils.ByteUtils;
52  import org.apache.commons.compress.utils.IOUtils;
53  import org.apache.commons.compress.utils.InputStreamStatistics;
54  import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
55  import org.apache.commons.io.build.AbstractOrigin.ByteArrayOrigin;
56  import org.apache.commons.io.build.AbstractStreamBuilder;
57  import org.apache.commons.io.input.BoundedInputStream;
58  import org.apache.commons.io.input.ChecksumInputStream;
59  
60  /**
61   * Reads a 7z file, using SeekableByteChannel under the covers.
62   * <p>
63   * The 7z file format is a flexible container that can contain many compression and encryption types, but at the moment only only Copy, LZMA, LZMA2, BZIP2,
64   * Deflate and AES-256 + SHA-256 are supported.
65   * </p>
66   * <p>
67   * The format is very Windows/Intel specific, so it uses little-endian byte order, doesn't store user/group or permission bits, and represents times using NTFS
68   * timestamps (100 nanosecond units since 1 January 1601). Hence the official tools recommend against using it for backup purposes on *nix, and recommend
69   * .tar.7z or .tar.lzma or .tar.xz instead.
70   * </p>
71   * <p>
72   * Both the header and file contents may be compressed and/or encrypted. With both encrypted, neither file names nor file contents can be read, but the use of
73   * encryption isn't plausibly deniable.
74   * </p>
75   * <p>
76   * Multi volume archives can be read by concatenating the parts in correct order - either manually or by using {link
77   * org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel} for example.
78   * </p>
79   *
80   * @NotThreadSafe
81   * @since 1.6
82   */
83  public class SevenZFile implements Closeable {
84  
85      private static final class ArchiveStatistics {
86          private int numberOfPackedStreams;
87          private long numberOfCoders;
88          private long numberOfOutStreams;
89          private long numberOfInStreams;
90          private long numberOfUnpackSubStreams;
91          private int numberOfFolders;
92          private BitSet folderHasCrc;
93          private int numberOfEntries;
94          private int numberOfEntriesWithStream;
95  
96          void assertValidity(final int maxMemoryLimitInKb) throws IOException {
97              if (numberOfEntriesWithStream > 0 && numberOfFolders == 0) {
98                  throw new IOException("archive with entries but no folders");
99              }
100             if (numberOfEntriesWithStream > numberOfUnpackSubStreams) {
101                 throw new IOException("archive doesn't contain enough substreams for entries");
102             }
103 
104             final long memoryNeededInKb = estimateSize() / 1024;
105             if (maxMemoryLimitInKb < memoryNeededInKb) {
106                 throw new MemoryLimitException(memoryNeededInKb, maxMemoryLimitInKb);
107             }
108         }
109 
110         private long bindPairSize() {
111             return 16;
112         }
113 
114         private long coderSize() {
115             return 2 /* methodId is between 1 and four bytes currently, COPY and LZMA2 are the most common with 1 */
116                     + 16 + 4 /* properties, guess */
117             ;
118         }
119 
120         private long entrySize() {
121             return 100; /* real size depends on name length, everything without name is about 70 bytes */
122         }
123 
124         long estimateSize() {
125             final long lowerBound = 16L * numberOfPackedStreams /* packSizes, packCrcs in Archive */
126                     + numberOfPackedStreams / 8 /* packCrcsDefined in Archive */
127                     + numberOfFolders * folderSize() /* folders in Archive */
128                     + numberOfCoders * coderSize() /* coders in Folder */
129                     + (numberOfOutStreams - numberOfFolders) * bindPairSize() /* bindPairs in Folder */
130                     + 8L * (numberOfInStreams - numberOfOutStreams + numberOfFolders) /* packedStreams in Folder */
131                     + 8L * numberOfOutStreams /* unpackSizes in Folder */
132                     + numberOfEntries * entrySize() /* files in Archive */
133                     + streamMapSize();
134             return 2 * lowerBound /* conservative guess */;
135         }
136 
137         private long folderSize() {
138             return 30; /* nested arrays are accounted for separately */
139         }
140 
141         private long streamMapSize() {
142             return 8 * numberOfFolders /* folderFirstPackStreamIndex, folderFirstFileIndex */
143                     + 8 * numberOfPackedStreams /* packStreamOffsets */
144                     + 4 * numberOfEntries /* fileFolderIndex */
145             ;
146         }
147 
148         @Override
149         public String toString() {
150             return "Archive with " + numberOfEntries + " entries in " + numberOfFolders + " folders. Estimated size " + estimateSize() / 1024L + " kB.";
151         }
152     }
153 
154     /**
155      * Builds new instances of {@link SevenZFile}.
156      *
157      * @since 1.26.0
158      */
159     public static class Builder extends AbstractStreamBuilder<SevenZFile, Builder> {
160 
161         static final int MEMORY_LIMIT_IN_KB = Integer.MAX_VALUE;
162         static final boolean USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES = false;
163         static final boolean TRY_TO_RECOVER_BROKEN_ARCHIVES = false;
164 
165         private SeekableByteChannel seekableByteChannel;
166         private String defaultName = DEFAULT_FILE_NAME;
167         private byte[] password;
168         private int maxMemoryLimitKb = MEMORY_LIMIT_IN_KB;
169         private boolean useDefaultNameForUnnamedEntries = USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES;
170         private boolean tryToRecoverBrokenArchives = TRY_TO_RECOVER_BROKEN_ARCHIVES;
171 
172         @SuppressWarnings("resource") // Caller closes
173         @Override
174         public SevenZFile get() throws IOException {
175             final SeekableByteChannel actualChannel;
176             final String actualDescription;
177             if (seekableByteChannel != null) {
178                 actualChannel = seekableByteChannel;
179                 actualDescription = defaultName;
180             } else if (checkOrigin() instanceof ByteArrayOrigin) {
181                 actualChannel = new SeekableInMemoryByteChannel(checkOrigin().getByteArray());
182                 actualDescription = defaultName;
183             } else {
184                 OpenOption[] openOptions = getOpenOptions();
185                 if (openOptions.length == 0) {
186                     openOptions = new OpenOption[] { StandardOpenOption.READ };
187                 }
188                 final Path path = getPath();
189                 actualChannel = Files.newByteChannel(path, openOptions);
190                 actualDescription = path.toAbsolutePath().toString();
191             }
192             final boolean closeOnError = seekableByteChannel != null;
193             return new SevenZFile(actualChannel, actualDescription, password, closeOnError, maxMemoryLimitKb, useDefaultNameForUnnamedEntries,
194                     tryToRecoverBrokenArchives);
195         }
196 
197         /**
198          * Sets the default name.
199          *
200          * @param defaultName the default name.
201          * @return {@code this} instance.
202          */
203         public Builder setDefaultName(final String defaultName) {
204             this.defaultName = defaultName;
205             return this;
206         }
207 
208         /**
209          * Sets the maximum amount of memory in kilobytes to use for parsing the archive and during extraction.
210          * <p>
211          * Not all codecs honor this setting. Currently only LZMA and LZMA2 are supported.
212          * </p>
213          *
214          * @param maxMemoryLimitKb the max memory limit in kilobytes.
215          * @return {@code this} instance.
216          */
217         public Builder setMaxMemoryLimitKb(final int maxMemoryLimitKb) {
218             this.maxMemoryLimitKb = maxMemoryLimitKb;
219             return this;
220         }
221 
222         /**
223          * Sets the password.
224          *
225          * @param password the password.
226          * @return {@code this} instance.
227          */
228         public Builder setPassword(final byte[] password) {
229             this.password = password != null ? password.clone() : null;
230             return this;
231         }
232 
233         /**
234          * Sets the password.
235          *
236          * @param password the password.
237          * @return {@code this} instance.
238          */
239         public Builder setPassword(final char[] password) {
240             this.password = password != null ? AES256SHA256Decoder.utf16Decode(password.clone()) : null;
241             return this;
242         }
243 
244         /**
245          * Sets the password.
246          *
247          * @param password the password.
248          * @return {@code this} instance.
249          */
250         public Builder setPassword(final String password) {
251             this.password = password != null ? AES256SHA256Decoder.utf16Decode(password.toCharArray()) : null;
252             return this;
253         }
254 
255         /**
256          * Sets the input channel.
257          *
258          * @param seekableByteChannel the input channel.
259          * @return {@code this} instance.
260          */
261         public Builder setSeekableByteChannel(final SeekableByteChannel seekableByteChannel) {
262             this.seekableByteChannel = seekableByteChannel;
263             return this;
264         }
265 
266         /**
267          * Sets whether {@link SevenZFile} will try to recover broken archives where the CRC of the file's metadata is 0.
268          * <p>
269          * This special kind of broken archive is encountered when mutli volume archives are closed prematurely. If you enable this option SevenZFile will trust
270          * data that looks as if it could contain metadata of an archive and allocate big amounts of memory. It is strongly recommended to not enable this
271          * option without setting {@link #setMaxMemoryLimitKb(int)} at the same time.
272          * </p>
273          *
274          * @param tryToRecoverBrokenArchives whether {@link SevenZFile} will try to recover broken archives where the CRC of the file's metadata is 0.
275          * @return {@code this} instance.
276          */
277         public Builder setTryToRecoverBrokenArchives(final boolean tryToRecoverBrokenArchives) {
278             this.tryToRecoverBrokenArchives = tryToRecoverBrokenArchives;
279             return this;
280         }
281 
282         /**
283          * Sets whether entries without a name should get their names set to the archive's default file name.
284          *
285          * @param useDefaultNameForUnnamedEntries whether entries without a name should get their names set to the archive's default file name.
286          * @return {@code this} instance.
287          */
288         public Builder setUseDefaultNameForUnnamedEntries(final boolean useDefaultNameForUnnamedEntries) {
289             this.useDefaultNameForUnnamedEntries = useDefaultNameForUnnamedEntries;
290             return this;
291         }
292 
293     }
294 
295     static final int SIGNATURE_HEADER_SIZE = 32;
296 
297     private static final String DEFAULT_FILE_NAME = "unknown archive";
298 
299     /** Shared with SevenZOutputFile and tests, neither mutates it. */
300     static final byte[] sevenZSignature = { // NOSONAR
301             (byte) '7', (byte) 'z', (byte) 0xBC, (byte) 0xAF, (byte) 0x27, (byte) 0x1C };
302 
303     private static int assertFitsIntoNonNegativeInt(final String what, final long value) throws IOException {
304         if (value > Integer.MAX_VALUE || value < 0) {
305             throw new IOException(String.format("Cannot handle % %,d", what, value));
306         }
307         return (int) value;
308     }
309 
310     /**
311      * Creates a new Builder.
312      *
313      * @return a new Builder.
314      * @since 1.26.0
315      */
316     public static Builder builder() {
317         return new Builder();
318     }
319 
320     private static ByteBuffer checkEndOfFile(final ByteBuffer buf, final int expectRemaining) throws EOFException {
321         final int remaining = buf.remaining();
322         if (remaining < expectRemaining) {
323             throw new EOFException(String.format("remaining %,d < expectRemaining %,d", remaining, expectRemaining));
324         }
325         return buf;
326     }
327 
328     private static void get(final ByteBuffer buf, final byte[] to) throws EOFException {
329         checkEndOfFile(buf, to.length).get(to);
330     }
331 
332     private static char getChar(final ByteBuffer buf) throws EOFException {
333         return checkEndOfFile(buf, Character.BYTES).getChar();
334     }
335 
336     private static int getInt(final ByteBuffer buf) throws EOFException {
337         return checkEndOfFile(buf, Integer.BYTES).getInt();
338     }
339 
340     private static long getLong(final ByteBuffer buf) throws EOFException {
341         return checkEndOfFile(buf, Long.BYTES).getLong();
342     }
343 
344     private static int getUnsignedByte(final ByteBuffer buf) throws EOFException {
345         if (!buf.hasRemaining()) {
346             throw new EOFException();
347         }
348         return buf.get() & 0xff;
349     }
350 
351     /**
352      * Checks if the signature matches what is expected for a 7z file.
353      *
354      * @param signature the bytes to check
355      * @param length    the number of bytes to check
356      * @return true, if this is the signature of a 7z archive.
357      * @since 1.8
358      */
359     public static boolean matches(final byte[] signature, final int length) {
360         if (length < sevenZSignature.length) {
361             return false;
362         }
363         for (int i = 0; i < sevenZSignature.length; i++) {
364             if (signature[i] != sevenZSignature[i]) {
365                 return false;
366             }
367         }
368         return true;
369     }
370 
371     private static SeekableByteChannel newByteChannel(final File file) throws IOException {
372         return Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ));
373     }
374 
375     private static long readUint64(final ByteBuffer in) throws IOException {
376         // long rather than int as it might get shifted beyond the range of an int
377         final long firstByte = getUnsignedByte(in);
378         int mask = 0x80;
379         long value = 0;
380         for (int i = 0; i < 8; i++) {
381             if ((firstByte & mask) == 0) {
382                 return value | (firstByte & mask - 1) << 8 * i;
383             }
384             final long nextByte = getUnsignedByte(in);
385             value |= nextByte << 8 * i;
386             mask >>>= 1;
387         }
388         return value;
389     }
390 
391     private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) {
392         if (bytesToSkip < 1) {
393             return 0;
394         }
395         final int current = input.position();
396         final int maxSkip = input.remaining();
397         if (maxSkip < bytesToSkip) {
398             bytesToSkip = maxSkip;
399         }
400         input.position(current + (int) bytesToSkip);
401         return bytesToSkip;
402     }
403 
404     private final String fileName;
405     private SeekableByteChannel channel;
406     private final Archive archive;
407     private int currentEntryIndex = -1;
408     private int currentFolderIndex = -1;
409     private InputStream currentFolderInputStream;
410     private byte[] password;
411     private long compressedBytesReadFromCurrentEntry;
412     private long uncompressedBytesReadFromCurrentEntry;
413     private final ArrayList<InputStream> deferredBlockStreams = new ArrayList<>();
414     private final int maxMemoryLimitKb;
415     private final boolean useDefaultNameForUnnamedEntries;
416 
417     private final boolean tryToRecoverBrokenArchives;
418 
419     /**
420      * Reads a file as unencrypted 7z archive.
421      *
422      * @param fileName the file to read.
423      * @throws IOException if reading the archive fails.
424      * @deprecated Use {@link Builder#get()}.
425      */
426     @Deprecated
427     public SevenZFile(final File fileName) throws IOException {
428         this(fileName, SevenZFileOptions.DEFAULT);
429     }
430 
431     /**
432      * Reads a file as 7z archive
433      *
434      * @param file     the file to read
435      * @param password optional password if the archive is encrypted - the byte array is supposed to be the UTF16-LE encoded representation of the password.
436      * @throws IOException if reading the archive fails
437      * @deprecated Use {@link Builder#get()}.
438      */
439     @SuppressWarnings("resource") // caller closes
440     @Deprecated
441     public SevenZFile(final File file, final byte[] password) throws IOException {
442         this(newByteChannel(file), file.getAbsolutePath(), password, true, SevenZFileOptions.DEFAULT);
443     }
444 
445     /**
446      * Reads a file as 7z archive
447      *
448      * @param file     the file to read
449      * @param password optional password if the archive is encrypted
450      * @throws IOException if reading the archive fails
451      * @since 1.17
452      * @deprecated Use {@link Builder#get()}.
453      */
454     @Deprecated
455     public SevenZFile(final File file, final char[] password) throws IOException {
456         this(file, password, SevenZFileOptions.DEFAULT);
457     }
458 
459     /**
460      * Reads a file as 7z archive with additional options.
461      *
462      * @param file     the file to read
463      * @param password optional password if the archive is encrypted
464      * @param options  the options to apply
465      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
466      * @since 1.19
467      * @deprecated Use {@link Builder#get()}.
468      */
469     @SuppressWarnings("resource") // caller closes
470     @Deprecated
471     public SevenZFile(final File file, final char[] password, final SevenZFileOptions options) throws IOException {
472         this(newByteChannel(file), // NOSONAR
473                 file.getAbsolutePath(), AES256SHA256Decoder.utf16Decode(password), true, options);
474     }
475 
476     /**
477      * Reads a file as unencrypted 7z archive
478      *
479      * @param file    the file to read
480      * @param options the options to apply
481      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
482      * @since 1.19
483      * @deprecated Use {@link Builder#get()}.
484      */
485     @Deprecated
486     public SevenZFile(final File file, final SevenZFileOptions options) throws IOException {
487         this(file, null, options);
488     }
489 
490     /**
491      * Reads a SeekableByteChannel as 7z archive
492      * <p>
493      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
494      * </p>
495      *
496      * @param channel the channel to read
497      * @throws IOException if reading the archive fails
498      * @since 1.13
499      * @deprecated Use {@link Builder#get()}.
500      */
501     @Deprecated
502     public SevenZFile(final SeekableByteChannel channel) throws IOException {
503         this(channel, SevenZFileOptions.DEFAULT);
504     }
505 
506     /**
507      * Reads a SeekableByteChannel as 7z archive
508      * <p>
509      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
510      * </p>
511      *
512      * @param channel  the channel to read
513      * @param password optional password if the archive is encrypted - the byte array is supposed to be the UTF16-LE encoded representation of the password.
514      * @throws IOException if reading the archive fails
515      * @since 1.13
516      * @deprecated Use {@link Builder#get()}.
517      */
518     @Deprecated
519     public SevenZFile(final SeekableByteChannel channel, final byte[] password) throws IOException {
520         this(channel, DEFAULT_FILE_NAME, password);
521     }
522 
523     /**
524      * Reads a SeekableByteChannel as 7z archive
525      * <p>
526      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
527      * </p>
528      *
529      * @param channel  the channel to read
530      * @param password optional password if the archive is encrypted
531      * @throws IOException if reading the archive fails
532      * @since 1.17
533      * @deprecated Use {@link Builder#get()}.
534      */
535     @Deprecated
536     public SevenZFile(final SeekableByteChannel channel, final char[] password) throws IOException {
537         this(channel, password, SevenZFileOptions.DEFAULT);
538     }
539 
540     /**
541      * Reads a SeekableByteChannel as 7z archive with additional options.
542      * <p>
543      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
544      * </p>
545      *
546      * @param channel  the channel to read
547      * @param password optional password if the archive is encrypted
548      * @param options  the options to apply
549      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
550      * @since 1.19
551      * @deprecated Use {@link Builder#get()}.
552      */
553     @Deprecated
554     public SevenZFile(final SeekableByteChannel channel, final char[] password, final SevenZFileOptions options) throws IOException {
555         this(channel, DEFAULT_FILE_NAME, password, options);
556     }
557 
558     /**
559      * Reads a SeekableByteChannel as 7z archive with additional options.
560      * <p>
561      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
562      * </p>
563      *
564      * @param channel the channel to read
565      * @param options the options to apply
566      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
567      * @since 1.19
568      * @deprecated Use {@link Builder#get()}.
569      */
570     @Deprecated
571     public SevenZFile(final SeekableByteChannel channel, final SevenZFileOptions options) throws IOException {
572         this(channel, DEFAULT_FILE_NAME, null, options);
573     }
574 
575     /**
576      * Reads a SeekableByteChannel as 7z archive
577      * <p>
578      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
579      * </p>
580      *
581      * @param channel  the channel to read
582      * @param fileName name of the archive - only used for error reporting
583      * @throws IOException if reading the archive fails
584      * @since 1.17
585      * @deprecated Use {@link Builder#get()}.
586      */
587     @Deprecated
588     public SevenZFile(final SeekableByteChannel channel, final String fileName) throws IOException {
589         this(channel, fileName, SevenZFileOptions.DEFAULT);
590     }
591 
592     /**
593      * Reads a SeekableByteChannel as 7z archive
594      * <p>
595      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
596      * </p>
597      *
598      * @param channel  the channel to read
599      * @param fileName name of the archive - only used for error reporting
600      * @param password optional password if the archive is encrypted - the byte array is supposed to be the UTF16-LE encoded representation of the password.
601      * @throws IOException if reading the archive fails
602      * @since 1.13
603      * @deprecated Use {@link Builder#get()}.
604      */
605     @Deprecated
606     public SevenZFile(final SeekableByteChannel channel, final String fileName, final byte[] password) throws IOException {
607         this(channel, fileName, password, false, SevenZFileOptions.DEFAULT);
608     }
609 
610     private SevenZFile(final SeekableByteChannel channel, final String fileName, final byte[] password, final boolean closeOnError, final int maxMemoryLimitKb,
611             final boolean useDefaultNameForUnnamedEntries, final boolean tryToRecoverBrokenArchives) throws IOException {
612         boolean succeeded = false;
613         this.channel = channel;
614         this.fileName = fileName;
615         this.maxMemoryLimitKb = maxMemoryLimitKb;
616         this.useDefaultNameForUnnamedEntries = useDefaultNameForUnnamedEntries;
617         this.tryToRecoverBrokenArchives = tryToRecoverBrokenArchives;
618         try {
619             archive = readHeaders(password);
620             if (password != null) {
621                 this.password = Arrays.copyOf(password, password.length);
622             } else {
623                 this.password = null;
624             }
625             succeeded = true;
626         } finally {
627             if (!succeeded && closeOnError) {
628                 this.channel.close();
629             }
630         }
631     }
632 
633     /**
634      * Constructs a new instance.
635      *
636      * @param channel      the channel to read.
637      * @param fileName     name of the archive - only used for error reporting.
638      * @param password     optional password if the archive is encrypted.
639      * @param closeOnError closes the channel on error.
640      * @param options      options.
641      * @throws IOException if reading the archive fails
642      * @deprecated Use {@link Builder#get()}.
643      */
644     @Deprecated
645     private SevenZFile(final SeekableByteChannel channel, final String fileName, final byte[] password, final boolean closeOnError,
646             final SevenZFileOptions options) throws IOException {
647         this(channel, fileName, password, closeOnError, options.getMaxMemoryLimitInKb(), options.getUseDefaultNameForUnnamedEntries(),
648                 options.getTryToRecoverBrokenArchives());
649     }
650 
651     /**
652      * Reads a SeekableByteChannel as 7z archive
653      * <p>
654      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
655      * </p>
656      *
657      * @param channel  the channel to read
658      * @param fileName name of the archive - only used for error reporting
659      * @param password optional password if the archive is encrypted
660      * @throws IOException if reading the archive fails
661      * @since 1.17
662      * @deprecated Use {@link Builder#get()}.
663      */
664     @Deprecated
665     public SevenZFile(final SeekableByteChannel channel, final String fileName, final char[] password) throws IOException {
666         this(channel, fileName, password, SevenZFileOptions.DEFAULT);
667     }
668 
669     /**
670      * Reads a SeekableByteChannel as 7z archive with additional options.
671      * <p>
672      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
673      * </p>
674      *
675      * @param channel  the channel to read
676      * @param fileName name of the archive - only used for error reporting
677      * @param password optional password if the archive is encrypted
678      * @param options  the options to apply
679      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
680      * @since 1.19
681      * @deprecated Use {@link Builder#get()}.
682      */
683     @Deprecated
684     public SevenZFile(final SeekableByteChannel channel, final String fileName, final char[] password, final SevenZFileOptions options) throws IOException {
685         this(channel, fileName, AES256SHA256Decoder.utf16Decode(password), false, options);
686     }
687 
688     /**
689      * Reads a SeekableByteChannel as 7z archive with additional options.
690      * <p>
691      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to read from an in-memory archive.
692      * </p>
693      *
694      * @param channel  the channel to read
695      * @param fileName name of the archive - only used for error reporting
696      * @param options  the options to apply
697      * @throws IOException if reading the archive fails or the memory limit (if set) is too small
698      * @since 1.19
699      * @deprecated Use {@link Builder#get()}.
700      */
701     @Deprecated
702     public SevenZFile(final SeekableByteChannel channel, final String fileName, final SevenZFileOptions options) throws IOException {
703         this(channel, fileName, null, false, options);
704     }
705 
706     private InputStream buildDecoderStack(final Folder folder, final long folderOffset, final int firstPackStreamIndex, final SevenZArchiveEntry entry)
707             throws IOException {
708         channel.position(folderOffset);
709         InputStream inputStreamStack = new FilterInputStream(
710                 new BufferedInputStream(new BoundedSeekableByteChannelInputStream(channel, archive.packSizes[firstPackStreamIndex]))) {
711             private void count(final int c) {
712                 compressedBytesReadFromCurrentEntry += c;
713             }
714 
715             @Override
716             public int read() throws IOException {
717                 final int r = in.read();
718                 if (r >= 0) {
719                     count(1);
720                 }
721                 return r;
722             }
723 
724             @Override
725             public int read(final byte[] b) throws IOException {
726                 return read(b, 0, b.length);
727             }
728 
729             @Override
730             public int read(final byte[] b, final int off, final int len) throws IOException {
731                 if (len == 0) {
732                     return 0;
733                 }
734                 final int r = in.read(b, off, len);
735                 if (r >= 0) {
736                     count(r);
737                 }
738                 return r;
739             }
740         };
741         final LinkedList<SevenZMethodConfiguration> methods = new LinkedList<>();
742         for (final Coder coder : folder.getOrderedCoders()) {
743             if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
744                 throw new IOException("Multi input/output stream coders are not yet supported");
745             }
746             final SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId);
747             inputStreamStack = Coders.addDecoder(fileName, inputStreamStack, folder.getUnpackSizeForCoder(coder), coder, password, maxMemoryLimitKb);
748             methods.addFirst(new SevenZMethodConfiguration(method, Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack)));
749         }
750         entry.setContentMethods(methods);
751         if (folder.hasCrc) {
752             // @formatter:off
753             return ChecksumInputStream.builder()
754                     .setChecksum(new CRC32())
755                     .setInputStream(inputStreamStack)
756                     .setCountThreshold(folder.getUnpackSize())
757                     .setExpectedChecksumValue(folder.crc)
758                     .get();
759             // @formatter:on
760         }
761         return inputStreamStack;
762     }
763 
764     /**
765      * Builds the decoding stream for the entry to be read. This method may be called from a random access(getInputStream) or sequential access(getNextEntry).
766      * If this method is called from a random access, some entries may need to be skipped(we put them to the deferredBlockStreams and skip them when actually
767      * needed to improve the performance)
768      *
769      * @param entryIndex     the index of the entry to be read
770      * @param isRandomAccess is this called in a random access
771      * @throws IOException if there are exceptions when reading the file
772      */
773     private void buildDecodingStream(final int entryIndex, final boolean isRandomAccess) throws IOException {
774         if (archive.streamMap == null) {
775             throw new IOException("Archive doesn't contain stream information to read entries");
776         }
777         final int folderIndex = archive.streamMap.fileFolderIndex[entryIndex];
778         if (folderIndex < 0) {
779             deferredBlockStreams.clear();
780             // TODO: previously it'd return an empty stream?
781             // new BoundedInputStream(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY), 0);
782             return;
783         }
784         final SevenZArchiveEntry file = archive.files[entryIndex];
785         boolean isInSameFolder = false;
786         if (currentFolderIndex == folderIndex) {
787             // (COMPRESS-320).
788             // The current entry is within the same (potentially opened) folder. The
789             // previous stream has to be fully decoded before we can start reading
790             // but don't do it eagerly -- if the user skips over the entire folder nothing
791             // is effectively decompressed.
792             if (entryIndex > 0) {
793                 file.setContentMethods(archive.files[entryIndex - 1].getContentMethods());
794             }
795 
796             // if this is called in a random access, then the content methods of previous entry may be null
797             // the content methods should be set to methods of the first entry as it must not be null,
798             // and the content methods would only be set if the content methods was not set
799             if (isRandomAccess && file.getContentMethods() == null) {
800                 final int folderFirstFileIndex = archive.streamMap.folderFirstFileIndex[folderIndex];
801                 final SevenZArchiveEntry folderFirstFile = archive.files[folderFirstFileIndex];
802                 file.setContentMethods(folderFirstFile.getContentMethods());
803             }
804             isInSameFolder = true;
805         } else {
806             currentFolderIndex = folderIndex;
807             // We're opening a new folder. Discard any queued streams/ folder stream.
808             reopenFolderInputStream(folderIndex, file);
809         }
810 
811         boolean haveSkippedEntries = false;
812         if (isRandomAccess) {
813             // entries will only need to be skipped if it's a random access
814             haveSkippedEntries = skipEntriesWhenNeeded(entryIndex, isInSameFolder, folderIndex);
815         }
816 
817         if (isRandomAccess && currentEntryIndex == entryIndex && !haveSkippedEntries) {
818             // we don't need to add another entry to the deferredBlockStreams when :
819             // 1. If this method is called in a random access and the entry index
820             // to be read equals to the current entry index, the input stream
821             // has already been put in the deferredBlockStreams
822             // 2. If this entry has not been read(which means no entries are skipped)
823             return;
824         }
825 
826         InputStream fileStream = BoundedInputStream.builder()
827                 .setInputStream(currentFolderInputStream)
828                 .setMaxCount(file.getSize())
829                 .setPropagateClose(false)
830                 .get();
831         if (file.getHasCrc()) {
832             // @formatter:off
833             fileStream = ChecksumInputStream.builder()
834                     .setChecksum(new CRC32())
835                     .setInputStream(fileStream)
836                     .setExpectedChecksumValue(file.getCrcValue())
837                     .get();
838             // @formatter:on
839         }
840 
841         deferredBlockStreams.add(fileStream);
842     }
843 
844     private void calculateStreamMap(final Archive archive) throws IOException {
845         int nextFolderPackStreamIndex = 0;
846         final int numFolders = archive.folders != null ? archive.folders.length : 0;
847         final int[] folderFirstPackStreamIndex = new int[numFolders];
848         for (int i = 0; i < numFolders; i++) {
849             folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex;
850             nextFolderPackStreamIndex += archive.folders[i].packedStreams.length;
851         }
852         long nextPackStreamOffset = 0;
853         final int numPackSizes = archive.packSizes.length;
854         final long[] packStreamOffsets = new long[numPackSizes];
855         for (int i = 0; i < numPackSizes; i++) {
856             packStreamOffsets[i] = nextPackStreamOffset;
857             nextPackStreamOffset += archive.packSizes[i];
858         }
859         final int[] folderFirstFileIndex = new int[numFolders];
860         final int[] fileFolderIndex = new int[archive.files.length];
861         int nextFolderIndex = 0;
862         int nextFolderUnpackStreamIndex = 0;
863         for (int i = 0; i < archive.files.length; i++) {
864             if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) {
865                 fileFolderIndex[i] = -1;
866                 continue;
867             }
868             if (nextFolderUnpackStreamIndex == 0) {
869                 for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) {
870                     folderFirstFileIndex[nextFolderIndex] = i;
871                     if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) {
872                         break;
873                     }
874                 }
875                 if (nextFolderIndex >= archive.folders.length) {
876                     throw new IOException("Too few folders in archive");
877                 }
878             }
879             fileFolderIndex[i] = nextFolderIndex;
880             if (!archive.files[i].hasStream()) {
881                 continue;
882             }
883             ++nextFolderUnpackStreamIndex;
884             if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) {
885                 ++nextFolderIndex;
886                 nextFolderUnpackStreamIndex = 0;
887             }
888         }
889         archive.streamMap = new StreamMap(folderFirstPackStreamIndex, packStreamOffsets, folderFirstFileIndex, fileFolderIndex);
890     }
891 
892     private void checkEntryIsInitialized(final Map<Integer, SevenZArchiveEntry> archiveEntries, final int index) {
893         archiveEntries.computeIfAbsent(index, i -> new SevenZArchiveEntry());
894     }
895 
896     /**
897      * Closes the archive.
898      *
899      * @throws IOException if closing the file fails
900      */
901     @Override
902     public void close() throws IOException {
903         if (channel != null) {
904             try {
905                 channel.close();
906             } finally {
907                 channel = null;
908                 if (password != null) {
909                     Arrays.fill(password, (byte) 0);
910                 }
911                 password = null;
912             }
913         }
914     }
915 
916     private InputStream getCurrentStream() throws IOException {
917         if (archive.files[currentEntryIndex].getSize() == 0) {
918             return new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY);
919         }
920         if (deferredBlockStreams.isEmpty()) {
921             throw new IllegalStateException("No current 7z entry (call getNextEntry() first).");
922         }
923         while (deferredBlockStreams.size() > 1) {
924             // In solid compression mode we need to decompress all leading folder'
925             // streams to get access to an entry. We defer this until really needed
926             // so that entire blocks can be skipped without wasting time for decompression.
927             try (InputStream stream = deferredBlockStreams.remove(0)) {
928                 org.apache.commons.io.IOUtils.skip(stream, Long.MAX_VALUE, org.apache.commons.io.IOUtils::byteArray);
929             }
930             compressedBytesReadFromCurrentEntry = 0;
931         }
932         return deferredBlockStreams.get(0);
933     }
934 
935     /**
936      * Gets a default file name from the archive name - if known.
937      * <p>
938      * This implements the same heuristics the 7z tools use. In 7z's case if an archive contains entries without a name - i.e.
939      * {@link SevenZArchiveEntry#getName} returns {@code null} - then its command line and GUI tools will use this default name when extracting the entries.
940      * </p>
941      *
942      * @return null if the name of the archive is unknown. Otherwise, if the name of the archive has got any extension, it is stripped and the remainder
943      *         returned. Finally, if the name of the archive hasn't got any extension, then a {@code ~} character is appended to the archive name.
944      * @since 1.19
945      */
946     public String getDefaultName() {
947         if (DEFAULT_FILE_NAME.equals(fileName) || fileName == null) {
948             return null;
949         }
950 
951         final String lastSegment = new File(fileName).getName();
952         final int dotPos = lastSegment.lastIndexOf(".");
953         if (dotPos > 0) { // if the file starts with a dot then this is not an extension
954             return lastSegment.substring(0, dotPos);
955         }
956         return lastSegment + "~";
957     }
958 
959     /**
960      * Gets a copy of meta-data of all archive entries.
961      * <p>
962      * This method only provides meta-data, the entries can not be used to read the contents, you still need to process all entries in order using
963      * {@link #getNextEntry} for that.
964      * </p>
965      * <p>
966      * The content methods are only available for entries that have already been reached via {@link #getNextEntry}.
967      * </p>
968      *
969      * @return a copy of meta-data of all archive entries.
970      * @since 1.11
971      */
972     public Iterable<SevenZArchiveEntry> getEntries() {
973         return new ArrayList<>(Arrays.asList(archive.files));
974     }
975 
976     /**
977      * Gets an InputStream for reading the contents of the given entry.
978      * <p>
979      * For archives using solid compression randomly accessing entries will be significantly slower than reading the archive sequentially.
980      * </p>
981      *
982      * @param entry the entry to get the stream for.
983      * @return a stream to read the entry from.
984      * @throws IOException if unable to create an input stream from the entry
985      * @since 1.20
986      */
987     public InputStream getInputStream(final SevenZArchiveEntry entry) throws IOException {
988         int entryIndex = -1;
989         for (int i = 0; i < archive.files.length; i++) {
990             if (entry == archive.files[i]) {
991                 entryIndex = i;
992                 break;
993             }
994         }
995 
996         if (entryIndex < 0) {
997             throw new IllegalArgumentException("Can not find " + entry.getName() + " in " + fileName);
998         }
999 
1000         buildDecodingStream(entryIndex, true);
1001         currentEntryIndex = entryIndex;
1002         currentFolderIndex = archive.streamMap.fileFolderIndex[entryIndex];
1003         return getCurrentStream();
1004     }
1005 
1006     /**
1007      * Gets the next Archive Entry in this archive.
1008      *
1009      * @return the next entry, or {@code null} if there are no more entries
1010      * @throws IOException if the next entry could not be read
1011      */
1012     public SevenZArchiveEntry getNextEntry() throws IOException {
1013         if (currentEntryIndex >= archive.files.length - 1) {
1014             return null;
1015         }
1016         ++currentEntryIndex;
1017         final SevenZArchiveEntry entry = archive.files[currentEntryIndex];
1018         if (entry.getName() == null && useDefaultNameForUnnamedEntries) {
1019             entry.setName(getDefaultName());
1020         }
1021         buildDecodingStream(currentEntryIndex, false);
1022         uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0;
1023         return entry;
1024     }
1025 
1026     /**
1027      * Gets statistics for bytes read from the current entry.
1028      *
1029      * @return statistics for bytes read from the current entry
1030      * @since 1.17
1031      */
1032     public InputStreamStatistics getStatisticsForCurrentEntry() {
1033         return new InputStreamStatistics() {
1034             @Override
1035             public long getCompressedCount() {
1036                 return compressedBytesReadFromCurrentEntry;
1037             }
1038 
1039             @Override
1040             public long getUncompressedCount() {
1041                 return uncompressedBytesReadFromCurrentEntry;
1042             }
1043         };
1044     }
1045 
1046     /**
1047      * Tests if any data of current entry has been read or not. This is achieved by comparing the bytes remaining to read and the size of the file.
1048      *
1049      * @return true if any data of current entry has been read
1050      * @since 1.21
1051      */
1052     private boolean hasCurrentEntryBeenRead() {
1053         boolean hasCurrentEntryBeenRead = false;
1054         if (!deferredBlockStreams.isEmpty()) {
1055             final InputStream currentEntryInputStream = deferredBlockStreams.get(deferredBlockStreams.size() - 1);
1056             // get the bytes remaining to read, and compare it with the size of
1057             // the file to figure out if the file has been read
1058             if (currentEntryInputStream instanceof ChecksumInputStream) {
1059                 hasCurrentEntryBeenRead = ((ChecksumInputStream) currentEntryInputStream).getRemaining() != archive.files[currentEntryIndex].getSize();
1060             } else if (currentEntryInputStream instanceof BoundedInputStream) {
1061                 hasCurrentEntryBeenRead = ((BoundedInputStream) currentEntryInputStream).getRemaining() != archive.files[currentEntryIndex].getSize();
1062             }
1063         }
1064         return hasCurrentEntryBeenRead;
1065     }
1066 
1067     private Archive initializeArchive(final StartHeader startHeader, final byte[] password, final boolean verifyCrc) throws IOException {
1068         assertFitsIntoNonNegativeInt("nextHeaderSize", startHeader.nextHeaderSize);
1069         final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize;
1070         channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset);
1071         if (verifyCrc) {
1072             final long position = channel.position();
1073             final CheckedInputStream cis = new CheckedInputStream(Channels.newInputStream(channel), new CRC32());
1074             if (cis.skip(nextHeaderSizeInt) != nextHeaderSizeInt) {
1075                 throw new IOException("Problem computing NextHeader CRC-32");
1076             }
1077             if (startHeader.nextHeaderCrc != cis.getChecksum().getValue()) {
1078                 throw new IOException("NextHeader CRC-32 mismatch");
1079             }
1080             channel.position(position);
1081         }
1082         Archive archive = new Archive();
1083         ByteBuffer buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN);
1084         readFully(buf);
1085         int nid = getUnsignedByte(buf);
1086         if (nid == NID.kEncodedHeader) {
1087             buf = readEncodedHeader(buf, archive, password);
1088             // Archive gets rebuilt with the new header
1089             archive = new Archive();
1090             nid = getUnsignedByte(buf);
1091         }
1092         if (nid != NID.kHeader) {
1093             throw new IOException("Broken or unsupported archive: no Header");
1094         }
1095         readHeader(buf, archive);
1096         archive.subStreamsInfo = null;
1097         return archive;
1098     }
1099 
1100     /**
1101      * Reads a byte of data.
1102      *
1103      * @return the byte read, or -1 if end of input is reached
1104      * @throws IOException if an I/O error has occurred
1105      */
1106     public int read() throws IOException {
1107         final int b = getCurrentStream().read();
1108         if (b >= 0) {
1109             uncompressedBytesReadFromCurrentEntry++;
1110         }
1111         return b;
1112     }
1113 
1114     /**
1115      * Reads data into an array of bytes.
1116      *
1117      * @param b the array to write data to
1118      * @return the number of bytes read, or -1 if end of input is reached
1119      * @throws IOException if an I/O error has occurred
1120      */
1121     public int read(final byte[] b) throws IOException {
1122         return read(b, 0, b.length);
1123     }
1124 
1125     /**
1126      * Reads data into an array of bytes.
1127      *
1128      * @param b   the array to write data to
1129      * @param off offset into the buffer to start filling at
1130      * @param len of bytes to read
1131      * @return the number of bytes read, or -1 if end of input is reached
1132      * @throws IOException if an I/O error has occurred
1133      */
1134     public int read(final byte[] b, final int off, final int len) throws IOException {
1135         if (len == 0) {
1136             return 0;
1137         }
1138         final int cnt = getCurrentStream().read(b, off, len);
1139         if (cnt > 0) {
1140             uncompressedBytesReadFromCurrentEntry += cnt;
1141         }
1142         return cnt;
1143     }
1144 
1145     private BitSet readAllOrBits(final ByteBuffer header, final int size) throws IOException {
1146         final int areAllDefined = getUnsignedByte(header);
1147         final BitSet bits;
1148         if (areAllDefined != 0) {
1149             bits = new BitSet(size);
1150             for (int i = 0; i < size; i++) {
1151                 bits.set(i, true);
1152             }
1153         } else {
1154             bits = readBits(header, size);
1155         }
1156         return bits;
1157     }
1158 
1159     private void readArchiveProperties(final ByteBuffer input) throws IOException {
1160         // FIXME: the reference implementation just throws them away?
1161         long nid = readUint64(input);
1162         while (nid != NID.kEnd) {
1163             final long propertySize = readUint64(input);
1164             final byte[] property = new byte[(int) propertySize];
1165             get(input, property);
1166             nid = readUint64(input);
1167         }
1168     }
1169 
1170     private BitSet readBits(final ByteBuffer header, final int size) throws IOException {
1171         final BitSet bits = new BitSet(size);
1172         int mask = 0;
1173         int cache = 0;
1174         for (int i = 0; i < size; i++) {
1175             if (mask == 0) {
1176                 mask = 0x80;
1177                 cache = getUnsignedByte(header);
1178             }
1179             bits.set(i, (cache & mask) != 0);
1180             mask >>>= 1;
1181         }
1182         return bits;
1183     }
1184 
1185     private ByteBuffer readEncodedHeader(final ByteBuffer header, final Archive archive, final byte[] password) throws IOException {
1186         final int pos = header.position();
1187         final ArchiveStatistics stats = new ArchiveStatistics();
1188         sanityCheckStreamsInfo(header, stats);
1189         stats.assertValidity(maxMemoryLimitKb);
1190         header.position(pos);
1191 
1192         readStreamsInfo(header, archive);
1193 
1194         if (archive.folders == null || archive.folders.length == 0) {
1195             throw new IOException("no folders, can't read encoded header");
1196         }
1197         if (archive.packSizes == null || archive.packSizes.length == 0) {
1198             throw new IOException("no packed streams, can't read encoded header");
1199         }
1200 
1201         // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage?
1202         final Folder folder = archive.folders[0];
1203         final int firstPackStreamIndex = 0;
1204         final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + 0;
1205 
1206         channel.position(folderOffset);
1207         InputStream inputStreamStack = new BoundedSeekableByteChannelInputStream(channel, archive.packSizes[firstPackStreamIndex]);
1208         for (final Coder coder : folder.getOrderedCoders()) {
1209             if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
1210                 throw new IOException("Multi input/output stream coders are not yet supported");
1211             }
1212             inputStreamStack = Coders.addDecoder(fileName, inputStreamStack, // NOSONAR
1213                     folder.getUnpackSizeForCoder(coder), coder, password, maxMemoryLimitKb);
1214         }
1215         if (folder.hasCrc) {
1216             // @formatter:off
1217             inputStreamStack = ChecksumInputStream.builder()
1218                     .setChecksum(new CRC32())
1219                     .setInputStream(inputStreamStack)
1220                     .setCountThreshold(folder.getUnpackSize())
1221                     .setExpectedChecksumValue(folder.crc)
1222                     .get();
1223             // @formatter:on
1224         }
1225         final int unpackSize = assertFitsIntoNonNegativeInt("unpackSize", folder.getUnpackSize());
1226         final byte[] nextHeader = IOUtils.readRange(inputStreamStack, unpackSize);
1227         if (nextHeader.length < unpackSize) {
1228             throw new IOException("premature end of stream");
1229         }
1230         inputStreamStack.close();
1231         return ByteBuffer.wrap(nextHeader).order(ByteOrder.LITTLE_ENDIAN);
1232     }
1233 
1234     private void readFilesInfo(final ByteBuffer header, final Archive archive) throws IOException {
1235         final int numFilesInt = (int) readUint64(header);
1236         final Map<Integer, SevenZArchiveEntry> fileMap = new LinkedHashMap<>();
1237         BitSet isEmptyStream = null;
1238         BitSet isEmptyFile = null;
1239         BitSet isAnti = null;
1240         while (true) {
1241             final int propertyType = getUnsignedByte(header);
1242             if (propertyType == 0) {
1243                 break;
1244             }
1245             final long size = readUint64(header);
1246             switch (propertyType) {
1247             case NID.kEmptyStream: {
1248                 isEmptyStream = readBits(header, numFilesInt);
1249                 break;
1250             }
1251             case NID.kEmptyFile: {
1252                 isEmptyFile = readBits(header, isEmptyStream.cardinality());
1253                 break;
1254             }
1255             case NID.kAnti: {
1256                 isAnti = readBits(header, isEmptyStream.cardinality());
1257                 break;
1258             }
1259             case NID.kName: {
1260                 /* final int external = */ getUnsignedByte(header);
1261                 final byte[] names = new byte[(int) (size - 1)];
1262                 final int namesLength = names.length;
1263                 get(header, names);
1264                 int nextFile = 0;
1265                 int nextName = 0;
1266                 for (int i = 0; i < namesLength; i += 2) {
1267                     if (names[i] == 0 && names[i + 1] == 0) {
1268                         checkEntryIsInitialized(fileMap, nextFile);
1269                         fileMap.get(nextFile).setName(new String(names, nextName, i - nextName, UTF_16LE));
1270                         nextName = i + 2;
1271                         nextFile++;
1272                     }
1273                 }
1274                 if (nextName != namesLength || nextFile != numFilesInt) {
1275                     throw new IOException("Error parsing file names");
1276                 }
1277                 break;
1278             }
1279             case NID.kCTime: {
1280                 final BitSet timesDefined = readAllOrBits(header, numFilesInt);
1281                 /* final int external = */ getUnsignedByte(header);
1282                 for (int i = 0; i < numFilesInt; i++) {
1283                     checkEntryIsInitialized(fileMap, i);
1284                     final SevenZArchiveEntry entryAtIndex = fileMap.get(i);
1285                     entryAtIndex.setHasCreationDate(timesDefined.get(i));
1286                     if (entryAtIndex.getHasCreationDate()) {
1287                         entryAtIndex.setCreationDate(getLong(header));
1288                     }
1289                 }
1290                 break;
1291             }
1292             case NID.kATime: {
1293                 final BitSet timesDefined = readAllOrBits(header, numFilesInt);
1294                 /* final int external = */ getUnsignedByte(header);
1295                 for (int i = 0; i < numFilesInt; i++) {
1296                     checkEntryIsInitialized(fileMap, i);
1297                     final SevenZArchiveEntry entryAtIndex = fileMap.get(i);
1298                     entryAtIndex.setHasAccessDate(timesDefined.get(i));
1299                     if (entryAtIndex.getHasAccessDate()) {
1300                         entryAtIndex.setAccessDate(getLong(header));
1301                     }
1302                 }
1303                 break;
1304             }
1305             case NID.kMTime: {
1306                 final BitSet timesDefined = readAllOrBits(header, numFilesInt);
1307                 /* final int external = */ getUnsignedByte(header);
1308                 for (int i = 0; i < numFilesInt; i++) {
1309                     checkEntryIsInitialized(fileMap, i);
1310                     final SevenZArchiveEntry entryAtIndex = fileMap.get(i);
1311                     entryAtIndex.setHasLastModifiedDate(timesDefined.get(i));
1312                     if (entryAtIndex.getHasLastModifiedDate()) {
1313                         entryAtIndex.setLastModifiedDate(getLong(header));
1314                     }
1315                 }
1316                 break;
1317             }
1318             case NID.kWinAttributes: {
1319                 final BitSet attributesDefined = readAllOrBits(header, numFilesInt);
1320                 /* final int external = */ getUnsignedByte(header);
1321                 for (int i = 0; i < numFilesInt; i++) {
1322                     checkEntryIsInitialized(fileMap, i);
1323                     final SevenZArchiveEntry entryAtIndex = fileMap.get(i);
1324                     entryAtIndex.setHasWindowsAttributes(attributesDefined.get(i));
1325                     if (entryAtIndex.getHasWindowsAttributes()) {
1326                         entryAtIndex.setWindowsAttributes(getInt(header));
1327                     }
1328                 }
1329                 break;
1330             }
1331             case NID.kDummy: {
1332                 // 7z 9.20 asserts the content is all zeros and ignores the property
1333                 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1334 
1335                 skipBytesFully(header, size);
1336                 break;
1337             }
1338 
1339             default: {
1340                 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1341                 skipBytesFully(header, size);
1342                 break;
1343             }
1344             }
1345         }
1346         int nonEmptyFileCounter = 0;
1347         int emptyFileCounter = 0;
1348         for (int i = 0; i < numFilesInt; i++) {
1349             final SevenZArchiveEntry entryAtIndex = fileMap.get(i);
1350             if (entryAtIndex == null) {
1351                 continue;
1352             }
1353             entryAtIndex.setHasStream(isEmptyStream == null || !isEmptyStream.get(i));
1354             if (entryAtIndex.hasStream()) {
1355                 if (archive.subStreamsInfo == null) {
1356                     throw new IOException("Archive contains file with streams but no subStreamsInfo");
1357                 }
1358                 entryAtIndex.setDirectory(false);
1359                 entryAtIndex.setAntiItem(false);
1360                 entryAtIndex.setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter));
1361                 entryAtIndex.setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]);
1362                 entryAtIndex.setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]);
1363                 if (entryAtIndex.getSize() < 0) {
1364                     throw new IOException("broken archive, entry with negative size");
1365                 }
1366                 ++nonEmptyFileCounter;
1367             } else {
1368                 entryAtIndex.setDirectory(isEmptyFile == null || !isEmptyFile.get(emptyFileCounter));
1369                 entryAtIndex.setAntiItem(isAnti != null && isAnti.get(emptyFileCounter));
1370                 entryAtIndex.setHasCrc(false);
1371                 entryAtIndex.setSize(0);
1372                 ++emptyFileCounter;
1373             }
1374         }
1375         archive.files = fileMap.values().stream().filter(Objects::nonNull).toArray(SevenZArchiveEntry[]::new);
1376         calculateStreamMap(archive);
1377     }
1378 
1379     private Folder readFolder(final ByteBuffer header) throws IOException {
1380         final Folder folder = new Folder();
1381 
1382         final long numCoders = readUint64(header);
1383         final Coder[] coders = new Coder[(int) numCoders];
1384         long totalInStreams = 0;
1385         long totalOutStreams = 0;
1386         for (int i = 0; i < coders.length; i++) {
1387             final int bits = getUnsignedByte(header);
1388             final int idSize = bits & 0xf;
1389             final boolean isSimple = (bits & 0x10) == 0;
1390             final boolean hasAttributes = (bits & 0x20) != 0;
1391             final boolean moreAlternativeMethods = (bits & 0x80) != 0;
1392 
1393             final byte[] decompressionMethodId = new byte[idSize];
1394             get(header, decompressionMethodId);
1395             final long numInStreams;
1396             final long numOutStreams;
1397             if (isSimple) {
1398                 numInStreams = 1;
1399                 numOutStreams = 1;
1400             } else {
1401                 numInStreams = readUint64(header);
1402                 numOutStreams = readUint64(header);
1403             }
1404             totalInStreams += numInStreams;
1405             totalOutStreams += numOutStreams;
1406             byte[] properties = null;
1407             if (hasAttributes) {
1408                 final long propertiesSize = readUint64(header);
1409                 properties = new byte[(int) propertiesSize];
1410                 get(header, properties);
1411             }
1412             // would need to keep looping as above:
1413             if (moreAlternativeMethods) {
1414                 throw new IOException("Alternative methods are unsupported, please report. " + // NOSONAR
1415                         "The reference implementation doesn't support them either.");
1416             }
1417             coders[i] = new Coder(decompressionMethodId, numInStreams, numOutStreams, properties);
1418         }
1419         folder.coders = coders;
1420         folder.totalInputStreams = totalInStreams;
1421         folder.totalOutputStreams = totalOutStreams;
1422 
1423         final long numBindPairs = totalOutStreams - 1;
1424         final BindPair[] bindPairs = new BindPair[(int) numBindPairs];
1425         for (int i = 0; i < bindPairs.length; i++) {
1426             bindPairs[i] = new BindPair(readUint64(header), readUint64(header));
1427         }
1428         folder.bindPairs = bindPairs;
1429 
1430         final long numPackedStreams = totalInStreams - numBindPairs;
1431         final long[] packedStreams = new long[(int) numPackedStreams];
1432         if (numPackedStreams == 1) {
1433             int i;
1434             for (i = 0; i < (int) totalInStreams; i++) {
1435                 if (folder.findBindPairForInStream(i) < 0) {
1436                     break;
1437                 }
1438             }
1439             packedStreams[0] = i;
1440         } else {
1441             for (int i = 0; i < (int) numPackedStreams; i++) {
1442                 packedStreams[i] = readUint64(header);
1443             }
1444         }
1445         folder.packedStreams = packedStreams;
1446 
1447         return folder;
1448     }
1449 
1450     private void readFully(final ByteBuffer buf) throws IOException {
1451         buf.rewind();
1452         IOUtils.readFully(channel, buf);
1453         buf.flip();
1454     }
1455 
1456     private void readHeader(final ByteBuffer header, final Archive archive) throws IOException {
1457         final int pos = header.position();
1458         final ArchiveStatistics stats = sanityCheckAndCollectStatistics(header);
1459         stats.assertValidity(maxMemoryLimitKb);
1460         header.position(pos);
1461 
1462         int nid = getUnsignedByte(header);
1463 
1464         if (nid == NID.kArchiveProperties) {
1465             readArchiveProperties(header);
1466             nid = getUnsignedByte(header);
1467         }
1468 
1469         if (nid == NID.kAdditionalStreamsInfo) {
1470             throw new IOException("Additional streams unsupported");
1471             // nid = getUnsignedByte(header);
1472         }
1473 
1474         if (nid == NID.kMainStreamsInfo) {
1475             readStreamsInfo(header, archive);
1476             nid = getUnsignedByte(header);
1477         }
1478 
1479         if (nid == NID.kFilesInfo) {
1480             readFilesInfo(header, archive);
1481             nid = getUnsignedByte(header);
1482         }
1483     }
1484 
1485     private Archive readHeaders(final byte[] password) throws IOException {
1486         final ByteBuffer buf = ByteBuffer.allocate(12 /* signature + 2 bytes version + 4 bytes CRC */).order(ByteOrder.LITTLE_ENDIAN);
1487         readFully(buf);
1488         final byte[] signature = new byte[6];
1489         buf.get(signature);
1490         if (!Arrays.equals(signature, sevenZSignature)) {
1491             throw new IOException("Bad 7z signature");
1492         }
1493         // 7zFormat.txt has it wrong - it's first major then minor
1494         final byte archiveVersionMajor = buf.get();
1495         final byte archiveVersionMinor = buf.get();
1496         if (archiveVersionMajor != 0) {
1497             throw new IOException(String.format("Unsupported 7z version (%d,%d)", archiveVersionMajor, archiveVersionMinor));
1498         }
1499 
1500         boolean headerLooksValid = false; // See https://www.7-zip.org/recover.html - "There is no correct End Header at the end of archive"
1501         final long startHeaderCrc = 0xffffFFFFL & buf.getInt();
1502         if (startHeaderCrc == 0) {
1503             // This is an indication of a corrupt header - peek the next 20 bytes
1504             final long currentPosition = channel.position();
1505             final ByteBuffer peekBuf = ByteBuffer.allocate(20);
1506             readFully(peekBuf);
1507             channel.position(currentPosition);
1508             // Header invalid if all data is 0
1509             while (peekBuf.hasRemaining()) {
1510                 if (peekBuf.get() != 0) {
1511                     headerLooksValid = true;
1512                     break;
1513                 }
1514             }
1515         } else {
1516             headerLooksValid = true;
1517         }
1518 
1519         if (headerLooksValid) {
1520             return initializeArchive(readStartHeader(startHeaderCrc), password, true);
1521         }
1522         // No valid header found - probably first file of multipart archive was removed too early. Scan for end header.
1523         if (tryToRecoverBrokenArchives) {
1524             return tryToLocateEndHeader(password);
1525         }
1526         throw new IOException("archive seems to be invalid.\nYou may want to retry and enable the"
1527                 + " tryToRecoverBrokenArchives if the archive could be a multi volume archive that has been closed" + " prematurely.");
1528     }
1529 
1530     private void readPackInfo(final ByteBuffer header, final Archive archive) throws IOException {
1531         archive.packPos = readUint64(header);
1532         final int numPackStreamsInt = (int) readUint64(header);
1533         int nid = getUnsignedByte(header);
1534         if (nid == NID.kSize) {
1535             archive.packSizes = new long[numPackStreamsInt];
1536             for (int i = 0; i < archive.packSizes.length; i++) {
1537                 archive.packSizes[i] = readUint64(header);
1538             }
1539             nid = getUnsignedByte(header);
1540         }
1541 
1542         if (nid == NID.kCRC) {
1543             archive.packCrcsDefined = readAllOrBits(header, numPackStreamsInt);
1544             archive.packCrcs = new long[numPackStreamsInt];
1545             for (int i = 0; i < numPackStreamsInt; i++) {
1546                 if (archive.packCrcsDefined.get(i)) {
1547                     archive.packCrcs[i] = 0xffffFFFFL & getInt(header);
1548                 }
1549             }
1550             // read one more
1551             getUnsignedByte(header);
1552         }
1553     }
1554 
1555     private StartHeader readStartHeader(final long startHeaderCrc) throws IOException {
1556         // using Stream rather than ByteBuffer for the benefit of the built-in CRC check
1557         try (DataInputStream dataInputStream = new DataInputStream(ChecksumInputStream.builder()
1558                 // @formatter:off
1559                 .setChecksum(new CRC32())
1560                 .setInputStream(new BoundedSeekableByteChannelInputStream(channel, 20))
1561                 .setCountThreshold(20L)
1562                 .setExpectedChecksumValue(startHeaderCrc)
1563                 .get())) {
1564                 // @formatter:on
1565             final long nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong());
1566             if (nextHeaderOffset < 0 || nextHeaderOffset + SIGNATURE_HEADER_SIZE > channel.size()) {
1567                 throw new IOException("nextHeaderOffset is out of bounds");
1568             }
1569             final long nextHeaderSize = Long.reverseBytes(dataInputStream.readLong());
1570             final long nextHeaderEnd = nextHeaderOffset + nextHeaderSize;
1571             if (nextHeaderEnd < nextHeaderOffset || nextHeaderEnd + SIGNATURE_HEADER_SIZE > channel.size()) {
1572                 throw new IOException("nextHeaderSize is out of bounds");
1573             }
1574             final long nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt());
1575             return new StartHeader(nextHeaderOffset, nextHeaderSize, nextHeaderCrc);
1576         }
1577     }
1578 
1579     private void readStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException {
1580         int nid = getUnsignedByte(header);
1581 
1582         if (nid == NID.kPackInfo) {
1583             readPackInfo(header, archive);
1584             nid = getUnsignedByte(header);
1585         }
1586 
1587         if (nid == NID.kUnpackInfo) {
1588             readUnpackInfo(header, archive);
1589             nid = getUnsignedByte(header);
1590         } else {
1591             // archive without unpack/coders info
1592             archive.folders = Folder.EMPTY_FOLDER_ARRAY;
1593         }
1594 
1595         if (nid == NID.kSubStreamsInfo) {
1596             readSubStreamsInfo(header, archive);
1597             nid = getUnsignedByte(header);
1598         }
1599     }
1600 
1601     private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException {
1602         for (final Folder folder : archive.folders) {
1603             folder.numUnpackSubStreams = 1;
1604         }
1605         long unpackStreamsCount = archive.folders.length;
1606 
1607         int nid = getUnsignedByte(header);
1608         if (nid == NID.kNumUnpackStream) {
1609             unpackStreamsCount = 0;
1610             for (final Folder folder : archive.folders) {
1611                 final long numStreams = readUint64(header);
1612                 folder.numUnpackSubStreams = (int) numStreams;
1613                 unpackStreamsCount += numStreams;
1614             }
1615             nid = getUnsignedByte(header);
1616         }
1617 
1618         final int totalUnpackStreams = (int) unpackStreamsCount;
1619         final SubStreamsInfo subStreamsInfo = new SubStreamsInfo(totalUnpackStreams);
1620         int nextUnpackStream = 0;
1621         for (final Folder folder : archive.folders) {
1622             if (folder.numUnpackSubStreams == 0) {
1623                 continue;
1624             }
1625             long sum = 0;
1626             if (nid == NID.kSize) {
1627                 for (int i = 0; i < folder.numUnpackSubStreams - 1; i++) {
1628                     final long size = readUint64(header);
1629                     subStreamsInfo.unpackSizes[nextUnpackStream++] = size;
1630                     sum += size;
1631                 }
1632             }
1633             if (sum > folder.getUnpackSize()) {
1634                 throw new IOException("sum of unpack sizes of folder exceeds total unpack size");
1635             }
1636             subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum;
1637         }
1638         if (nid == NID.kSize) {
1639             nid = getUnsignedByte(header);
1640         }
1641 
1642         int numDigests = 0;
1643         for (final Folder folder : archive.folders) {
1644             if (folder.numUnpackSubStreams != 1 || !folder.hasCrc) {
1645                 numDigests += folder.numUnpackSubStreams;
1646             }
1647         }
1648 
1649         if (nid == NID.kCRC) {
1650             final BitSet hasMissingCrc = readAllOrBits(header, numDigests);
1651             final long[] missingCrcs = new long[numDigests];
1652             for (int i = 0; i < numDigests; i++) {
1653                 if (hasMissingCrc.get(i)) {
1654                     missingCrcs[i] = 0xffffFFFFL & getInt(header);
1655                 }
1656             }
1657             int nextCrc = 0;
1658             int nextMissingCrc = 0;
1659             for (final Folder folder : archive.folders) {
1660                 if (folder.numUnpackSubStreams == 1 && folder.hasCrc) {
1661                     subStreamsInfo.hasCrc.set(nextCrc, true);
1662                     subStreamsInfo.crcs[nextCrc] = folder.crc;
1663                     ++nextCrc;
1664                 } else {
1665                     for (int i = 0; i < folder.numUnpackSubStreams; i++) {
1666                         subStreamsInfo.hasCrc.set(nextCrc, hasMissingCrc.get(nextMissingCrc));
1667                         subStreamsInfo.crcs[nextCrc] = missingCrcs[nextMissingCrc];
1668                         ++nextCrc;
1669                         ++nextMissingCrc;
1670                     }
1671                 }
1672             }
1673 
1674             nid = getUnsignedByte(header);
1675         }
1676 
1677         archive.subStreamsInfo = subStreamsInfo;
1678     }
1679 
1680     private void readUnpackInfo(final ByteBuffer header, final Archive archive) throws IOException {
1681         int nid = getUnsignedByte(header);
1682         final int numFoldersInt = (int) readUint64(header);
1683         final Folder[] folders = new Folder[numFoldersInt];
1684         archive.folders = folders;
1685         /* final int external = */ getUnsignedByte(header);
1686         for (int i = 0; i < numFoldersInt; i++) {
1687             folders[i] = readFolder(header);
1688         }
1689 
1690         nid = getUnsignedByte(header);
1691         for (final Folder folder : folders) {
1692             assertFitsIntoNonNegativeInt("totalOutputStreams", folder.totalOutputStreams);
1693             folder.unpackSizes = new long[(int) folder.totalOutputStreams];
1694             for (int i = 0; i < folder.totalOutputStreams; i++) {
1695                 folder.unpackSizes[i] = readUint64(header);
1696             }
1697         }
1698 
1699         nid = getUnsignedByte(header);
1700         if (nid == NID.kCRC) {
1701             final BitSet crcsDefined = readAllOrBits(header, numFoldersInt);
1702             for (int i = 0; i < numFoldersInt; i++) {
1703                 if (crcsDefined.get(i)) {
1704                     folders[i].hasCrc = true;
1705                     folders[i].crc = 0xffffFFFFL & getInt(header);
1706                 } else {
1707                     folders[i].hasCrc = false;
1708                 }
1709             }
1710 
1711             nid = getUnsignedByte(header);
1712         }
1713     }
1714 
1715     /**
1716      * Discard any queued streams/ folder stream, and reopen the current folder input stream.
1717      *
1718      * @param folderIndex the index of the folder to reopen
1719      * @param file        the 7z entry to read
1720      * @throws IOException if exceptions occur when reading the 7z file
1721      */
1722     private void reopenFolderInputStream(final int folderIndex, final SevenZArchiveEntry file) throws IOException {
1723         deferredBlockStreams.clear();
1724         if (currentFolderInputStream != null) {
1725             currentFolderInputStream.close();
1726             currentFolderInputStream = null;
1727         }
1728         final Folder folder = archive.folders[folderIndex];
1729         final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex];
1730         final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + archive.streamMap.packStreamOffsets[firstPackStreamIndex];
1731 
1732         currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file);
1733     }
1734 
1735     private ArchiveStatistics sanityCheckAndCollectStatistics(final ByteBuffer header) throws IOException {
1736         final ArchiveStatistics stats = new ArchiveStatistics();
1737 
1738         int nid = getUnsignedByte(header);
1739 
1740         if (nid == NID.kArchiveProperties) {
1741             sanityCheckArchiveProperties(header);
1742             nid = getUnsignedByte(header);
1743         }
1744 
1745         if (nid == NID.kAdditionalStreamsInfo) {
1746             throw new IOException("Additional streams unsupported");
1747             // nid = getUnsignedByte(header);
1748         }
1749 
1750         if (nid == NID.kMainStreamsInfo) {
1751             sanityCheckStreamsInfo(header, stats);
1752             nid = getUnsignedByte(header);
1753         }
1754 
1755         if (nid == NID.kFilesInfo) {
1756             sanityCheckFilesInfo(header, stats);
1757             nid = getUnsignedByte(header);
1758         }
1759 
1760         if (nid != NID.kEnd) {
1761             throw new IOException("Badly terminated header, found " + nid);
1762         }
1763 
1764         return stats;
1765     }
1766 
1767     private void sanityCheckArchiveProperties(final ByteBuffer header) throws IOException {
1768         long nid = readUint64(header);
1769         while (nid != NID.kEnd) {
1770             final int propertySize = assertFitsIntoNonNegativeInt("propertySize", readUint64(header));
1771             if (skipBytesFully(header, propertySize) < propertySize) {
1772                 throw new IOException("invalid property size");
1773             }
1774             nid = readUint64(header);
1775         }
1776     }
1777 
1778     private void sanityCheckFilesInfo(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
1779         stats.numberOfEntries = assertFitsIntoNonNegativeInt("numFiles", readUint64(header));
1780 
1781         int emptyStreams = -1;
1782         while (true) {
1783             final int propertyType = getUnsignedByte(header);
1784             if (propertyType == 0) {
1785                 break;
1786             }
1787             final long size = readUint64(header);
1788             switch (propertyType) {
1789             case NID.kEmptyStream: {
1790                 emptyStreams = readBits(header, stats.numberOfEntries).cardinality();
1791                 break;
1792             }
1793             case NID.kEmptyFile: {
1794                 if (emptyStreams == -1) {
1795                     throw new IOException("Header format error: kEmptyStream must appear before kEmptyFile");
1796                 }
1797                 readBits(header, emptyStreams);
1798                 break;
1799             }
1800             case NID.kAnti: {
1801                 if (emptyStreams == -1) {
1802                     throw new IOException("Header format error: kEmptyStream must appear before kAnti");
1803                 }
1804                 readBits(header, emptyStreams);
1805                 break;
1806             }
1807             case NID.kName: {
1808                 final int external = getUnsignedByte(header);
1809                 if (external != 0) {
1810                     throw new IOException("Not implemented");
1811                 }
1812                 final int namesLength = assertFitsIntoNonNegativeInt("file names length", size - 1);
1813                 if ((namesLength & 1) != 0) {
1814                     throw new IOException("File names length invalid");
1815                 }
1816 
1817                 int filesSeen = 0;
1818                 for (int i = 0; i < namesLength; i += 2) {
1819                     final char c = getChar(header);
1820                     if (c == 0) {
1821                         filesSeen++;
1822                     }
1823                 }
1824                 if (filesSeen != stats.numberOfEntries) {
1825                     throw new IOException("Invalid number of file names (" + filesSeen + " instead of " + stats.numberOfEntries + ")");
1826                 }
1827                 break;
1828             }
1829             case NID.kCTime: {
1830                 final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality();
1831                 final int external = getUnsignedByte(header);
1832                 if (external != 0) {
1833                     throw new IOException("Not implemented");
1834                 }
1835                 if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) {
1836                     throw new IOException("invalid creation dates size");
1837                 }
1838                 break;
1839             }
1840             case NID.kATime: {
1841                 final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality();
1842                 final int external = getUnsignedByte(header);
1843                 if (external != 0) {
1844                     throw new IOException("Not implemented");
1845                 }
1846                 if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) {
1847                     throw new IOException("invalid access dates size");
1848                 }
1849                 break;
1850             }
1851             case NID.kMTime: {
1852                 final int timesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality();
1853                 final int external = getUnsignedByte(header);
1854                 if (external != 0) {
1855                     throw new IOException("Not implemented");
1856                 }
1857                 if (skipBytesFully(header, 8 * timesDefined) < 8 * timesDefined) {
1858                     throw new IOException("invalid modification dates size");
1859                 }
1860                 break;
1861             }
1862             case NID.kWinAttributes: {
1863                 final int attributesDefined = readAllOrBits(header, stats.numberOfEntries).cardinality();
1864                 final int external = getUnsignedByte(header);
1865                 if (external != 0) {
1866                     throw new IOException("Not implemented");
1867                 }
1868                 if (skipBytesFully(header, 4 * attributesDefined) < 4 * attributesDefined) {
1869                     throw new IOException("invalid windows attributes size");
1870                 }
1871                 break;
1872             }
1873             case NID.kStartPos: {
1874                 throw new IOException("kStartPos is unsupported, please report");
1875             }
1876             case NID.kDummy: {
1877                 // 7z 9.20 asserts the content is all zeros and ignores the property
1878                 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1879 
1880                 if (skipBytesFully(header, size) < size) {
1881                     throw new IOException("Incomplete kDummy property");
1882                 }
1883                 break;
1884             }
1885 
1886             default: {
1887                 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1888                 if (skipBytesFully(header, size) < size) {
1889                     throw new IOException("Incomplete property of type " + propertyType);
1890                 }
1891                 break;
1892             }
1893             }
1894         }
1895         stats.numberOfEntriesWithStream = stats.numberOfEntries - Math.max(emptyStreams, 0);
1896     }
1897 
1898     private int sanityCheckFolder(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
1899 
1900         final int numCoders = assertFitsIntoNonNegativeInt("numCoders", readUint64(header));
1901         if (numCoders == 0) {
1902             throw new IOException("Folder without coders");
1903         }
1904         stats.numberOfCoders += numCoders;
1905 
1906         long totalOutStreams = 0;
1907         long totalInStreams = 0;
1908         for (int i = 0; i < numCoders; i++) {
1909             final int bits = getUnsignedByte(header);
1910             final int idSize = bits & 0xf;
1911             get(header, new byte[idSize]);
1912 
1913             final boolean isSimple = (bits & 0x10) == 0;
1914             final boolean hasAttributes = (bits & 0x20) != 0;
1915             final boolean moreAlternativeMethods = (bits & 0x80) != 0;
1916             if (moreAlternativeMethods) {
1917                 throw new IOException("Alternative methods are unsupported, please report. " + // NOSONAR
1918                         "The reference implementation doesn't support them either.");
1919             }
1920 
1921             if (isSimple) {
1922                 totalInStreams++;
1923                 totalOutStreams++;
1924             } else {
1925                 totalInStreams += assertFitsIntoNonNegativeInt("numInStreams", readUint64(header));
1926                 totalOutStreams += assertFitsIntoNonNegativeInt("numOutStreams", readUint64(header));
1927             }
1928 
1929             if (hasAttributes) {
1930                 final int propertiesSize = assertFitsIntoNonNegativeInt("propertiesSize", readUint64(header));
1931                 if (skipBytesFully(header, propertiesSize) < propertiesSize) {
1932                     throw new IOException("invalid propertiesSize in folder");
1933                 }
1934             }
1935         }
1936         assertFitsIntoNonNegativeInt("totalInStreams", totalInStreams);
1937         assertFitsIntoNonNegativeInt("totalOutStreams", totalOutStreams);
1938         stats.numberOfOutStreams += totalOutStreams;
1939         stats.numberOfInStreams += totalInStreams;
1940 
1941         if (totalOutStreams == 0) {
1942             throw new IOException("Total output streams can't be 0");
1943         }
1944 
1945         final int numBindPairs = assertFitsIntoNonNegativeInt("numBindPairs", totalOutStreams - 1);
1946         if (totalInStreams < numBindPairs) {
1947             throw new IOException("Total input streams can't be less than the number of bind pairs");
1948         }
1949         final BitSet inStreamsBound = new BitSet((int) totalInStreams);
1950         for (int i = 0; i < numBindPairs; i++) {
1951             final int inIndex = assertFitsIntoNonNegativeInt("inIndex", readUint64(header));
1952             if (totalInStreams <= inIndex) {
1953                 throw new IOException("inIndex is bigger than number of inStreams");
1954             }
1955             inStreamsBound.set(inIndex);
1956             final int outIndex = assertFitsIntoNonNegativeInt("outIndex", readUint64(header));
1957             if (totalOutStreams <= outIndex) {
1958                 throw new IOException("outIndex is bigger than number of outStreams");
1959             }
1960         }
1961 
1962         final int numPackedStreams = assertFitsIntoNonNegativeInt("numPackedStreams", totalInStreams - numBindPairs);
1963 
1964         if (numPackedStreams == 1) {
1965             if (inStreamsBound.nextClearBit(0) == -1) {
1966                 throw new IOException("Couldn't find stream's bind pair index");
1967             }
1968         } else {
1969             for (int i = 0; i < numPackedStreams; i++) {
1970                 final int packedStreamIndex = assertFitsIntoNonNegativeInt("packedStreamIndex", readUint64(header));
1971                 if (packedStreamIndex >= totalInStreams) {
1972                     throw new IOException("packedStreamIndex is bigger than number of totalInStreams");
1973                 }
1974             }
1975         }
1976 
1977         return (int) totalOutStreams;
1978     }
1979 
1980     private void sanityCheckPackInfo(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
1981         final long packPos = readUint64(header);
1982         if (packPos < 0 || SIGNATURE_HEADER_SIZE + packPos > channel.size() || SIGNATURE_HEADER_SIZE + packPos < 0) {
1983             throw new IOException("packPos (" + packPos + ") is out of range");
1984         }
1985         final long numPackStreams = readUint64(header);
1986         stats.numberOfPackedStreams = assertFitsIntoNonNegativeInt("numPackStreams", numPackStreams);
1987         int nid = getUnsignedByte(header);
1988         if (nid == NID.kSize) {
1989             long totalPackSizes = 0;
1990             for (int i = 0; i < stats.numberOfPackedStreams; i++) {
1991                 final long packSize = readUint64(header);
1992                 totalPackSizes += packSize;
1993                 final long endOfPackStreams = SIGNATURE_HEADER_SIZE + packPos + totalPackSizes;
1994                 if (packSize < 0 || endOfPackStreams > channel.size() || endOfPackStreams < packPos) {
1995                     throw new IOException("packSize (" + packSize + ") is out of range");
1996                 }
1997             }
1998             nid = getUnsignedByte(header);
1999         }
2000 
2001         if (nid == NID.kCRC) {
2002             final int crcsDefined = readAllOrBits(header, stats.numberOfPackedStreams).cardinality();
2003             if (skipBytesFully(header, 4 * crcsDefined) < 4 * crcsDefined) {
2004                 throw new IOException("invalid number of CRCs in PackInfo");
2005             }
2006             nid = getUnsignedByte(header);
2007         }
2008 
2009         if (nid != NID.kEnd) {
2010             throw new IOException("Badly terminated PackInfo (" + nid + ")");
2011         }
2012     }
2013 
2014     private void sanityCheckStreamsInfo(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
2015         int nid = getUnsignedByte(header);
2016 
2017         if (nid == NID.kPackInfo) {
2018             sanityCheckPackInfo(header, stats);
2019             nid = getUnsignedByte(header);
2020         }
2021 
2022         if (nid == NID.kUnpackInfo) {
2023             sanityCheckUnpackInfo(header, stats);
2024             nid = getUnsignedByte(header);
2025         }
2026 
2027         if (nid == NID.kSubStreamsInfo) {
2028             sanityCheckSubStreamsInfo(header, stats);
2029             nid = getUnsignedByte(header);
2030         }
2031 
2032         if (nid != NID.kEnd) {
2033             throw new IOException("Badly terminated StreamsInfo");
2034         }
2035     }
2036 
2037     private void sanityCheckSubStreamsInfo(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
2038 
2039         int nid = getUnsignedByte(header);
2040         final List<Integer> numUnpackSubStreamsPerFolder = new LinkedList<>();
2041         if (nid == NID.kNumUnpackStream) {
2042             for (int i = 0; i < stats.numberOfFolders; i++) {
2043                 numUnpackSubStreamsPerFolder.add(assertFitsIntoNonNegativeInt("numStreams", readUint64(header)));
2044             }
2045             stats.numberOfUnpackSubStreams = numUnpackSubStreamsPerFolder.stream().mapToLong(Integer::longValue).sum();
2046             nid = getUnsignedByte(header);
2047         } else {
2048             stats.numberOfUnpackSubStreams = stats.numberOfFolders;
2049         }
2050 
2051         assertFitsIntoNonNegativeInt("totalUnpackStreams", stats.numberOfUnpackSubStreams);
2052 
2053         if (nid == NID.kSize) {
2054             for (final int numUnpackSubStreams : numUnpackSubStreamsPerFolder) {
2055                 if (numUnpackSubStreams == 0) {
2056                     continue;
2057                 }
2058                 for (int i = 0; i < numUnpackSubStreams - 1; i++) {
2059                     final long size = readUint64(header);
2060                     if (size < 0) {
2061                         throw new IOException("negative unpackSize");
2062                     }
2063                 }
2064             }
2065             nid = getUnsignedByte(header);
2066         }
2067 
2068         int numDigests = 0;
2069         if (numUnpackSubStreamsPerFolder.isEmpty()) {
2070             numDigests = stats.folderHasCrc == null ? stats.numberOfFolders : stats.numberOfFolders - stats.folderHasCrc.cardinality();
2071         } else {
2072             int folderIdx = 0;
2073             for (final int numUnpackSubStreams : numUnpackSubStreamsPerFolder) {
2074                 if (numUnpackSubStreams != 1 || stats.folderHasCrc == null || !stats.folderHasCrc.get(folderIdx++)) {
2075                     numDigests += numUnpackSubStreams;
2076                 }
2077             }
2078         }
2079 
2080         if (nid == NID.kCRC) {
2081             assertFitsIntoNonNegativeInt("numDigests", numDigests);
2082             final int missingCrcs = readAllOrBits(header, numDigests).cardinality();
2083             if (skipBytesFully(header, 4 * missingCrcs) < 4 * missingCrcs) {
2084                 throw new IOException("invalid number of missing CRCs in SubStreamInfo");
2085             }
2086             nid = getUnsignedByte(header);
2087         }
2088 
2089         if (nid != NID.kEnd) {
2090             throw new IOException("Badly terminated SubStreamsInfo");
2091         }
2092     }
2093 
2094     private void sanityCheckUnpackInfo(final ByteBuffer header, final ArchiveStatistics stats) throws IOException {
2095         int nid = getUnsignedByte(header);
2096         if (nid != NID.kFolder) {
2097             throw new IOException("Expected kFolder, got " + nid);
2098         }
2099         final long numFolders = readUint64(header);
2100         stats.numberOfFolders = assertFitsIntoNonNegativeInt("numFolders", numFolders);
2101         final int external = getUnsignedByte(header);
2102         if (external != 0) {
2103             throw new IOException("External unsupported");
2104         }
2105 
2106         final List<Integer> numberOfOutputStreamsPerFolder = new LinkedList<>();
2107         for (int i = 0; i < stats.numberOfFolders; i++) {
2108             numberOfOutputStreamsPerFolder.add(sanityCheckFolder(header, stats));
2109         }
2110 
2111         final long totalNumberOfBindPairs = stats.numberOfOutStreams - stats.numberOfFolders;
2112         final long packedStreamsRequiredByFolders = stats.numberOfInStreams - totalNumberOfBindPairs;
2113         if (packedStreamsRequiredByFolders < stats.numberOfPackedStreams) {
2114             throw new IOException("archive doesn't contain enough packed streams");
2115         }
2116 
2117         nid = getUnsignedByte(header);
2118         if (nid != NID.kCodersUnpackSize) {
2119             throw new IOException("Expected kCodersUnpackSize, got " + nid);
2120         }
2121 
2122         for (final int numberOfOutputStreams : numberOfOutputStreamsPerFolder) {
2123             for (int i = 0; i < numberOfOutputStreams; i++) {
2124                 final long unpackSize = readUint64(header);
2125                 if (unpackSize < 0) {
2126                     throw new IllegalArgumentException("negative unpackSize");
2127                 }
2128             }
2129         }
2130 
2131         nid = getUnsignedByte(header);
2132         if (nid == NID.kCRC) {
2133             stats.folderHasCrc = readAllOrBits(header, stats.numberOfFolders);
2134             final int crcsDefined = stats.folderHasCrc.cardinality();
2135             if (skipBytesFully(header, 4 * crcsDefined) < 4 * crcsDefined) {
2136                 throw new IOException("invalid number of CRCs in UnpackInfo");
2137             }
2138             nid = getUnsignedByte(header);
2139         }
2140 
2141         if (nid != NID.kEnd) {
2142             throw new IOException("Badly terminated UnpackInfo");
2143         }
2144     }
2145 
2146     /**
2147      * Skips all the entries if needed. Entries need to be skipped when:
2148      * <p>
2149      * 1. it's a random access 2. one of these 2 condition is meet :
2150      * </p>
2151      * <p>
2152      * 2.1 currentEntryIndex != entryIndex : this means there are some entries to be skipped(currentEntryIndex < entryIndex) or the entry has already been
2153      * read(currentEntryIndex > entryIndex)
2154      * </p>
2155      * <p>
2156      * 2.2 currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead: if the entry to be read is the current entry, but some data of it has been read before,
2157      * then we need to reopen the stream of the folder and skip all the entries before the current entries
2158      * </p>
2159      *
2160      * @param entryIndex     the entry to be read
2161      * @param isInSameFolder are the entry to be read and the current entry in the same folder
2162      * @param folderIndex    the index of the folder which contains the entry
2163      * @return true if there are entries actually skipped
2164      * @throws IOException there are exceptions when skipping entries
2165      * @since 1.21
2166      */
2167     private boolean skipEntriesWhenNeeded(final int entryIndex, final boolean isInSameFolder, final int folderIndex) throws IOException {
2168         final SevenZArchiveEntry file = archive.files[entryIndex];
2169         // if the entry to be read is the current entry, and the entry has not
2170         // been read yet, then there's nothing we need to do
2171         if (currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead()) {
2172             return false;
2173         }
2174 
2175         // 1. if currentEntryIndex < entryIndex :
2176         // this means there are some entries to be skipped(currentEntryIndex < entryIndex)
2177         // 2. if currentEntryIndex > entryIndex || (currentEntryIndex == entryIndex && hasCurrentEntryBeenRead) :
2178         // this means the entry has already been read before, and we need to reopen the
2179         // stream of the folder and skip all the entries before the current entries
2180         int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[currentFolderIndex];
2181         if (isInSameFolder) {
2182             if (currentEntryIndex < entryIndex) {
2183                 // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped
2184                 filesToSkipStartIndex = currentEntryIndex + 1;
2185             } else {
2186                 // the entry is in the same folder of current entry, but it has already been read before, we need to reset
2187                 // the position of the currentFolderInputStream to the beginning of folder, and then skip the files
2188                 // from the start entry of the folder again
2189                 reopenFolderInputStream(folderIndex, file);
2190             }
2191         }
2192 
2193         for (int i = filesToSkipStartIndex; i < entryIndex; i++) {
2194             final SevenZArchiveEntry fileToSkip = archive.files[i];
2195             InputStream fileStreamToSkip = BoundedInputStream.builder()
2196                     .setInputStream(currentFolderInputStream)
2197                     .setMaxCount(fileToSkip.getSize())
2198                     .setPropagateClose(false)
2199                     .get();
2200             if (fileToSkip.getHasCrc()) {
2201                 // @formatter:off
2202                 fileStreamToSkip = ChecksumInputStream.builder()
2203                         .setChecksum(new CRC32())
2204                         .setInputStream(fileStreamToSkip)
2205                         .setCountThreshold(fileToSkip.getSize())
2206                         .setExpectedChecksumValue(fileToSkip.getCrcValue())
2207                         .get();
2208                 // @formatter:on
2209             }
2210             deferredBlockStreams.add(fileStreamToSkip);
2211 
2212             // set the content methods as well, it equals to file.getContentMethods() because they are in same folder
2213             fileToSkip.setContentMethods(file.getContentMethods());
2214         }
2215         return true;
2216     }
2217 
2218     @Override
2219     public String toString() {
2220         return archive.toString();
2221     }
2222 
2223     private Archive tryToLocateEndHeader(final byte[] password) throws IOException {
2224         final ByteBuffer nidBuf = ByteBuffer.allocate(1);
2225         final long searchLimit = 1024L * 1024 * 1;
2226         // Main header, plus bytes that readStartHeader would read
2227         final long previousDataSize = channel.position() + 20;
2228         final long minPos;
2229         // Determine minimal position - can't start before current position
2230         if (channel.position() + searchLimit > channel.size()) {
2231             minPos = channel.position();
2232         } else {
2233             minPos = channel.size() - searchLimit;
2234         }
2235         long pos = channel.size() - 1;
2236         // Loop: Try from end of archive
2237         while (pos > minPos) {
2238             pos--;
2239             channel.position(pos);
2240             nidBuf.rewind();
2241             if (channel.read(nidBuf) < 1) {
2242                 throw new EOFException();
2243             }
2244             final int nid = nidBuf.array()[0];
2245             // First indicator: Byte equals one of these header identifiers
2246             if (nid == NID.kEncodedHeader || nid == NID.kHeader) {
2247                 try {
2248                     // Try to initialize Archive structure from here
2249                     final long nextHeaderOffset = pos - previousDataSize;
2250                     final long nextHeaderSize = channel.size() - pos;
2251                     final StartHeader startHeader = new StartHeader(nextHeaderOffset, nextHeaderSize, 0);
2252                     final Archive result = initializeArchive(startHeader, password, false);
2253                     // Sanity check: There must be some data...
2254                     if (result.packSizes.length > 0 && result.files.length > 0) {
2255                         return result;
2256                     }
2257                 } catch (final Exception ignored) {
2258                     // Wrong guess...
2259                 }
2260             }
2261         }
2262         throw new IOException("Start header corrupt and unable to guess end header");
2263     }
2264 }