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