    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.DataInput;
    024import java.io.DataInputStream;
    025import java.io.File;
    026import java.io.IOException;
    027import java.io.InputStream;
    028import java.io.RandomAccessFile;
    029import java.util.ArrayList;
    030import java.util.Arrays;
    031import java.util.BitSet;
    032import java.util.LinkedList;
    033import java.util.zip.CRC32;
    034
    035import org.apache.commons.compress.utils.BoundedInputStream;
    036import org.apache.commons.compress.utils.CRC32VerifyingInputStream;
    037import org.apache.commons.compress.utils.CharsetNames;
    038import org.apache.commons.compress.utils.IOUtils;
    039
    040/**
    041 * Reads a 7z file, using RandomAccessFile under
    042 * the covers.
    043 * <p>
    044 * The 7z file format is a flexible container
    045 * that can contain many compression and
    046 * encryption types, but at the moment only
    047 * only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256
    048 * are supported.
    049 * <p>
    050 * The format is very Windows/Intel specific,
    051 * so it uses little-endian byte order,
    052 * doesn't store user/group or permission bits,
    053 * and represents times using NTFS timestamps
    054 * (100 nanosecond units since 1 January 1601).
    055 * Hence the official tools recommend against
    056 * using it for backup purposes on *nix, and
    057 * recommend .tar.7z or .tar.lzma or .tar.xz
    058 * instead.
    059 * <p>
    060 * Both the header and file contents may be
    061 * compressed and/or encrypted. With both
    062 * encrypted, neither file names nor file
    063 * contents can be read, but the use of
    064 * encryption isn't plausibly deniable.
    065 *
    066 * @NotThreadSafe
    067 * @since 1.6
    068 */
    069public class SevenZFile implements Closeable {
    070    static final int SIGNATURE_HEADER_SIZE = 32;
    071
    072    private final String fileName;
    073    private RandomAccessFile file;
    074    private final Archive archive;
    075    private int currentEntryIndex = -1;
    076    private int currentFolderIndex = -1;
    077    private InputStream currentFolderInputStream = null;
    078    private byte[] password;
    079
    080    private final ArrayList<InputStream> deferredBlockStreams = new ArrayList<InputStream>();
    081
    082    static final byte[] sevenZSignature = {
    083        (byte)'7', (byte)'z', (byte)0xBC, (byte)0xAF, (byte)0x27, (byte)0x1C
    084    };
    085
    086    /**
    087     * Reads a file as 7z archive
    088     *
    089     * @param filename the file to read
    090     * @param password optional password if the archive is encrypted -
    091     * the byte array is supposed to be the UTF16-LE encoded
    092     * representation of the password.
    093     * @throws IOException if reading the archive fails
    094     */
    095    public SevenZFile(final File filename, final byte[] password) throws IOException {
    096        boolean succeeded = false;
    097        this.file = new RandomAccessFile(filename, "r");
    098        this.fileName = filename.getAbsolutePath();
    099        try {
    100            archive = readHeaders(password);
    101            if (password != null) {
    102                this.password = new byte[password.length];
    103                System.arraycopy(password, 0, this.password, 0, password.length);
    104            } else {
    105                this.password = null;
    106            }
    107            succeeded = true;
    108        } finally {
    109            if (!succeeded) {
    110                this.file.close();
    111            }
    112        }
    113    }
    114
    115    /**
    116     * Reads a file as unencrypted 7z archive
    117     *
    118     * @param filename the file to read
    119     * @throws IOException if reading the archive fails
    120     */
    121    public SevenZFile(final File filename) throws IOException {
    122        this(filename, null);
    123    }
    124
    125    /**
    126     * Closes the archive.
    127     * @throws IOException if closing the file fails
    128     */
    129    @Override
    130    public void close() throws IOException {
    131        if (file != null) {
    132            try {
    133                file.close();
    134            } finally {
    135                file = null;
    136                if (password != null) {
    137                    Arrays.fill(password, (byte) 0);
    138                }
    139                password = null;
    140            }
    141        }
    142    }
    143
    144    /**
    145     * Returns the next Archive Entry in this archive.
    146     *
    147     * @return the next entry,
    148     *         or {@code null} if there are no more entries
    149     * @throws IOException if the next entry could not be read
    150     */
    151    public SevenZArchiveEntry getNextEntry() throws IOException {
    152        if (currentEntryIndex >= archive.files.length - 1) {
    153            return null;
    154        }
    155        ++currentEntryIndex;
    156        final SevenZArchiveEntry entry = archive.files[currentEntryIndex];
    157        buildDecodingStream();
    158        return entry;
    159    }
    160
    161    /**
    162     * Returns meta-data of all archive entries.
    163     *
    164     * <p>This method only provides meta-data, the entries can not be
    165     * used to read the contents, you still need to process all
    166     * entries in order using {@link #getNextEntry} for that.</p>
    167     *
    168     * <p>The content methods are only available for entries that have
    169     * already been reached via {@link #getNextEntry}.</p>
    170     *
    171     * @return meta-data of all archive entries.
    172     * @since 1.11
    173     */
    174    public Iterable<SevenZArchiveEntry> getEntries() {
    175        return Arrays.asList(archive.files);
    176    }
    177
    178    private Archive readHeaders(final byte[] password) throws IOException {
    179        final byte[] signature = new byte[6];
    180        file.readFully(signature);
    181        if (!Arrays.equals(signature, sevenZSignature)) {
    182            throw new IOException("Bad 7z signature");
    183        }
    184        // 7zFormat.txt has it wrong - it's first major then minor
    185        final byte archiveVersionMajor = file.readByte();
    186        final byte archiveVersionMinor = file.readByte();
    187        if (archiveVersionMajor != 0) {
    188            throw new IOException(String.format("Unsupported 7z version (%d,%d)",
    189                    archiveVersionMajor, archiveVersionMinor));
    190        }
    191
    192        final long startHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(file.readInt());
    193        final StartHeader startHeader = readStartHeader(startHeaderCrc);
    194
    195        final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize;
    196        if (nextHeaderSizeInt != startHeader.nextHeaderSize) {
    197            throw new IOException("cannot handle nextHeaderSize " + startHeader.nextHeaderSize);
    198        }
    199        file.seek(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset);
    200        final byte[] nextHeader = new byte[nextHeaderSizeInt];
    201        file.readFully(nextHeader);
    202        final CRC32 crc = new CRC32();
    203        crc.update(nextHeader);
    204        if (startHeader.nextHeaderCrc != crc.getValue()) {
    205            throw new IOException("NextHeader CRC mismatch");
    206        }
    207
    208        final ByteArrayInputStream byteStream = new ByteArrayInputStream(nextHeader);
    209        DataInputStream nextHeaderInputStream = new DataInputStream(
    210                byteStream);
    211        Archive archive = new Archive();
    212        int nid = nextHeaderInputStream.readUnsignedByte();
    213        if (nid == NID.kEncodedHeader) {
    214            nextHeaderInputStream =
    215                readEncodedHeader(nextHeaderInputStream, archive, password);
    216            // Archive gets rebuilt with the new header
    217            archive = new Archive();
    218            nid = nextHeaderInputStream.readUnsignedByte();
    219        }
    220        if (nid == NID.kHeader) {
    221            readHeader(nextHeaderInputStream, archive);
    222            nextHeaderInputStream.close();
    223        } else {
    224            throw new IOException("Broken or unsupported archive: no Header");
    225        }
    226        return archive;
    227    }
    228
    229    private StartHeader readStartHeader(final long startHeaderCrc) throws IOException {
    230        final StartHeader startHeader = new StartHeader();
    231        DataInputStream dataInputStream = null;
    232        try {
    233             dataInputStream = new DataInputStream(new CRC32VerifyingInputStream(
    234                    new BoundedRandomAccessFileInputStream(file, 20), 20, startHeaderCrc));
    235             startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong());
    236             startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong());
    237             startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt());
    238             return startHeader;
    239        } finally {
    240            if (dataInputStream != null) {
    241                dataInputStream.close();
    242            }
    243        }
    244    }
    245
    246    private void readHeader(final DataInput header, final Archive archive) throws IOException {
    247        int nid = header.readUnsignedByte();
    248
    249        if (nid == NID.kArchiveProperties) {
    250            readArchiveProperties(header);
    251            nid = header.readUnsignedByte();
    252        }
    253
    254        if (nid == NID.kAdditionalStreamsInfo) {
    255            throw new IOException("Additional streams unsupported");
    256            //nid = header.readUnsignedByte();
    257        }
    258
    259        if (nid == NID.kMainStreamsInfo) {
    260            readStreamsInfo(header, archive);
    261            nid = header.readUnsignedByte();
    262        }
    263
    264        if (nid == NID.kFilesInfo) {
    265            readFilesInfo(header, archive);
    266            nid = header.readUnsignedByte();
    267        }
    268
    269        if (nid != NID.kEnd) {
    270            throw new IOException("Badly terminated header, found " + nid);
    271        }
    272    }
    273
    274    private void readArchiveProperties(final DataInput input) throws IOException {
    275        // FIXME: the reference implementation just throws them away?
    276        int nid =  input.readUnsignedByte();
    277        while (nid != NID.kEnd) {
    278            final long propertySize = readUint64(input);
    279            final byte[] property = new byte[(int)propertySize];
    280            input.readFully(property);
    281            nid = input.readUnsignedByte();
    282        }
    283    }
    284
    285    private DataInputStream readEncodedHeader(final DataInputStream header, final Archive archive,
    286                                              final byte[] password) throws IOException {
    287        readStreamsInfo(header, archive);
    288
    289        // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage?
    290        final Folder folder = archive.folders[0];
    291        final int firstPackStreamIndex = 0;
    292        final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
    293                0;
    294
    295        file.seek(folderOffset);
    296        InputStream inputStreamStack = new BoundedRandomAccessFileInputStream(file,
    297                archive.packSizes[firstPackStreamIndex]);
    298        for (final Coder coder : folder.getOrderedCoders()) {
    299            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
    300                throw new IOException("Multi input/output stream coders are not yet supported");
    301            }
    302            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack,
    303                    folder.getUnpackSizeForCoder(coder), coder, password);
    304        }
    305        if (folder.hasCrc) {
    306            inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack,
    307                    folder.getUnpackSize(), folder.crc);
    308        }
    309        final byte[] nextHeader = new byte[(int)folder.getUnpackSize()];
    310        final DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack);
    311        try {
    312            nextHeaderInputStream.readFully(nextHeader);
    313        } finally {
    314            nextHeaderInputStream.close();
    315        }
    316        return new DataInputStream(new ByteArrayInputStream(nextHeader));
    317    }
    318
    319    private void readStreamsInfo(final DataInput header, final Archive archive) throws IOException {
    320        int nid = header.readUnsignedByte();
    321
    322        if (nid == NID.kPackInfo) {
    323            readPackInfo(header, archive);
    324            nid = header.readUnsignedByte();
    325        }
    326
    327        if (nid == NID.kUnpackInfo) {
    328            readUnpackInfo(header, archive);
    329            nid = header.readUnsignedByte();
    330        } else {
    331            // archive without unpack/coders info
    332            archive.folders = new Folder[0];
    333        }
    334
    335        if (nid == NID.kSubStreamsInfo) {
    336            readSubStreamsInfo(header, archive);
    337            nid = header.readUnsignedByte();
    338        }
    339
    340        if (nid != NID.kEnd) {
    341            throw new IOException("Badly terminated StreamsInfo");
    342        }
    343    }
    344
    345    private void readPackInfo(final DataInput header, final Archive archive) throws IOException {
    346        archive.packPos = readUint64(header);
    347        final long numPackStreams = readUint64(header);
    348        int nid = header.readUnsignedByte();
    349        if (nid == NID.kSize) {
    350            archive.packSizes = new long[(int)numPackStreams];
    351            for (int i = 0; i < archive.packSizes.length; i++) {
    352                archive.packSizes[i] = readUint64(header);
    353            }
    354            nid = header.readUnsignedByte();
    355        }
    356
    357        if (nid == NID.kCRC) {
    358            archive.packCrcsDefined = readAllOrBits(header, (int)numPackStreams);
    359            archive.packCrcs = new long[(int)numPackStreams];
    360            for (int i = 0; i < (int)numPackStreams; i++) {
    361                if (archive.packCrcsDefined.get(i)) {
    362                    archive.packCrcs[i] = 0xffffFFFFL & Integer.reverseBytes(header.readInt());
    363                }
    364            }
    365
    366            nid = header.readUnsignedByte();
    367        }
    368
    369        if (nid != NID.kEnd) {
    370            throw new IOException("Badly terminated PackInfo (" + nid + ")");
    371        }
    372    }
    373
    374    private void readUnpackInfo(final DataInput header, final Archive archive) throws IOException {
    375        int nid = header.readUnsignedByte();
    376        if (nid != NID.kFolder) {
    377            throw new IOException("Expected kFolder, got " + nid);
    378        }
    379        final long numFolders = readUint64(header);
    380        final Folder[] folders = new Folder[(int)numFolders];
    381        archive.folders = folders;
    382        final int external = header.readUnsignedByte();
    383        if (external != 0) {
    384            throw new IOException("External unsupported");
    385        }
    386        for (int i = 0; i < (int)numFolders; i++) {
    387            folders[i] = readFolder(header);
    388        }
    389
    390        nid = header.readUnsignedByte();
    391        if (nid != NID.kCodersUnpackSize) {
    392            throw new IOException("Expected kCodersUnpackSize, got " + nid);
    393        }
    394        for (final Folder folder : folders) {
    395            folder.unpackSizes = new long[(int)folder.totalOutputStreams];
    396            for (int i = 0; i < folder.totalOutputStreams; i++) {
    397                folder.unpackSizes[i] = readUint64(header);
    398            }
    399        }
    400
    401        nid = header.readUnsignedByte();
    402        if (nid == NID.kCRC) {
    403            final BitSet crcsDefined = readAllOrBits(header, (int)numFolders);
    404            for (int i = 0; i < (int)numFolders; i++) {
    405                if (crcsDefined.get(i)) {
    406                    folders[i].hasCrc = true;
    407                    folders[i].crc = 0xffffFFFFL & Integer.reverseBytes(header.readInt());
    408                } else {
    409                    folders[i].hasCrc = false;
    410                }
    411            }
    412
    413            nid = header.readUnsignedByte();
    414        }
    415
    416        if (nid != NID.kEnd) {
    417            throw new IOException("Badly terminated UnpackInfo");
    418        }
    419    }
    420
    421    private void readSubStreamsInfo(final DataInput header, final Archive archive) throws IOException {
    422        for (final Folder folder : archive.folders) {
    423            folder.numUnpackSubStreams = 1;
    424        }
    425        int totalUnpackStreams = archive.folders.length;
    426
    427        int nid = header.readUnsignedByte();
    428        if (nid == NID.kNumUnpackStream) {
    429            totalUnpackStreams = 0;
    430            for (final Folder folder : archive.folders) {
    431                final long numStreams = readUint64(header);
    432                folder.numUnpackSubStreams = (int)numStreams;
    433                totalUnpackStreams += numStreams;
    434            }
    435            nid = header.readUnsignedByte();
    436        }
    437
    438        final SubStreamsInfo subStreamsInfo = new SubStreamsInfo();
    439        subStreamsInfo.unpackSizes = new long[totalUnpackStreams];
    440        subStreamsInfo.hasCrc = new BitSet(totalUnpackStreams);
    441        subStreamsInfo.crcs = new long[totalUnpackStreams];
    442
    443        int nextUnpackStream = 0;
    444        for (final Folder folder : archive.folders) {
    445            if (folder.numUnpackSubStreams == 0) {
    446                continue;
    447            }
    448            long sum = 0;
    449            if (nid == NID.kSize) {
    450                for (int i = 0; i < folder.numUnpackSubStreams - 1; i++) {
    451                    final long size = readUint64(header);
    452                    subStreamsInfo.unpackSizes[nextUnpackStream++] = size;
    453                    sum += size;
    454                }
    455            }
    456            subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum;
    457        }
    458        if (nid == NID.kSize) {
    459            nid = header.readUnsignedByte();
    460        }
    461
    462        int numDigests = 0;
    463        for (final Folder folder : archive.folders) {
    464            if (folder.numUnpackSubStreams != 1 || !folder.hasCrc) {
    465                numDigests += folder.numUnpackSubStreams;
    466            }
    467        }
    468
    469        if (nid == NID.kCRC) {
    470            final BitSet hasMissingCrc = readAllOrBits(header, numDigests);
    471            final long[] missingCrcs = new long[numDigests];
    472            for (int i = 0; i < numDigests; i++) {
    473                if (hasMissingCrc.get(i)) {
    474                    missingCrcs[i] = 0xffffFFFFL & Integer.reverseBytes(header.readInt());
    475                }
    476            }
    477            int nextCrc = 0;
    478            int nextMissingCrc = 0;
    479            for (final Folder folder: archive.folders) {
    480                if (folder.numUnpackSubStreams == 1 && folder.hasCrc) {
    481                    subStreamsInfo.hasCrc.set(nextCrc, true);
    482                    subStreamsInfo.crcs[nextCrc] = folder.crc;
    483                    ++nextCrc;
    484                } else {
    485                    for (int i = 0; i < folder.numUnpackSubStreams; i++) {
    486                        subStreamsInfo.hasCrc.set(nextCrc, hasMissingCrc.get(nextMissingCrc));
    487                        subStreamsInfo.crcs[nextCrc] = missingCrcs[nextMissingCrc];
    488                        ++nextCrc;
    489                        ++nextMissingCrc;
    490                    }
    491                }
    492            }
    493
    494            nid = header.readUnsignedByte();
    495        }
    496
    497        if (nid != NID.kEnd) {
    498            throw new IOException("Badly terminated SubStreamsInfo");
    499        }
    500
    501        archive.subStreamsInfo = subStreamsInfo;
    502    }
    503
    504    private Folder readFolder(final DataInput header) throws IOException {
    505        final Folder folder = new Folder();
    506
    507        final long numCoders = readUint64(header);
    508        final Coder[] coders = new Coder[(int)numCoders];
    509        long totalInStreams = 0;
    510        long totalOutStreams = 0;
    511        for (int i = 0; i < coders.length; i++) {
    512            coders[i] = new Coder();
    513            final int bits = header.readUnsignedByte();
    514            final int idSize = bits & 0xf;
    515            final boolean isSimple = (bits & 0x10) == 0;
    516            final boolean hasAttributes = (bits & 0x20) != 0;
    517            final boolean moreAlternativeMethods = (bits & 0x80) != 0;
    518
    519            coders[i].decompressionMethodId = new byte[idSize];
    520            header.readFully(coders[i].decompressionMethodId);
    521            if (isSimple) {
    522                coders[i].numInStreams = 1;
    523                coders[i].numOutStreams = 1;
    524            } else {
    525                coders[i].numInStreams = readUint64(header);
    526                coders[i].numOutStreams = readUint64(header);
    527            }
    528            totalInStreams += coders[i].numInStreams;
    529            totalOutStreams += coders[i].numOutStreams;
    530            if (hasAttributes) {
    531                final long propertiesSize = readUint64(header);
    532                coders[i].properties = new byte[(int)propertiesSize];
    533                header.readFully(coders[i].properties);
    534            }
    535            // would need to keep looping as above:
    536            while (moreAlternativeMethods) {
    537                throw new IOException("Alternative methods are unsupported, please report. " +
    538                        "The reference implementation doesn't support them either.");
    539            }
    540        }
    541        folder.coders = coders;
    542        folder.totalInputStreams = totalInStreams;
    543        folder.totalOutputStreams = totalOutStreams;
    544
    545        if (totalOutStreams == 0) {
    546            throw new IOException("Total output streams can't be 0");
    547        }
    548        final long numBindPairs = totalOutStreams - 1;
    549        final BindPair[] bindPairs = new BindPair[(int)numBindPairs];
    550        for (int i = 0; i < bindPairs.length; i++) {
    551            bindPairs[i] = new BindPair();
    552            bindPairs[i].inIndex = readUint64(header);
    553            bindPairs[i].outIndex = readUint64(header);
    554        }
    555        folder.bindPairs = bindPairs;
    556
    557        if (totalInStreams < numBindPairs) {
    558            throw new IOException("Total input streams can't be less than the number of bind pairs");
    559        }
    560        final long numPackedStreams = totalInStreams - numBindPairs;
    561        final long packedStreams[] = new long[(int)numPackedStreams];
    562        if (numPackedStreams == 1) {
    563            int i;
    564            for (i = 0; i < (int)totalInStreams; i++) {
    565                if (folder.findBindPairForInStream(i) < 0) {
    566                    break;
    567                }
    568            }
    569            if (i == (int)totalInStreams) {
    570                throw new IOException("Couldn't find stream's bind pair index");
    571            }
    572            packedStreams[0] = i;
    573        } else {
    574            for (int i = 0; i < (int)numPackedStreams; i++) {
    575                packedStreams[i] = readUint64(header);
    576            }
    577        }
    578        folder.packedStreams = packedStreams;
    579
    580        return folder;
    581    }
    582
    583    private BitSet readAllOrBits(final DataInput header, final int size) throws IOException {
    584        final int areAllDefined = header.readUnsignedByte();
    585        final BitSet bits;
    586        if (areAllDefined != 0) {
    587            bits = new BitSet(size);
    588            for (int i = 0; i < size; i++) {
    589                bits.set(i, true);
    590            }
    591        } else {
    592            bits = readBits(header, size);
    593        }
    594        return bits;
    595    }
    596
    597    private BitSet readBits(final DataInput header, final int size) throws IOException {
    598        final BitSet bits = new BitSet(size);
    599        int mask = 0;
    600        int cache = 0;
    601        for (int i = 0; i < size; i++) {
    602            if (mask == 0) {
    603                mask = 0x80;
    604                cache = header.readUnsignedByte();
    605            }
    606            bits.set(i, (cache & mask) != 0);
    607            mask >>>= 1;
    608        }
    609        return bits;
    610    }
    611
    612    private void readFilesInfo(final DataInput header, final Archive archive) throws IOException {
    613        final long numFiles = readUint64(header);
    614        final SevenZArchiveEntry[] files = new SevenZArchiveEntry[(int)numFiles];
    615        for (int i = 0; i < files.length; i++) {
    616            files[i] = new SevenZArchiveEntry();
    617        }
    618        BitSet isEmptyStream = null;
    619        BitSet isEmptyFile = null;
    620        BitSet isAnti = null;
    621        while (true) {
    622            final int propertyType = header.readUnsignedByte();
    623            if (propertyType == 0) {
    624                break;
    625            }
    626            final long size = readUint64(header);
    627            switch (propertyType) {
    628                case NID.kEmptyStream: {
    629                    isEmptyStream = readBits(header, files.length);
    630                    break;
    631                }
    632                case NID.kEmptyFile: {
    633                    if (isEmptyStream == null) { // protect against NPE
    634                        throw new IOException("Header format error: kEmptyStream must appear before kEmptyFile");
    635                    }
    636                    isEmptyFile = readBits(header, isEmptyStream.cardinality());
    637                    break;
    638                }
    639                case NID.kAnti: {
    640                    if (isEmptyStream == null) { // protect against NPE
    641                        throw new IOException("Header format error: kEmptyStream must appear before kAnti");
    642                    }
    643                    isAnti = readBits(header, isEmptyStream.cardinality());
    644                    break;
    645                }
    646                case NID.kName: {
    647                    final int external = header.readUnsignedByte();
    648                    if (external != 0) {
    649                        throw new IOException("Not implemented");
    650                    }
    651                    if (((size - 1) & 1) != 0) {
    652                        throw new IOException("File names length invalid");
    653                    }
    654                    final byte[] names = new byte[(int)(size - 1)];
    655                    header.readFully(names);
    656                    int nextFile = 0;
    657                    int nextName = 0;
    658                    for (int i = 0; i < names.length; i += 2) {
    659                        if (names[i] == 0 && names[i+1] == 0) {
    660                            files[nextFile++].setName(new String(names, nextName, i-nextName, CharsetNames.UTF_16LE));
    661                            nextName = i + 2;
    662                        }
    663                    }
    664                    if (nextName != names.length || nextFile != files.length) {
    665                        throw new IOException("Error parsing file names");
    666                    }
    667                    break;
    668                }
    669                case NID.kCTime: {
    670                    final BitSet timesDefined = readAllOrBits(header, files.length);
    671                    final int external = header.readUnsignedByte();
    672                    if (external != 0) {
    673                        throw new IOException("Unimplemented");
    674                    }
    675                    for (int i = 0; i < files.length; i++) {
    676                        files[i].setHasCreationDate(timesDefined.get(i));
    677                        if (files[i].getHasCreationDate()) {
    678                            files[i].setCreationDate(Long.reverseBytes(header.readLong()));
    679                        }
    680                    }
    681                    break;
    682                }
    683                case NID.kATime: {
    684                    final BitSet timesDefined = readAllOrBits(header, files.length);
    685                    final int external = header.readUnsignedByte();
    686                    if (external != 0) {
    687                        throw new IOException("Unimplemented");
    688                    }
    689                    for (int i = 0; i < files.length; i++) {
    690                        files[i].setHasAccessDate(timesDefined.get(i));
    691                        if (files[i].getHasAccessDate()) {
    692                            files[i].setAccessDate(Long.reverseBytes(header.readLong()));
    693                        }
    694                    }
    695                    break;
    696                }
    697                case NID.kMTime: {
    698                    final BitSet timesDefined = readAllOrBits(header, files.length);
    699                    final int external = header.readUnsignedByte();
    700                    if (external != 0) {
    701                        throw new IOException("Unimplemented");
    702                    }
    703                    for (int i = 0; i < files.length; i++) {
    704                        files[i].setHasLastModifiedDate(timesDefined.get(i));
    705                        if (files[i].getHasLastModifiedDate()) {
    706                            files[i].setLastModifiedDate(Long.reverseBytes(header.readLong()));
    707                        }
    708                    }
    709                    break;
    710                }
    711                case NID.kWinAttributes: {
    712                    final BitSet attributesDefined = readAllOrBits(header, files.length);
    713                    final int external = header.readUnsignedByte();
    714                    if (external != 0) {
    715                        throw new IOException("Unimplemented");
    716                    }
    717                    for (int i = 0; i < files.length; i++) {
    718                        files[i].setHasWindowsAttributes(attributesDefined.get(i));
    719                        if (files[i].getHasWindowsAttributes()) {
    720                            files[i].setWindowsAttributes(Integer.reverseBytes(header.readInt()));
    721                        }
    722                    }
    723                    break;
    724                }
    725                case NID.kStartPos: {
    726                    throw new IOException("kStartPos is unsupported, please report");
    727                }
    728                case NID.kDummy: {
    729                    // 7z 9.20 asserts the content is all zeros and ignores the property
    730                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
    731
    732                    if (skipBytesFully(header, size) < size) {
    733                        throw new IOException("Incomplete kDummy property");
    734                    }
    735                    break;
    736                }
    737
    738                default: {
    739                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
    740                    if (skipBytesFully(header, size) < size) {
    741                        throw new IOException("Incomplete property of type " + propertyType);
    742                    }
    743                    break;
    744                }
    745            }
    746        }
    747        int nonEmptyFileCounter = 0;
    748        int emptyFileCounter = 0;
    749        for (int i = 0; i < files.length; i++) {
    750            files[i].setHasStream(isEmptyStream == null ? true : !isEmptyStream.get(i));
    751            if (files[i].hasStream()) {
    752                files[i].setDirectory(false);
    753                files[i].setAntiItem(false);
    754                files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter));
    755                files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]);
    756                files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]);
    757                ++nonEmptyFileCounter;
    758            } else {
    759                files[i].setDirectory(isEmptyFile == null ? true : !isEmptyFile.get(emptyFileCounter));
    760                files[i].setAntiItem(isAnti == null ? false : isAnti.get(emptyFileCounter));
    761                files[i].setHasCrc(false);
    762                files[i].setSize(0);
    763                ++emptyFileCounter;
    764            }
    765        }
    766        archive.files = files;
    767        calculateStreamMap(archive);
    768    }
    769
    770    private void calculateStreamMap(final Archive archive) throws IOException {
    771        final StreamMap streamMap = new StreamMap();
    772
    773        int nextFolderPackStreamIndex = 0;
    774        final int numFolders = archive.folders != null ? archive.folders.length : 0;
    775        streamMap.folderFirstPackStreamIndex = new int[numFolders];
    776        for (int i = 0; i < numFolders; i++) {
    777            streamMap.folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex;
    778            nextFolderPackStreamIndex += archive.folders[i].packedStreams.length;
    779        }
    780
    781        long nextPackStreamOffset = 0;
    782        final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0;
    783        streamMap.packStreamOffsets = new long[numPackSizes];
    784        for (int i = 0; i < numPackSizes; i++) {
    785            streamMap.packStreamOffsets[i] = nextPackStreamOffset;
    786            nextPackStreamOffset += archive.packSizes[i];
    787        }
    788
    789        streamMap.folderFirstFileIndex = new int[numFolders];
    790        streamMap.fileFolderIndex = new int[archive.files.length];
    791        int nextFolderIndex = 0;
    792        int nextFolderUnpackStreamIndex = 0;
    793        for (int i = 0; i < archive.files.length; i++) {
    794            if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) {
    795                streamMap.fileFolderIndex[i] = -1;
    796                continue;
    797            }
    798            if (nextFolderUnpackStreamIndex == 0) {
    799                for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) {
    800                    streamMap.folderFirstFileIndex[nextFolderIndex] = i;
    801                    if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) {
    802                        break;
    803                    }
    804                }
    805                if (nextFolderIndex >= archive.folders.length) {
    806                    throw new IOException("Too few folders in archive");
    807                }
    808            }
    809            streamMap.fileFolderIndex[i] = nextFolderIndex;
    810            if (!archive.files[i].hasStream()) {
    811                continue;
    812            }
    813            ++nextFolderUnpackStreamIndex;
    814            if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) {
    815                ++nextFolderIndex;
    816                nextFolderUnpackStreamIndex = 0;
    817            }
    818        }
    819
    820        archive.streamMap = streamMap;
    821    }
    822
    823    private void buildDecodingStream() throws IOException {
    824        final int folderIndex = archive.streamMap.fileFolderIndex[currentEntryIndex];
    825        if (folderIndex < 0) {
    826            deferredBlockStreams.clear();
    827            // TODO: previously it'd return an empty stream?
    828            // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0);
    829            return;
    830        }
    831        final SevenZArchiveEntry file = archive.files[currentEntryIndex];
    832        if (currentFolderIndex == folderIndex) {
    833            // (COMPRESS-320).
    834            // The current entry is within the same (potentially opened) folder. The
    835            // previous stream has to be fully decoded before we can start reading
    836            // but don't do it eagerly -- if the user skips over the entire folder nothing
    837            // is effectively decompressed.
    838
    839            file.setContentMethods(archive.files[currentEntryIndex - 1].getContentMethods());
    840        } else {
    841            // We're opening a new folder. Discard any queued streams/ folder stream.
    842            currentFolderIndex = folderIndex;
    843            deferredBlockStreams.clear();
    844            if (currentFolderInputStream != null) {
    845                currentFolderInputStream.close();
    846                currentFolderInputStream = null;
    847            }
    848
    849            final Folder folder = archive.folders[folderIndex];
    850            final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex];
    851            final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
    852                    archive.streamMap.packStreamOffsets[firstPackStreamIndex];
    853            currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file);
    854        }
    855
    856        InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize());
    857        if (file.getHasCrc()) {
    858            fileStream = new CRC32VerifyingInputStream(fileStream, file.getSize(), file.getCrcValue());
    859        }
    860
    861        deferredBlockStreams.add(fileStream);
    862    }
    863
    864    private InputStream buildDecoderStack(final Folder folder, final long folderOffset,
    865                final int firstPackStreamIndex, final SevenZArchiveEntry entry) throws IOException {
    866        file.seek(folderOffset);
    867        InputStream inputStreamStack =
    868            new BufferedInputStream(
    869              new BoundedRandomAccessFileInputStream(file,
    870                  archive.packSizes[firstPackStreamIndex]));
    871        final LinkedList<SevenZMethodConfiguration> methods = new LinkedList<SevenZMethodConfiguration>();
    872        for (final Coder coder : folder.getOrderedCoders()) {
    873            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
    874                throw new IOException("Multi input/output stream coders are not yet supported");
    875            }
    876            final SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId);
    877            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack,
    878                    folder.getUnpackSizeForCoder(coder), coder, password);
    879            methods.addFirst(new SevenZMethodConfiguration(method,
    880                     Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack)));
    881        }
    882        entry.setContentMethods(methods);
    883        if (folder.hasCrc) {
    884            return new CRC32VerifyingInputStream(inputStreamStack,
    885                    folder.getUnpackSize(), folder.crc);
    886        }
    887        return inputStreamStack;
    888    }
    889
    890    /**
    891     * Reads a byte of data.
    892     *
    893     * @return the byte read, or -1 if end of input is reached
    894     * @throws IOException
    895     *             if an I/O error has occurred
    896     */
    897    public int read() throws IOException {
    898        return getCurrentStream().read();
    899    }
    900
    901    private InputStream getCurrentStream() throws IOException {
    902        if (archive.files[currentEntryIndex].getSize() == 0) {
    903            return new ByteArrayInputStream(new byte[0]);
    904        }
    905        if (deferredBlockStreams.isEmpty()) {
    906            throw new IllegalStateException("No current 7z entry (call getNextEntry() first).");
    907        }
    908
    909        while (deferredBlockStreams.size() > 1) {
    910            // In solid compression mode we need to decompress all leading folder'
    911            // streams to get access to an entry. We defer this until really needed
    912            // so that entire blocks can be skipped without wasting time for decompression.
    913            final InputStream stream = deferredBlockStreams.remove(0);
    914            IOUtils.skip(stream, Long.MAX_VALUE);
    915            stream.close();
    916        }
    917
    918        return deferredBlockStreams.get(0);
    919    }
    920
    921    /**
    922     * Reads data into an array of bytes.
    923     *
    924     * @param b the array to write data to
    925     * @return the number of bytes read, or -1 if end of input is reached
    926     * @throws IOException
    927     *             if an I/O error has occurred
    928     */
    929    public int read(final byte[] b) throws IOException {
    930        return read(b, 0, b.length);
    931    }
    932
    933    /**
    934     * Reads data into an array of bytes.
    935     *
    936     * @param b the array to write data to
    937     * @param off offset into the buffer to start filling at
    938     * @param len of bytes to read
    939     * @return the number of bytes read, or -1 if end of input is reached
    940     * @throws IOException
    941     *             if an I/O error has occurred
    942     */
    943    public int read(final byte[] b, final int off, final int len) throws IOException {
    944        return getCurrentStream().read(b, off, len);
    945    }
    946
    947    private static long readUint64(final DataInput in) throws IOException {
    948        // long rather than int as it might get shifted beyond the range of an int
    949        final long firstByte = in.readUnsignedByte();
    950        int mask = 0x80;
    951        long value = 0;
    952        for (int i = 0; i < 8; i++) {
    953            if ((firstByte & mask) == 0) {
    954                return value | ((firstByte & (mask - 1)) << (8 * i));
    955            }
    956            final long nextByte = in.readUnsignedByte();
    957            value |= nextByte << (8 * i);
    958            mask >>>= 1;
    959        }
    960        return value;
    961    }
    962
    963    /**
    964     * Checks if the signature matches what is expected for a 7z file.
    965     *
    966     * @param signature
    967     *            the bytes to check
    968     * @param length
    969     *            the number of bytes to check
    970     * @return true, if this is the signature of a 7z archive.
    971     * @since 1.8
    972     */
    973    public static boolean matches(final byte[] signature, final int length) {
    974        if (length < sevenZSignature.length) {
    975            return false;
    976        }
    977
    978        for (int i = 0; i < sevenZSignature.length; i++) {
    979            if (signature[i] != sevenZSignature[i]) {
    980                return false;
    981            }
    982        }
    983        return true;
    984    }
    985
    986    private static long skipBytesFully(final DataInput input, long bytesToSkip) throws IOException {
    987        if (bytesToSkip < 1) {
    988            return 0;
    989        }
    990        long skipped = 0;
    991        while (bytesToSkip > Integer.MAX_VALUE) {
    992            final long skippedNow = skipBytesFully(input, Integer.MAX_VALUE);
    993            if (skippedNow == 0) {
    994                return skipped;
    995            }
    996            skipped += skippedNow;
    997            bytesToSkip -= skippedNow;
    998        }
    999        while (bytesToSkip > 0) {
    1000            final int skippedNow = input.skipBytes((int) bytesToSkip);
    1001            if (skippedNow == 0) {
    1002                return skipped;
    1003            }
    1004            skipped += skippedNow;
    1005            bytesToSkip -= skippedNow;
    1006        }
    1007        return skipped;
    1008    }
    1009
    1010    @Override
    1011    public String toString() {
    1012      return archive.toString();
    1013    }
    1014}