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