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