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