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}