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 java.io.BufferedInputStream;
021import java.io.ByteArrayInputStream;
022import java.io.Closeable;
023import java.io.DataInputStream;
024import java.io.File;
025import java.io.FilterInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.nio.ByteBuffer;
029import java.nio.ByteOrder;
030import java.nio.CharBuffer;
031import java.nio.channels.SeekableByteChannel;
032import java.nio.charset.StandardCharsets;
033import java.nio.charset.CharsetEncoder;
034import java.nio.file.Files;
035import java.nio.file.StandardOpenOption;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.BitSet;
039import java.util.EnumSet;
040import java.util.LinkedList;
041import java.util.zip.CRC32;
042
043import org.apache.commons.compress.utils.BoundedInputStream;
044import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
045import org.apache.commons.compress.utils.CharsetNames;
046import org.apache.commons.compress.utils.IOUtils;
047import org.apache.commons.compress.utils.InputStreamStatistics;
048
049/**
050 * Reads a 7z file, using SeekableByteChannel under
051 * the covers.
052 * <p>
053 * The 7z file format is a flexible container
054 * that can contain many compression and
055 * encryption types, but at the moment only
056 * only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256
057 * are supported.
058 * <p>
059 * The format is very Windows/Intel specific,
060 * so it uses little-endian byte order,
061 * doesn't store user/group or permission bits,
062 * and represents times using NTFS timestamps
063 * (100 nanosecond units since 1 January 1601).
064 * Hence the official tools recommend against
065 * using it for backup purposes on *nix, and
066 * recommend .tar.7z or .tar.lzma or .tar.xz
067 * instead.
068 * <p>
069 * Both the header and file contents may be
070 * compressed and/or encrypted. With both
071 * encrypted, neither file names nor file
072 * contents can be read, but the use of
073 * encryption isn't plausibly deniable.
074 *
075 * <p>Multi volume archives can be read by concatenating the parts in
076 * correct order - either manually or by using {link
077 * org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel}
078 * for example.</p>
079 *
080 * @NotThreadSafe
081 * @since 1.6
082 */
083public class SevenZFile implements Closeable {
084    static final int SIGNATURE_HEADER_SIZE = 32;
085
086    private static final String DEFAULT_FILE_NAME = "unknown archive";
087
088    private final String fileName;
089    private SeekableByteChannel channel;
090    private final Archive archive;
091    private int currentEntryIndex = -1;
092    private int currentFolderIndex = -1;
093    private InputStream currentFolderInputStream = null;
094    private byte[] password;
095    private final SevenZFileOptions options;
096
097    private long compressedBytesReadFromCurrentEntry;
098    private long uncompressedBytesReadFromCurrentEntry;
099
100    private final ArrayList<InputStream> deferredBlockStreams = new ArrayList<>();
101
102    // shared with SevenZOutputFile and tests, neither mutates it
103    static final byte[] sevenZSignature = { //NOSONAR
104        (byte)'7', (byte)'z', (byte)0xBC, (byte)0xAF, (byte)0x27, (byte)0x1C
105    };
106
107    /**
108     * Reads a file as 7z archive
109     *
110     * @param fileName the file to read
111     * @param password optional password if the archive is encrypted
112     * @throws IOException if reading the archive fails
113     * @since 1.17
114     */
115    public SevenZFile(final File fileName, final char[] password) throws IOException {
116        this(fileName, password, SevenZFileOptions.DEFAULT);
117    }
118
119    /**
120     * Reads a file as 7z archive with additional options.
121     *
122     * @param fileName the file to read
123     * @param password optional password if the archive is encrypted
124     * @param options the options to apply
125     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
126     * @since 1.19
127     */
128    public SevenZFile(final File fileName, final char[] password, SevenZFileOptions options) throws IOException {
129        this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), // NOSONAR
130                fileName.getAbsolutePath(), utf16Decode(password), true, options);
131    }
132
133    /**
134     * Reads a file as 7z archive
135     *
136     * @param fileName the file to read
137     * @param password optional password if the archive is encrypted -
138     * the byte array is supposed to be the UTF16-LE encoded
139     * representation of the password.
140     * @throws IOException if reading the archive fails
141     * @deprecated use the char[]-arg version for the password instead
142     */
143    @Deprecated
144    public SevenZFile(final File fileName, final byte[] password) throws IOException {
145        this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)),
146                fileName.getAbsolutePath(), password, true, SevenZFileOptions.DEFAULT);
147    }
148
149    /**
150     * Reads a SeekableByteChannel as 7z archive
151     *
152     * <p>{@link
153     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
154     * allows you to read from an in-memory archive.</p>
155     *
156     * @param channel the channel to read
157     * @throws IOException if reading the archive fails
158     * @since 1.13
159     */
160    public SevenZFile(final SeekableByteChannel channel) throws IOException {
161        this(channel, SevenZFileOptions.DEFAULT);
162    }
163
164    /**
165     * Reads a SeekableByteChannel as 7z archive with addtional options.
166     *
167     * <p>{@link
168     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
169     * allows you to read from an in-memory archive.</p>
170     *
171     * @param channel the channel to read
172     * @param options the options to apply
173     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
174     * @since 1.19
175     */
176    public SevenZFile(final SeekableByteChannel channel, SevenZFileOptions options) throws IOException {
177        this(channel, DEFAULT_FILE_NAME, (char[]) null, options);
178    }
179
180    /**
181     * Reads a SeekableByteChannel as 7z archive
182     *
183     * <p>{@link
184     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
185     * allows you to read from an in-memory archive.</p>
186     *
187     * @param channel the channel to read
188     * @param password optional password if the archive is encrypted
189     * @throws IOException if reading the archive fails
190     * @since 1.17
191     */
192    public SevenZFile(final SeekableByteChannel channel,
193                      final char[] password) throws IOException {
194        this(channel, password, SevenZFileOptions.DEFAULT);
195    }
196
197    /**
198     * Reads a SeekableByteChannel as 7z archive with additional options.
199     *
200     * <p>{@link
201     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
202     * allows you to read from an in-memory archive.</p>
203     *
204     * @param channel the channel to read
205     * @param password optional password if the archive is encrypted
206     * @param options the options to apply
207     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
208     * @since 1.19
209     */
210    public SevenZFile(final SeekableByteChannel channel, final char[] password, final SevenZFileOptions options)
211            throws IOException {
212        this(channel, DEFAULT_FILE_NAME, password, options);
213    }
214
215    /**
216     * Reads a SeekableByteChannel as 7z archive
217     *
218     * <p>{@link
219     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
220     * allows you to read from an in-memory archive.</p>
221     *
222     * @param channel the channel to read
223     * @param fileName name of the archive - only used for error reporting
224     * @param password optional password if the archive is encrypted
225     * @throws IOException if reading the archive fails
226     * @since 1.17
227     */
228    public SevenZFile(final SeekableByteChannel channel, String fileName,
229                      final char[] password) throws IOException {
230        this(channel, fileName, password, SevenZFileOptions.DEFAULT);
231    }
232
233    /**
234     * Reads a SeekableByteChannel as 7z archive with addtional options.
235     *
236     * <p>{@link
237     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
238     * allows you to read from an in-memory archive.</p>
239     *
240     * @param channel the channel to read
241     * @param fileName name of the archive - only used for error reporting
242     * @param password optional password if the archive is encrypted
243     * @param options the options to apply
244     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
245     * @since 1.19
246     */
247    public SevenZFile(final SeekableByteChannel channel, String fileName, final char[] password,
248            final SevenZFileOptions options) throws IOException {
249        this(channel, fileName, utf16Decode(password), false, options);
250    }
251
252    /**
253     * Reads a SeekableByteChannel as 7z archive
254     *
255     * <p>{@link
256     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
257     * allows you to read from an in-memory archive.</p>
258     *
259     * @param channel the channel to read
260     * @param fileName name of the archive - only used for error reporting
261     * @throws IOException if reading the archive fails
262     * @since 1.17
263     */
264    public SevenZFile(final SeekableByteChannel channel, String fileName)
265        throws IOException {
266        this(channel, fileName, SevenZFileOptions.DEFAULT);
267    }
268
269    /**
270     * Reads a SeekableByteChannel as 7z archive with additional options.
271     *
272     * <p>{@link
273     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
274     * allows you to read from an in-memory archive.</p>
275     *
276     * @param channel the channel to read
277     * @param fileName name of the archive - only used for error reporting
278     * @param options the options to apply
279     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
280     * @since 1.19
281     */
282    public SevenZFile(final SeekableByteChannel channel, String fileName, final SevenZFileOptions options)
283            throws IOException {
284        this(channel, fileName, null, false, options);
285    }
286
287    /**
288     * Reads a SeekableByteChannel as 7z archive
289     *
290     * <p>{@link
291     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
292     * allows you to read from an in-memory archive.</p>
293     *
294     * @param channel the channel to read
295     * @param password optional password if the archive is encrypted -
296     * the byte array is supposed to be the UTF16-LE encoded
297     * representation of the password.
298     * @throws IOException if reading the archive fails
299     * @since 1.13
300     * @deprecated use the char[]-arg version for the password instead
301     */
302    @Deprecated
303    public SevenZFile(final SeekableByteChannel channel,
304                      final byte[] password) throws IOException {
305        this(channel, DEFAULT_FILE_NAME, password);
306    }
307
308    /**
309     * Reads a SeekableByteChannel as 7z archive
310     *
311     * <p>{@link
312     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
313     * allows you to read from an in-memory archive.</p>
314     *
315     * @param channel the channel to read
316     * @param fileName name of the archive - only used for error reporting
317     * @param password optional password if the archive is encrypted -
318     * the byte array is supposed to be the UTF16-LE encoded
319     * representation of the password.
320     * @throws IOException if reading the archive fails
321     * @since 1.13
322     * @deprecated use the char[]-arg version for the password instead
323     */
324    @Deprecated
325    public SevenZFile(final SeekableByteChannel channel, String fileName,
326                      final byte[] password) throws IOException {
327        this(channel, fileName, password, false, SevenZFileOptions.DEFAULT);
328    }
329
330    private SevenZFile(final SeekableByteChannel channel, String filename,
331                       final byte[] password, boolean closeOnError, SevenZFileOptions options) throws IOException {
332        boolean succeeded = false;
333        this.channel = channel;
334        this.fileName = filename;
335        this.options = options;
336        try {
337            archive = readHeaders(password);
338            if (password != null) {
339                this.password = Arrays.copyOf(password, password.length);
340            } else {
341                this.password = null;
342            }
343            succeeded = true;
344        } finally {
345            if (!succeeded && closeOnError) {
346                this.channel.close();
347            }
348        }
349    }
350
351    /**
352     * Reads a file as unencrypted 7z archive
353     *
354     * @param fileName the file to read
355     * @throws IOException if reading the archive fails
356     */
357    public SevenZFile(final File fileName) throws IOException {
358        this(fileName, SevenZFileOptions.DEFAULT);
359    }
360
361    /**
362     * Reads a file as unencrypted 7z archive
363     *
364     * @param fileName the file to read
365     * @param options the options to apply
366     * @throws IOException if reading the archive fails or the memory limit (if set) is too small
367     * @since 1.19
368     */
369    public SevenZFile(final File fileName, final SevenZFileOptions options) throws IOException {
370        this(fileName, (char[]) null, options);
371    }
372
373    /**
374     * Closes the archive.
375     * @throws IOException if closing the file fails
376     */
377    @Override
378    public void close() throws IOException {
379        if (channel != null) {
380            try {
381                channel.close();
382            } finally {
383                channel = null;
384                if (password != null) {
385                    Arrays.fill(password, (byte) 0);
386                }
387                password = null;
388            }
389        }
390    }
391
392    /**
393     * Returns the next Archive Entry in this archive.
394     *
395     * @return the next entry,
396     *         or {@code null} if there are no more entries
397     * @throws IOException if the next entry could not be read
398     */
399    public SevenZArchiveEntry getNextEntry() throws IOException {
400        if (currentEntryIndex >= archive.files.length - 1) {
401            return null;
402        }
403        ++currentEntryIndex;
404        final SevenZArchiveEntry entry = archive.files[currentEntryIndex];
405        if (entry.getName() == null && options.getUseDefaultNameForUnnamedEntries()) {
406            entry.setName(getDefaultName());
407        }
408        buildDecodingStream(currentEntryIndex);
409        uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0;
410        return entry;
411    }
412
413    /**
414     * Returns meta-data of all archive entries.
415     *
416     * <p>This method only provides meta-data, the entries can not be
417     * used to read the contents, you still need to process all
418     * entries in order using {@link #getNextEntry} for that.</p>
419     *
420     * <p>The content methods are only available for entries that have
421     * already been reached via {@link #getNextEntry}.</p>
422     *
423     * @return meta-data of all archive entries.
424     * @since 1.11
425     */
426    public Iterable<SevenZArchiveEntry> getEntries() {
427        return Arrays.asList(archive.files);
428    }
429
430    private Archive readHeaders(final byte[] password) throws IOException {
431        ByteBuffer buf = ByteBuffer.allocate(12 /* signature + 2 bytes version + 4 bytes CRC */)
432            .order(ByteOrder.LITTLE_ENDIAN);
433        readFully(buf);
434        final byte[] signature = new byte[6];
435        buf.get(signature);
436        if (!Arrays.equals(signature, sevenZSignature)) {
437            throw new IOException("Bad 7z signature");
438        }
439        // 7zFormat.txt has it wrong - it's first major then minor
440        final byte archiveVersionMajor = buf.get();
441        final byte archiveVersionMinor = buf.get();
442        if (archiveVersionMajor != 0) {
443            throw new IOException(String.format("Unsupported 7z version (%d,%d)",
444                    archiveVersionMajor, archiveVersionMinor));
445        }
446
447        boolean headerLooksValid = false;  // See https://www.7-zip.org/recover.html - "There is no correct End Header at the end of archive"
448        final long startHeaderCrc = 0xffffFFFFL & buf.getInt();
449        if (startHeaderCrc == 0) {
450            // This is an indication of a corrupt header - peek the next 20 bytes
451            long currentPosition = channel.position();
452            ByteBuffer peekBuf = ByteBuffer.allocate(20);
453            readFully(peekBuf);
454            channel.position(currentPosition);
455            // Header invalid if all data is 0
456            while (peekBuf.hasRemaining()) {
457                if (peekBuf.get()!=0) {
458                    headerLooksValid = true;
459                    break;
460                }
461            }
462        } else {
463            headerLooksValid = true;
464        }
465
466        if (headerLooksValid) {
467            final StartHeader startHeader = readStartHeader(startHeaderCrc);
468            return initializeArchive(startHeader, password, true);
469        } else {
470            // No valid header found - probably first file of multipart archive was removed too early. Scan for end header.
471            return tryToLocateEndHeader(password);
472        }
473    }
474
475    private Archive tryToLocateEndHeader(final byte[] password) throws IOException {
476        ByteBuffer nidBuf = ByteBuffer.allocate(1);
477        final long searchLimit = 1024l * 1024 * 1;
478        // Main header, plus bytes that readStartHeader would read
479        final long previousDataSize = channel.position() + 20;
480        final long minPos;
481        // Determine minimal position - can't start before current position
482        if (channel.position() + searchLimit > channel.size()) {
483            minPos = channel.position();
484        } else {
485            minPos = channel.size() - searchLimit;
486        }
487        long pos = channel.size() - 1;
488        // Loop: Try from end of archive
489        while (pos > minPos) {
490            pos--;
491            channel.position(pos);
492            nidBuf.rewind();
493            channel.read(nidBuf);
494            int nid = nidBuf.array()[0];
495            // First indicator: Byte equals one of these header identifiers
496            if (nid == NID.kEncodedHeader || nid == NID.kHeader) {
497                try {
498                    // Try to initialize Archive structure from here
499                    final StartHeader startHeader = new StartHeader();
500                    startHeader.nextHeaderOffset = pos - previousDataSize;
501                    startHeader.nextHeaderSize = channel.size() - pos;
502                    Archive result = initializeArchive(startHeader, password, false);
503                    // Sanity check: There must be some data...
504                    if (result.packSizes != null && result.files.length > 0) {
505                        return result;
506                    }
507                } catch (Exception ignore) {
508                    // Wrong guess...
509                }
510            }
511        }
512        throw new IOException("Start header corrupt and unable to guess end header");
513    }
514
515    private Archive initializeArchive(StartHeader startHeader, final byte[] password, boolean verifyCrc) throws IOException {
516        assertFitsIntoInt("nextHeaderSize", startHeader.nextHeaderSize);
517        final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize;
518        channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset);
519        ByteBuffer buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN);
520        readFully(buf);
521        if (verifyCrc) {
522            final CRC32 crc = new CRC32();
523            crc.update(buf.array());
524            if (startHeader.nextHeaderCrc != crc.getValue()) {
525                throw new IOException("NextHeader CRC mismatch");
526            }
527        }
528
529        Archive archive = new Archive();
530        int nid = getUnsignedByte(buf);
531        if (nid == NID.kEncodedHeader) {
532            buf = readEncodedHeader(buf, archive, password);
533            // Archive gets rebuilt with the new header
534            archive = new Archive();
535            nid = getUnsignedByte(buf);
536        }
537        if (nid == NID.kHeader) {
538            readHeader(buf, archive);
539        } else {
540            throw new IOException("Broken or unsupported archive: no Header");
541        }
542        return archive;
543    }
544
545    private StartHeader readStartHeader(final long startHeaderCrc) throws IOException {
546        final StartHeader startHeader = new StartHeader();
547        // using Stream rather than ByteBuffer for the benefit of the
548        // built-in CRC check
549        try (DataInputStream dataInputStream = new DataInputStream(new CRC32VerifyingInputStream(
550                new BoundedSeekableByteChannelInputStream(channel, 20), 20, startHeaderCrc))) {
551             startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong());
552             startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong());
553             startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt());
554             return startHeader;
555        }
556    }
557
558    private void readHeader(final ByteBuffer header, final Archive archive) throws IOException {
559        int nid = getUnsignedByte(header);
560
561        if (nid == NID.kArchiveProperties) {
562            readArchiveProperties(header);
563            nid = getUnsignedByte(header);
564        }
565
566        if (nid == NID.kAdditionalStreamsInfo) {
567            throw new IOException("Additional streams unsupported");
568            //nid = header.readUnsignedByte();
569        }
570
571        if (nid == NID.kMainStreamsInfo) {
572            readStreamsInfo(header, archive);
573            nid = getUnsignedByte(header);
574        }
575
576        if (nid == NID.kFilesInfo) {
577            readFilesInfo(header, archive);
578            nid = getUnsignedByte(header);
579        }
580
581        if (nid != NID.kEnd) {
582            throw new IOException("Badly terminated header, found " + nid);
583        }
584    }
585
586    private void readArchiveProperties(final ByteBuffer input) throws IOException {
587        // FIXME: the reference implementation just throws them away?
588        int nid =  getUnsignedByte(input);
589        while (nid != NID.kEnd) {
590            final long propertySize = readUint64(input);
591            assertFitsIntoInt("propertySize", propertySize);
592            final byte[] property = new byte[(int)propertySize];
593            input.get(property);
594            nid = getUnsignedByte(input);
595        }
596    }
597
598    private ByteBuffer readEncodedHeader(final ByteBuffer header, final Archive archive,
599                                         final byte[] password) throws IOException {
600        readStreamsInfo(header, archive);
601
602        // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage?
603        final Folder folder = archive.folders[0];
604        final int firstPackStreamIndex = 0;
605        final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
606                0;
607
608        channel.position(folderOffset);
609        InputStream inputStreamStack = new BoundedSeekableByteChannelInputStream(channel,
610                archive.packSizes[firstPackStreamIndex]);
611        for (final Coder coder : folder.getOrderedCoders()) {
612            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
613                throw new IOException("Multi input/output stream coders are not yet supported");
614            }
615            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack, //NOSONAR
616                    folder.getUnpackSizeForCoder(coder), coder, password, options.getMaxMemoryLimitInKb());
617        }
618        if (folder.hasCrc) {
619            inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack,
620                    folder.getUnpackSize(), folder.crc);
621        }
622        assertFitsIntoInt("unpackSize", folder.getUnpackSize());
623        final byte[] nextHeader = new byte[(int)folder.getUnpackSize()];
624        try (DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack)) {
625            nextHeaderInputStream.readFully(nextHeader);
626        }
627        return ByteBuffer.wrap(nextHeader).order(ByteOrder.LITTLE_ENDIAN);
628    }
629
630    private void readStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException {
631        int nid = getUnsignedByte(header);
632
633        if (nid == NID.kPackInfo) {
634            readPackInfo(header, archive);
635            nid = getUnsignedByte(header);
636        }
637
638        if (nid == NID.kUnpackInfo) {
639            readUnpackInfo(header, archive);
640            nid = getUnsignedByte(header);
641        } else {
642            // archive without unpack/coders info
643            archive.folders = new Folder[0];
644        }
645
646        if (nid == NID.kSubStreamsInfo) {
647            readSubStreamsInfo(header, archive);
648            nid = getUnsignedByte(header);
649        }
650
651        if (nid != NID.kEnd) {
652            throw new IOException("Badly terminated StreamsInfo");
653        }
654    }
655
656    private void readPackInfo(final ByteBuffer header, final Archive archive) throws IOException {
657        archive.packPos = readUint64(header);
658        final long numPackStreams = readUint64(header);
659        assertFitsIntoInt("numPackStreams", numPackStreams);
660        final int numPackStreamsInt = (int) numPackStreams;
661        int nid = getUnsignedByte(header);
662        if (nid == NID.kSize) {
663            archive.packSizes = new long[numPackStreamsInt];
664            for (int i = 0; i < archive.packSizes.length; i++) {
665                archive.packSizes[i] = readUint64(header);
666            }
667            nid = getUnsignedByte(header);
668        }
669
670        if (nid == NID.kCRC) {
671            archive.packCrcsDefined = readAllOrBits(header, numPackStreamsInt);
672            archive.packCrcs = new long[numPackStreamsInt];
673            for (int i = 0; i < numPackStreamsInt; i++) {
674                if (archive.packCrcsDefined.get(i)) {
675                    archive.packCrcs[i] = 0xffffFFFFL & header.getInt();
676                }
677            }
678
679            nid = getUnsignedByte(header);
680        }
681
682        if (nid != NID.kEnd) {
683            throw new IOException("Badly terminated PackInfo (" + nid + ")");
684        }
685    }
686
687    private void readUnpackInfo(final ByteBuffer header, final Archive archive) throws IOException {
688        int nid = getUnsignedByte(header);
689        if (nid != NID.kFolder) {
690            throw new IOException("Expected kFolder, got " + nid);
691        }
692        final long numFolders = readUint64(header);
693        assertFitsIntoInt("numFolders", numFolders);
694        final int numFoldersInt = (int) numFolders;
695        final Folder[] folders = new Folder[numFoldersInt];
696        archive.folders = folders;
697        final int external = getUnsignedByte(header);
698        if (external != 0) {
699            throw new IOException("External unsupported");
700        }
701        for (int i = 0; i < numFoldersInt; i++) {
702            folders[i] = readFolder(header);
703        }
704
705        nid = getUnsignedByte(header);
706        if (nid != NID.kCodersUnpackSize) {
707            throw new IOException("Expected kCodersUnpackSize, got " + nid);
708        }
709        for (final Folder folder : folders) {
710            assertFitsIntoInt("totalOutputStreams", folder.totalOutputStreams);
711            folder.unpackSizes = new long[(int)folder.totalOutputStreams];
712            for (int i = 0; i < folder.totalOutputStreams; i++) {
713                folder.unpackSizes[i] = readUint64(header);
714            }
715        }
716
717        nid = getUnsignedByte(header);
718        if (nid == NID.kCRC) {
719            final BitSet crcsDefined = readAllOrBits(header, numFoldersInt);
720            for (int i = 0; i < numFoldersInt; i++) {
721                if (crcsDefined.get(i)) {
722                    folders[i].hasCrc = true;
723                    folders[i].crc = 0xffffFFFFL & header.getInt();
724                } else {
725                    folders[i].hasCrc = false;
726                }
727            }
728
729            nid = getUnsignedByte(header);
730        }
731
732        if (nid != NID.kEnd) {
733            throw new IOException("Badly terminated UnpackInfo");
734        }
735    }
736
737    private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException {
738        for (final Folder folder : archive.folders) {
739            folder.numUnpackSubStreams = 1;
740        }
741        int totalUnpackStreams = archive.folders.length;
742
743        int nid = getUnsignedByte(header);
744        if (nid == NID.kNumUnpackStream) {
745            totalUnpackStreams = 0;
746            for (final Folder folder : archive.folders) {
747                final long numStreams = readUint64(header);
748                assertFitsIntoInt("numStreams", numStreams);
749                folder.numUnpackSubStreams = (int)numStreams;
750                totalUnpackStreams += numStreams;
751            }
752            nid = getUnsignedByte(header);
753        }
754
755        final SubStreamsInfo subStreamsInfo = new SubStreamsInfo();
756        subStreamsInfo.unpackSizes = new long[totalUnpackStreams];
757        subStreamsInfo.hasCrc = new BitSet(totalUnpackStreams);
758        subStreamsInfo.crcs = new long[totalUnpackStreams];
759
760        int nextUnpackStream = 0;
761        for (final Folder folder : archive.folders) {
762            if (folder.numUnpackSubStreams == 0) {
763                continue;
764            }
765            long sum = 0;
766            if (nid == NID.kSize) {
767                for (int i = 0; i < folder.numUnpackSubStreams - 1; i++) {
768                    final long size = readUint64(header);
769                    subStreamsInfo.unpackSizes[nextUnpackStream++] = size;
770                    sum += size;
771                }
772            }
773            subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum;
774        }
775        if (nid == NID.kSize) {
776            nid = getUnsignedByte(header);
777        }
778
779        int numDigests = 0;
780        for (final Folder folder : archive.folders) {
781            if (folder.numUnpackSubStreams != 1 || !folder.hasCrc) {
782                numDigests += folder.numUnpackSubStreams;
783            }
784        }
785
786        if (nid == NID.kCRC) {
787            final BitSet hasMissingCrc = readAllOrBits(header, numDigests);
788            final long[] missingCrcs = new long[numDigests];
789            for (int i = 0; i < numDigests; i++) {
790                if (hasMissingCrc.get(i)) {
791                    missingCrcs[i] = 0xffffFFFFL & header.getInt();
792                }
793            }
794            int nextCrc = 0;
795            int nextMissingCrc = 0;
796            for (final Folder folder: archive.folders) {
797                if (folder.numUnpackSubStreams == 1 && folder.hasCrc) {
798                    subStreamsInfo.hasCrc.set(nextCrc, true);
799                    subStreamsInfo.crcs[nextCrc] = folder.crc;
800                    ++nextCrc;
801                } else {
802                    for (int i = 0; i < folder.numUnpackSubStreams; i++) {
803                        subStreamsInfo.hasCrc.set(nextCrc, hasMissingCrc.get(nextMissingCrc));
804                        subStreamsInfo.crcs[nextCrc] = missingCrcs[nextMissingCrc];
805                        ++nextCrc;
806                        ++nextMissingCrc;
807                    }
808                }
809            }
810
811            nid = getUnsignedByte(header);
812        }
813
814        if (nid != NID.kEnd) {
815            throw new IOException("Badly terminated SubStreamsInfo");
816        }
817
818        archive.subStreamsInfo = subStreamsInfo;
819    }
820
821    private Folder readFolder(final ByteBuffer header) throws IOException {
822        final Folder folder = new Folder();
823
824        final long numCoders = readUint64(header);
825        assertFitsIntoInt("numCoders", numCoders);
826        final Coder[] coders = new Coder[(int)numCoders];
827        long totalInStreams = 0;
828        long totalOutStreams = 0;
829        for (int i = 0; i < coders.length; i++) {
830            coders[i] = new Coder();
831            final int bits = getUnsignedByte(header);
832            final int idSize = bits & 0xf;
833            final boolean isSimple = (bits & 0x10) == 0;
834            final boolean hasAttributes = (bits & 0x20) != 0;
835            final boolean moreAlternativeMethods = (bits & 0x80) != 0;
836
837            coders[i].decompressionMethodId = new byte[idSize];
838            header.get(coders[i].decompressionMethodId);
839            if (isSimple) {
840                coders[i].numInStreams = 1;
841                coders[i].numOutStreams = 1;
842            } else {
843                coders[i].numInStreams = readUint64(header);
844                coders[i].numOutStreams = readUint64(header);
845            }
846            totalInStreams += coders[i].numInStreams;
847            totalOutStreams += coders[i].numOutStreams;
848            if (hasAttributes) {
849                final long propertiesSize = readUint64(header);
850                assertFitsIntoInt("propertiesSize", propertiesSize);
851                coders[i].properties = new byte[(int)propertiesSize];
852                header.get(coders[i].properties);
853            }
854            // would need to keep looping as above:
855            while (moreAlternativeMethods) {
856                throw new IOException("Alternative methods are unsupported, please report. " + // NOSONAR
857                    "The reference implementation doesn't support them either.");
858            }
859        }
860        folder.coders = coders;
861        assertFitsIntoInt("totalInStreams", totalInStreams);
862        folder.totalInputStreams = totalInStreams;
863        assertFitsIntoInt("totalOutStreams", totalOutStreams);
864        folder.totalOutputStreams = totalOutStreams;
865
866        if (totalOutStreams == 0) {
867            throw new IOException("Total output streams can't be 0");
868        }
869        final long numBindPairs = totalOutStreams - 1;
870        assertFitsIntoInt("numBindPairs", numBindPairs);
871        final BindPair[] bindPairs = new BindPair[(int)numBindPairs];
872        for (int i = 0; i < bindPairs.length; i++) {
873            bindPairs[i] = new BindPair();
874            bindPairs[i].inIndex = readUint64(header);
875            bindPairs[i].outIndex = readUint64(header);
876        }
877        folder.bindPairs = bindPairs;
878
879        if (totalInStreams < numBindPairs) {
880            throw new IOException("Total input streams can't be less than the number of bind pairs");
881        }
882        final long numPackedStreams = totalInStreams - numBindPairs;
883        assertFitsIntoInt("numPackedStreams", numPackedStreams);
884        final long packedStreams[] = new long[(int)numPackedStreams];
885        if (numPackedStreams == 1) {
886            int i;
887            for (i = 0; i < (int)totalInStreams; i++) {
888                if (folder.findBindPairForInStream(i) < 0) {
889                    break;
890                }
891            }
892            if (i == (int)totalInStreams) {
893                throw new IOException("Couldn't find stream's bind pair index");
894            }
895            packedStreams[0] = i;
896        } else {
897            for (int i = 0; i < (int)numPackedStreams; i++) {
898                packedStreams[i] = readUint64(header);
899            }
900        }
901        folder.packedStreams = packedStreams;
902
903        return folder;
904    }
905
906    private BitSet readAllOrBits(final ByteBuffer header, final int size) throws IOException {
907        final int areAllDefined = getUnsignedByte(header);
908        final BitSet bits;
909        if (areAllDefined != 0) {
910            bits = new BitSet(size);
911            for (int i = 0; i < size; i++) {
912                bits.set(i, true);
913            }
914        } else {
915            bits = readBits(header, size);
916        }
917        return bits;
918    }
919
920    private BitSet readBits(final ByteBuffer header, final int size) throws IOException {
921        final BitSet bits = new BitSet(size);
922        int mask = 0;
923        int cache = 0;
924        for (int i = 0; i < size; i++) {
925            if (mask == 0) {
926                mask = 0x80;
927                cache = getUnsignedByte(header);
928            }
929            bits.set(i, (cache & mask) != 0);
930            mask >>>= 1;
931        }
932        return bits;
933    }
934
935    private void readFilesInfo(final ByteBuffer header, final Archive archive) throws IOException {
936        final long numFiles = readUint64(header);
937        assertFitsIntoInt("numFiles", numFiles);
938        final SevenZArchiveEntry[] files = new SevenZArchiveEntry[(int)numFiles];
939        for (int i = 0; i < files.length; i++) {
940            files[i] = new SevenZArchiveEntry();
941        }
942        BitSet isEmptyStream = null;
943        BitSet isEmptyFile = null;
944        BitSet isAnti = null;
945        while (true) {
946            final int propertyType = getUnsignedByte(header);
947            if (propertyType == 0) {
948                break;
949            }
950            final long size = readUint64(header);
951            switch (propertyType) {
952                case NID.kEmptyStream: {
953                    isEmptyStream = readBits(header, files.length);
954                    break;
955                }
956                case NID.kEmptyFile: {
957                    if (isEmptyStream == null) { // protect against NPE
958                        throw new IOException("Header format error: kEmptyStream must appear before kEmptyFile");
959                    }
960                    isEmptyFile = readBits(header, isEmptyStream.cardinality());
961                    break;
962                }
963                case NID.kAnti: {
964                    if (isEmptyStream == null) { // protect against NPE
965                        throw new IOException("Header format error: kEmptyStream must appear before kAnti");
966                    }
967                    isAnti = readBits(header, isEmptyStream.cardinality());
968                    break;
969                }
970                case NID.kName: {
971                    final int external = getUnsignedByte(header);
972                    if (external != 0) {
973                        throw new IOException("Not implemented");
974                    }
975                    if (((size - 1) & 1) != 0) {
976                        throw new IOException("File names length invalid");
977                    }
978                    assertFitsIntoInt("file names length", size - 1);
979                    final byte[] names = new byte[(int)(size - 1)];
980                    header.get(names);
981                    int nextFile = 0;
982                    int nextName = 0;
983                    for (int i = 0; i < names.length; i += 2) {
984                        if (names[i] == 0 && names[i+1] == 0) {
985                            files[nextFile++].setName(new String(names, nextName, i-nextName, CharsetNames.UTF_16LE));
986                            nextName = i + 2;
987                        }
988                    }
989                    if (nextName != names.length || nextFile != files.length) {
990                        throw new IOException("Error parsing file names");
991                    }
992                    break;
993                }
994                case NID.kCTime: {
995                    final BitSet timesDefined = readAllOrBits(header, files.length);
996                    final int external = getUnsignedByte(header);
997                    if (external != 0) {
998                        throw new IOException("Unimplemented");
999                    }
1000                    for (int i = 0; i < files.length; i++) {
1001                        files[i].setHasCreationDate(timesDefined.get(i));
1002                        if (files[i].getHasCreationDate()) {
1003                            files[i].setCreationDate(header.getLong());
1004                        }
1005                    }
1006                    break;
1007                }
1008                case NID.kATime: {
1009                    final BitSet timesDefined = readAllOrBits(header, files.length);
1010                    final int external = getUnsignedByte(header);
1011                    if (external != 0) {
1012                        throw new IOException("Unimplemented");
1013                    }
1014                    for (int i = 0; i < files.length; i++) {
1015                        files[i].setHasAccessDate(timesDefined.get(i));
1016                        if (files[i].getHasAccessDate()) {
1017                            files[i].setAccessDate(header.getLong());
1018                        }
1019                    }
1020                    break;
1021                }
1022                case NID.kMTime: {
1023                    final BitSet timesDefined = readAllOrBits(header, files.length);
1024                    final int external = getUnsignedByte(header);
1025                    if (external != 0) {
1026                        throw new IOException("Unimplemented");
1027                    }
1028                    for (int i = 0; i < files.length; i++) {
1029                        files[i].setHasLastModifiedDate(timesDefined.get(i));
1030                        if (files[i].getHasLastModifiedDate()) {
1031                            files[i].setLastModifiedDate(header.getLong());
1032                        }
1033                    }
1034                    break;
1035                }
1036                case NID.kWinAttributes: {
1037                    final BitSet attributesDefined = readAllOrBits(header, files.length);
1038                    final int external = getUnsignedByte(header);
1039                    if (external != 0) {
1040                        throw new IOException("Unimplemented");
1041                    }
1042                    for (int i = 0; i < files.length; i++) {
1043                        files[i].setHasWindowsAttributes(attributesDefined.get(i));
1044                        if (files[i].getHasWindowsAttributes()) {
1045                            files[i].setWindowsAttributes(header.getInt());
1046                        }
1047                    }
1048                    break;
1049                }
1050                case NID.kStartPos: {
1051                    throw new IOException("kStartPos is unsupported, please report");
1052                }
1053                case NID.kDummy: {
1054                    // 7z 9.20 asserts the content is all zeros and ignores the property
1055                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1056
1057                    if (skipBytesFully(header, size) < size) {
1058                        throw new IOException("Incomplete kDummy property");
1059                    }
1060                    break;
1061                }
1062
1063                default: {
1064                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
1065                    if (skipBytesFully(header, size) < size) {
1066                        throw new IOException("Incomplete property of type " + propertyType);
1067                    }
1068                    break;
1069                }
1070            }
1071        }
1072        int nonEmptyFileCounter = 0;
1073        int emptyFileCounter = 0;
1074        for (int i = 0; i < files.length; i++) {
1075            files[i].setHasStream(isEmptyStream == null || !isEmptyStream.get(i));
1076            if (files[i].hasStream()) {
1077                if (archive.subStreamsInfo == null) {
1078                    throw new IOException("Archive contains file with streams but no subStreamsInfo");
1079                }
1080                files[i].setDirectory(false);
1081                files[i].setAntiItem(false);
1082                files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter));
1083                files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]);
1084                files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]);
1085                ++nonEmptyFileCounter;
1086            } else {
1087                files[i].setDirectory(isEmptyFile == null || !isEmptyFile.get(emptyFileCounter));
1088                files[i].setAntiItem(isAnti != null && isAnti.get(emptyFileCounter));
1089                files[i].setHasCrc(false);
1090                files[i].setSize(0);
1091                ++emptyFileCounter;
1092            }
1093        }
1094        archive.files = files;
1095        calculateStreamMap(archive);
1096    }
1097
1098    private void calculateStreamMap(final Archive archive) throws IOException {
1099        final StreamMap streamMap = new StreamMap();
1100
1101        int nextFolderPackStreamIndex = 0;
1102        final int numFolders = archive.folders != null ? archive.folders.length : 0;
1103        streamMap.folderFirstPackStreamIndex = new int[numFolders];
1104        for (int i = 0; i < numFolders; i++) {
1105            streamMap.folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex;
1106            nextFolderPackStreamIndex += archive.folders[i].packedStreams.length;
1107        }
1108
1109        long nextPackStreamOffset = 0;
1110        final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0;
1111        streamMap.packStreamOffsets = new long[numPackSizes];
1112        for (int i = 0; i < numPackSizes; i++) {
1113            streamMap.packStreamOffsets[i] = nextPackStreamOffset;
1114            nextPackStreamOffset += archive.packSizes[i];
1115        }
1116
1117        streamMap.folderFirstFileIndex = new int[numFolders];
1118        streamMap.fileFolderIndex = new int[archive.files.length];
1119        int nextFolderIndex = 0;
1120        int nextFolderUnpackStreamIndex = 0;
1121        for (int i = 0; i < archive.files.length; i++) {
1122            if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) {
1123                streamMap.fileFolderIndex[i] = -1;
1124                continue;
1125            }
1126            if (nextFolderUnpackStreamIndex == 0) {
1127                for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) {
1128                    streamMap.folderFirstFileIndex[nextFolderIndex] = i;
1129                    if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) {
1130                        break;
1131                    }
1132                }
1133                if (nextFolderIndex >= archive.folders.length) {
1134                    throw new IOException("Too few folders in archive");
1135                }
1136            }
1137            streamMap.fileFolderIndex[i] = nextFolderIndex;
1138            if (!archive.files[i].hasStream()) {
1139                continue;
1140            }
1141            ++nextFolderUnpackStreamIndex;
1142            if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) {
1143                ++nextFolderIndex;
1144                nextFolderUnpackStreamIndex = 0;
1145            }
1146        }
1147
1148        archive.streamMap = streamMap;
1149    }
1150
1151    private void buildDecodingStream(int entryIndex) throws IOException {
1152        if (archive.streamMap == null) {
1153            throw new IOException("Archive doesn't contain stream information to read entries");
1154        }
1155        final int folderIndex = archive.streamMap.fileFolderIndex[entryIndex];
1156        if (folderIndex < 0) {
1157            deferredBlockStreams.clear();
1158            // TODO: previously it'd return an empty stream?
1159            // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0);
1160            return;
1161        }
1162        final SevenZArchiveEntry file = archive.files[entryIndex];
1163        boolean isInSameFolder = false;
1164        final Folder folder = archive.folders[folderIndex];
1165        final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex];
1166        final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
1167                archive.streamMap.packStreamOffsets[firstPackStreamIndex];
1168        if (currentFolderIndex == folderIndex) {
1169            // (COMPRESS-320).
1170            // The current entry is within the same (potentially opened) folder. The
1171            // previous stream has to be fully decoded before we can start reading
1172            // but don't do it eagerly -- if the user skips over the entire folder nothing
1173            // is effectively decompressed.
1174
1175            file.setContentMethods(archive.files[entryIndex - 1].getContentMethods());
1176
1177            // if this is called in a random access, then the content methods of previous entry may be null
1178            // the content methods should be set to methods of the first entry as it must not be null,
1179            // and the content methods would only be set if the content methods was not set
1180            if(currentEntryIndex != entryIndex && file.getContentMethods() == null) {
1181                int folderFirstFileIndex = archive.streamMap.folderFirstFileIndex[folderIndex];
1182                SevenZArchiveEntry folderFirstFile = archive.files[folderFirstFileIndex];
1183                file.setContentMethods(folderFirstFile.getContentMethods());
1184            }
1185            isInSameFolder = true;
1186        } else {
1187            // We're opening a new folder. Discard any queued streams/ folder stream.
1188            currentFolderIndex = folderIndex;
1189            deferredBlockStreams.clear();
1190            if (currentFolderInputStream != null) {
1191                currentFolderInputStream.close();
1192                currentFolderInputStream = null;
1193            }
1194
1195            currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file);
1196        }
1197
1198        // if this mothod is called in a random access, then some entries need to be skipped,
1199        // if the entry of entryIndex is in the same folder of the currentFolderIndex,
1200        // then the entries between (currentEntryIndex + 1) and (entryIndex - 1) need to be skipped,
1201        // otherwise it's a new folder, and the entries between firstFileInFolderIndex and (entryIndex - 1) need to be skipped
1202        if(currentEntryIndex != entryIndex) {
1203            int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[folderIndex];
1204            if(isInSameFolder) {
1205                if(currentEntryIndex < entryIndex) {
1206                    // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped
1207                    filesToSkipStartIndex = currentEntryIndex + 1;
1208                } else {
1209                    // the entry is in the same folder of current entry, but it has already been read before, we need to reset
1210                    // the position of the currentFolderInputStream to the beginning of folder, and then skip the files
1211                    // from the start entry of the folder again
1212                    deferredBlockStreams.clear();
1213                    channel.position(folderOffset);
1214                }
1215            }
1216
1217            for(int i = filesToSkipStartIndex;i < entryIndex;i++) {
1218                SevenZArchiveEntry fileToSkip = archive.files[i];
1219                InputStream fileStreamToSkip = new BoundedInputStream(currentFolderInputStream, fileToSkip.getSize());
1220                if (fileToSkip.getHasCrc()) {
1221                    fileStreamToSkip = new CRC32VerifyingInputStream(fileStreamToSkip, fileToSkip.getSize(), fileToSkip.getCrcValue());
1222                }
1223                deferredBlockStreams.add(fileStreamToSkip);
1224
1225                // set the content methods as well, it equals to file.getContentMethods() because they are in same folder
1226                fileToSkip.setContentMethods(file.getContentMethods());
1227            }
1228        }
1229
1230        InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize());
1231        if (file.getHasCrc()) {
1232            fileStream = new CRC32VerifyingInputStream(fileStream, file.getSize(), file.getCrcValue());
1233        }
1234
1235        deferredBlockStreams.add(fileStream);
1236    }
1237
1238    private InputStream buildDecoderStack(final Folder folder, final long folderOffset,
1239                final int firstPackStreamIndex, final SevenZArchiveEntry entry) throws IOException {
1240        channel.position(folderOffset);
1241        InputStream inputStreamStack = new FilterInputStream(new BufferedInputStream(
1242              new BoundedSeekableByteChannelInputStream(channel,
1243                  archive.packSizes[firstPackStreamIndex]))) {
1244            @Override
1245            public int read() throws IOException {
1246                final int r = in.read();
1247                if (r >= 0) {
1248                    count(1);
1249                }
1250                return r;
1251            }
1252            @Override
1253            public int read(final byte[] b) throws IOException {
1254                return read(b, 0, b.length);
1255            }
1256            @Override
1257            public int read(final byte[] b, final int off, final int len) throws IOException {
1258                if (len == 0) {
1259                    return 0;
1260                }
1261                final int r = in.read(b, off, len);
1262                if (r >= 0) {
1263                    count(r);
1264                }
1265                return r;
1266            }
1267            private void count(int c) {
1268                compressedBytesReadFromCurrentEntry += c;
1269            }
1270        };
1271        final LinkedList<SevenZMethodConfiguration> methods = new LinkedList<>();
1272        for (final Coder coder : folder.getOrderedCoders()) {
1273            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
1274                throw new IOException("Multi input/output stream coders are not yet supported");
1275            }
1276            final SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId);
1277            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack,
1278                    folder.getUnpackSizeForCoder(coder), coder, password, options.getMaxMemoryLimitInKb());
1279            methods.addFirst(new SevenZMethodConfiguration(method,
1280                     Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack)));
1281        }
1282        entry.setContentMethods(methods);
1283        if (folder.hasCrc) {
1284            return new CRC32VerifyingInputStream(inputStreamStack,
1285                    folder.getUnpackSize(), folder.crc);
1286        }
1287        return inputStreamStack;
1288    }
1289
1290    /**
1291     * Reads a byte of data.
1292     *
1293     * @return the byte read, or -1 if end of input is reached
1294     * @throws IOException
1295     *             if an I/O error has occurred
1296     */
1297    public int read() throws IOException {
1298        int b = getCurrentStream().read();
1299        if (b >= 0) {
1300            uncompressedBytesReadFromCurrentEntry++;
1301        }
1302        return b;
1303    }
1304
1305    private InputStream getCurrentStream() throws IOException {
1306        if (archive.files[currentEntryIndex].getSize() == 0) {
1307            return new ByteArrayInputStream(new byte[0]);
1308        }
1309        if (deferredBlockStreams.isEmpty()) {
1310            throw new IllegalStateException("No current 7z entry (call getNextEntry() first).");
1311        }
1312
1313        while (deferredBlockStreams.size() > 1) {
1314            // In solid compression mode we need to decompress all leading folder'
1315            // streams to get access to an entry. We defer this until really needed
1316            // so that entire blocks can be skipped without wasting time for decompression.
1317            try (final InputStream stream = deferredBlockStreams.remove(0)) {
1318                IOUtils.skip(stream, Long.MAX_VALUE);
1319            }
1320            compressedBytesReadFromCurrentEntry = 0;
1321        }
1322
1323        return deferredBlockStreams.get(0);
1324    }
1325
1326    /**
1327     * Returns an InputStream for reading the contents of the given entry.
1328     *
1329     * <p>For archives using solid compression randomly accessing
1330     * entries will be significantly slower than reading the archive
1331     * sequentiallly.</p>
1332     *
1333     * @param entry the entry to get the stream for.
1334     * @return a stream to read the entry from.
1335     * @throws IOException if unable to create an input stream from the zipentry
1336     * @since Compress 1.20
1337     */
1338    public InputStream getInputStream(SevenZArchiveEntry entry) throws IOException {
1339        int entryIndex = -1;
1340        for (int i = 0; i < this.archive.files.length;i++) {
1341            if (entry == this.archive.files[i]) {
1342                entryIndex = i;
1343                break;
1344            }
1345        }
1346
1347        if (entryIndex < 0) {
1348            throw new IllegalArgumentException("Can not find " + entry.getName() + " in " + this.fileName);
1349        }
1350
1351        buildDecodingStream(entryIndex);
1352        currentEntryIndex = entryIndex;
1353        currentFolderIndex = archive.streamMap.fileFolderIndex[entryIndex];
1354        return getCurrentStream();
1355    }
1356
1357    /**
1358     * Reads data into an array of bytes.
1359     *
1360     * @param b the array to write data to
1361     * @return the number of bytes read, or -1 if end of input is reached
1362     * @throws IOException
1363     *             if an I/O error has occurred
1364     */
1365    public int read(final byte[] b) throws IOException {
1366        return read(b, 0, b.length);
1367    }
1368
1369    /**
1370     * Reads data into an array of bytes.
1371     *
1372     * @param b the array to write data to
1373     * @param off offset into the buffer to start filling at
1374     * @param len of bytes to read
1375     * @return the number of bytes read, or -1 if end of input is reached
1376     * @throws IOException
1377     *             if an I/O error has occurred
1378     */
1379    public int read(final byte[] b, final int off, final int len) throws IOException {
1380        if (len == 0) {
1381            return 0;
1382        }
1383        int cnt = getCurrentStream().read(b, off, len);
1384        if (cnt > 0) {
1385            uncompressedBytesReadFromCurrentEntry += cnt;
1386        }
1387        return cnt;
1388    }
1389
1390    /**
1391     * Provides statistics for bytes read from the current entry.
1392     *
1393     * @return statistics for bytes read from the current entry
1394     * @since 1.17
1395     */
1396    public InputStreamStatistics getStatisticsForCurrentEntry() {
1397        return new InputStreamStatistics() {
1398            @Override
1399            public long getCompressedCount() {
1400                return compressedBytesReadFromCurrentEntry;
1401            }
1402            @Override
1403            public long getUncompressedCount() {
1404                return uncompressedBytesReadFromCurrentEntry;
1405            }
1406        };
1407    }
1408
1409    private static long readUint64(final ByteBuffer in) throws IOException {
1410        // long rather than int as it might get shifted beyond the range of an int
1411        final long firstByte = getUnsignedByte(in);
1412        int mask = 0x80;
1413        long value = 0;
1414        for (int i = 0; i < 8; i++) {
1415            if ((firstByte & mask) == 0) {
1416                return value | ((firstByte & (mask - 1)) << (8 * i));
1417            }
1418            final long nextByte = getUnsignedByte(in);
1419            value |= nextByte << (8 * i);
1420            mask >>>= 1;
1421        }
1422        return value;
1423    }
1424
1425    private static int getUnsignedByte(ByteBuffer buf) {
1426        return buf.get() & 0xff;
1427    }
1428
1429    /**
1430     * Checks if the signature matches what is expected for a 7z file.
1431     *
1432     * @param signature
1433     *            the bytes to check
1434     * @param length
1435     *            the number of bytes to check
1436     * @return true, if this is the signature of a 7z archive.
1437     * @since 1.8
1438     */
1439    public static boolean matches(final byte[] signature, final int length) {
1440        if (length < sevenZSignature.length) {
1441            return false;
1442        }
1443
1444        for (int i = 0; i < sevenZSignature.length; i++) {
1445            if (signature[i] != sevenZSignature[i]) {
1446                return false;
1447            }
1448        }
1449        return true;
1450    }
1451
1452    private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) throws IOException {
1453        if (bytesToSkip < 1) {
1454            return 0;
1455        }
1456        int current = input.position();
1457        int maxSkip = input.remaining();
1458        if (maxSkip < bytesToSkip) {
1459            bytesToSkip = maxSkip;
1460        }
1461        input.position(current + (int) bytesToSkip);
1462        return bytesToSkip;
1463    }
1464
1465    private void readFully(ByteBuffer buf) throws IOException {
1466        buf.rewind();
1467        IOUtils.readFully(channel, buf);
1468        buf.flip();
1469    }
1470
1471    @Override
1472    public String toString() {
1473      return archive.toString();
1474    }
1475
1476    /**
1477     * Derives a default file name from the archive name - if known.
1478     *
1479     * <p>This implements the same heuristics the 7z tools use. In
1480     * 7z's case if an archive contains entries without a name -
1481     * i.e. {@link SevenZArchiveEntry#getName} returns {@code null} -
1482     * then its command line and GUI tools will use this default name
1483     * when extracting the entries.</p>
1484     *
1485     * @return null if the name of the archive is unknown. Otherwise
1486     * if the name of the archive has got any extension, it is
1487     * stripped and the remainder returned. Finally if the name of the
1488     * archive hasn't got any extension then a {@code ~} character is
1489     * appended to the archive name.
1490     *
1491     * @since 1.19
1492     */
1493    public String getDefaultName() {
1494        if (DEFAULT_FILE_NAME.equals(fileName) || fileName == null) {
1495            return null;
1496        }
1497
1498        final String lastSegment = new File(fileName).getName();
1499        final int dotPos = lastSegment.lastIndexOf(".");
1500        if (dotPos > 0) { // if the file starts with a dot then this is not an extension
1501            return lastSegment.substring(0, dotPos);
1502        }
1503        return lastSegment + "~";
1504    }
1505
1506    private static final CharsetEncoder PASSWORD_ENCODER = StandardCharsets.UTF_16LE.newEncoder();
1507
1508    private static byte[] utf16Decode(char[] chars) throws IOException {
1509        if (chars == null) {
1510            return null;
1511        }
1512        ByteBuffer encoded = PASSWORD_ENCODER.encode(CharBuffer.wrap(chars));
1513        if (encoded.hasArray()) {
1514            return encoded.array();
1515        }
1516        byte[] e = new byte[encoded.remaining()];
1517        encoded.get(e);
1518        return e;
1519    }
1520
1521    private static void assertFitsIntoInt(String what, long value) throws IOException {
1522        if (value > Integer.MAX_VALUE) {
1523            throw new IOException("Cannot handle " + what + value);
1524        }
1525    }
1526}