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