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