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