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