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 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 unecrypted 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    public void close() throws IOException {
130        if (file != null) {
131            try {
132                file.close();
133            } finally {
134                file = null;
135                if (password != null) {
136                    Arrays.fill(password, (byte) 0);
137                }
138                password = null;
139            }
140        }
141    }
142    
143    /**
144     * Returns the next Archive Entry in this archive.
145     *
146     * @return the next entry,
147     *         or {@code null} if there are no more entries
148     * @throws IOException if the next entry could not be read
149     */
150    public SevenZArchiveEntry getNextEntry() throws IOException {
151        if (currentEntryIndex >= archive.files.length - 1) {
152            return null;
153        }
154        ++currentEntryIndex;
155        final SevenZArchiveEntry entry = archive.files[currentEntryIndex];
156        buildDecodingStream();
157        return entry;
158    }
159    
160    /**
161     * Returns meta-data of all archive entries.
162     *
163     * <p>This method only provides meta-data, the entries can not be
164     * used to read the contents, you still need to process all
165     * entries in order using {@link #getNextEntry} for that.</p>
166     *
167     * <p>The content methods are only available for entries that have
168     * already been reached via {@link #getNextEntry}.</p>
169     *
170     * @return meta-data of all archive entries.
171     * @since 1.11
172     */
173    public Iterable<SevenZArchiveEntry> getEntries() {
174        return Arrays.asList(archive.files);
175    }
176    
177    private Archive readHeaders(byte[] password) throws IOException {
178        final byte[] signature = new byte[6];
179        file.readFully(signature);
180        if (!Arrays.equals(signature, sevenZSignature)) {
181            throw new IOException("Bad 7z signature");
182        }
183        // 7zFormat.txt has it wrong - it's first major then minor
184        final byte archiveVersionMajor = file.readByte();
185        final byte archiveVersionMinor = file.readByte();
186        if (archiveVersionMajor != 0) {
187            throw new IOException(String.format("Unsupported 7z version (%d,%d)",
188                    archiveVersionMajor, archiveVersionMinor));
189        }
190
191        final long startHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(file.readInt());
192        final StartHeader startHeader = readStartHeader(startHeaderCrc);
193        
194        final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize;
195        if (nextHeaderSizeInt != startHeader.nextHeaderSize) {
196            throw new IOException("cannot handle nextHeaderSize " + startHeader.nextHeaderSize);
197        }
198        file.seek(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset);
199        final byte[] nextHeader = new byte[nextHeaderSizeInt];
200        file.readFully(nextHeader);
201        final CRC32 crc = new CRC32();
202        crc.update(nextHeader);
203        if (startHeader.nextHeaderCrc != crc.getValue()) {
204            throw new IOException("NextHeader CRC mismatch");
205        }
206        
207        final ByteArrayInputStream byteStream = new ByteArrayInputStream(nextHeader);
208        DataInputStream nextHeaderInputStream = new DataInputStream(
209                byteStream);
210        Archive archive = new Archive();
211        int nid = nextHeaderInputStream.readUnsignedByte();
212        if (nid == NID.kEncodedHeader) {
213            nextHeaderInputStream =
214                readEncodedHeader(nextHeaderInputStream, archive, password);
215            // Archive gets rebuilt with the new header
216            archive = new Archive();
217            nid = nextHeaderInputStream.readUnsignedByte();
218        }
219        if (nid == NID.kHeader) {
220            readHeader(nextHeaderInputStream, archive);
221            nextHeaderInputStream.close();
222        } else {
223            throw new IOException("Broken or unsupported archive: no Header");
224        }
225        return archive;
226    }
227    
228    private StartHeader readStartHeader(final long startHeaderCrc) throws IOException {
229        final StartHeader startHeader = new StartHeader();
230        DataInputStream dataInputStream = null;
231        try {
232             dataInputStream = new DataInputStream(new CRC32VerifyingInputStream(
233                    new BoundedRandomAccessFileInputStream(file, 20), 20, startHeaderCrc));
234             startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong());
235             startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong());
236             startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt());
237             return startHeader;
238        } finally {
239            if (dataInputStream != null) {
240                dataInputStream.close();
241            }
242        }
243    }
244    
245    private void readHeader(final DataInput header, final Archive archive) throws IOException {
246        int nid = header.readUnsignedByte();
247        
248        if (nid == NID.kArchiveProperties) {
249            readArchiveProperties(header);
250            nid = header.readUnsignedByte();
251        }
252        
253        if (nid == NID.kAdditionalStreamsInfo) {
254            throw new IOException("Additional streams unsupported");
255            //nid = header.readUnsignedByte();
256        }
257        
258        if (nid == NID.kMainStreamsInfo) {
259            readStreamsInfo(header, archive);
260            nid = header.readUnsignedByte();
261        }
262        
263        if (nid == NID.kFilesInfo) {
264            readFilesInfo(header, archive);
265            nid = header.readUnsignedByte();
266        }
267        
268        if (nid != NID.kEnd) {
269            throw new IOException("Badly terminated header, found " + nid);
270        }
271    }
272    
273    private void readArchiveProperties(final DataInput input) throws IOException {
274        // FIXME: the reference implementation just throws them away?
275        int nid =  input.readUnsignedByte();
276        while (nid != NID.kEnd) {
277            final long propertySize = readUint64(input);
278            final byte[] property = new byte[(int)propertySize];
279            input.readFully(property);
280            nid = input.readUnsignedByte();
281        }
282    }
283    
284    private DataInputStream readEncodedHeader(final DataInputStream header, final Archive archive,
285                                              byte[] password) throws IOException {
286        readStreamsInfo(header, archive);
287        
288        // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage?
289        final Folder folder = archive.folders[0];
290        final int firstPackStreamIndex = 0;
291        final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
292                0;
293        
294        file.seek(folderOffset);
295        InputStream inputStreamStack = new BoundedRandomAccessFileInputStream(file,
296                archive.packSizes[firstPackStreamIndex]);
297        for (final Coder coder : folder.getOrderedCoders()) {
298            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
299                throw new IOException("Multi input/output stream coders are not yet supported");
300            }
301            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack,
302                    folder.getUnpackSizeForCoder(coder), coder, password);
303        }
304        if (folder.hasCrc) {
305            inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack,
306                    folder.getUnpackSize(), folder.crc);
307        }
308        final byte[] nextHeader = new byte[(int)folder.getUnpackSize()];
309        final DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack);
310        try {
311            nextHeaderInputStream.readFully(nextHeader);
312        } finally {
313            nextHeaderInputStream.close();
314        }
315        return new DataInputStream(new ByteArrayInputStream(nextHeader));
316    }
317    
318    private void readStreamsInfo(final DataInput header, final Archive archive) throws IOException {
319        int nid = header.readUnsignedByte();
320        
321        if (nid == NID.kPackInfo) {
322            readPackInfo(header, archive);
323            nid = header.readUnsignedByte();
324        }
325        
326        if (nid == NID.kUnpackInfo) {
327            readUnpackInfo(header, archive);
328            nid = header.readUnsignedByte();
329        } else {
330            // archive without unpack/coders info
331            archive.folders = new Folder[0];
332        }
333        
334        if (nid == NID.kSubStreamsInfo) {
335            readSubStreamsInfo(header, archive);
336            nid = header.readUnsignedByte();
337        }
338        
339        if (nid != NID.kEnd) {
340            throw new IOException("Badly terminated StreamsInfo");
341        }
342    }
343    
344    private void readPackInfo(final DataInput header, final Archive archive) throws IOException {
345        archive.packPos = readUint64(header);
346        final long numPackStreams = readUint64(header);
347        int nid = header.readUnsignedByte();
348        if (nid == NID.kSize) {
349            archive.packSizes = new long[(int)numPackStreams];
350            for (int i = 0; i < archive.packSizes.length; i++) {
351                archive.packSizes[i] = readUint64(header);
352            }
353            nid = header.readUnsignedByte();
354        }
355        
356        if (nid == NID.kCRC) {
357            archive.packCrcsDefined = readAllOrBits(header, (int)numPackStreams);
358            archive.packCrcs = new long[(int)numPackStreams];
359            for (int i = 0; i < (int)numPackStreams; i++) {
360                if (archive.packCrcsDefined.get(i)) {
361                    archive.packCrcs[i] = 0xffffFFFFL & Integer.reverseBytes(header.readInt());
362                }
363            }
364            
365            nid = header.readUnsignedByte();
366        }
367        
368        if (nid != NID.kEnd) {
369            throw new IOException("Badly terminated PackInfo (" + nid + ")");
370        }
371    }
372    
373    private void readUnpackInfo(final DataInput header, final Archive archive) throws IOException {
374        int nid = header.readUnsignedByte();
375        if (nid != NID.kFolder) {
376            throw new IOException("Expected kFolder, got " + nid);
377        }
378        final long numFolders = readUint64(header);
379        final Folder[] folders = new Folder[(int)numFolders];
380        archive.folders = folders;
381        final int external = header.readUnsignedByte();
382        if (external != 0) {
383            throw new IOException("External unsupported");
384        } else {
385            for (int i = 0; i < (int)numFolders; i++) {
386                folders[i] = readFolder(header);
387            }
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            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            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                    } else {
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                    }
668                    break;
669                }
670                case NID.kCTime: {
671                    final BitSet timesDefined = readAllOrBits(header, files.length);
672                    final int external = header.readUnsignedByte();
673                    if (external != 0) {
674                        throw new IOException("Unimplemented");
675                    } else {
676                        for (int i = 0; i < files.length; i++) {
677                            files[i].setHasCreationDate(timesDefined.get(i));
678                            if (files[i].getHasCreationDate()) {
679                                files[i].setCreationDate(Long.reverseBytes(header.readLong()));
680                            }
681                        }
682                    }
683                    break;
684                }
685                case NID.kATime: {
686                    final BitSet timesDefined = readAllOrBits(header, files.length);
687                    final int external = header.readUnsignedByte();
688                    if (external != 0) {
689                        throw new IOException("Unimplemented");
690                    } else {
691                        for (int i = 0; i < files.length; i++) {
692                            files[i].setHasAccessDate(timesDefined.get(i));
693                            if (files[i].getHasAccessDate()) {
694                                files[i].setAccessDate(Long.reverseBytes(header.readLong()));
695                            }
696                        }
697                    }
698                    break;
699                }
700                case NID.kMTime: {
701                    final BitSet timesDefined = readAllOrBits(header, files.length);
702                    final int external = header.readUnsignedByte();
703                    if (external != 0) {
704                        throw new IOException("Unimplemented");
705                    } else {
706                        for (int i = 0; i < files.length; i++) {
707                            files[i].setHasLastModifiedDate(timesDefined.get(i));
708                            if (files[i].getHasLastModifiedDate()) {
709                                files[i].setLastModifiedDate(Long.reverseBytes(header.readLong()));
710                            }
711                        }
712                    }
713                    break;
714                }
715                case NID.kWinAttributes: {
716                    final BitSet attributesDefined = readAllOrBits(header, files.length);
717                    final int external = header.readUnsignedByte();
718                    if (external != 0) {
719                        throw new IOException("Unimplemented");
720                    } else {
721                        for (int i = 0; i < files.length; i++) {
722                            files[i].setHasWindowsAttributes(attributesDefined.get(i));
723                            if (files[i].getHasWindowsAttributes()) {
724                                files[i].setWindowsAttributes(Integer.reverseBytes(header.readInt()));
725                            }
726                        }
727                    }
728                    break;
729                }
730                case NID.kStartPos: {
731                    throw new IOException("kStartPos is unsupported, please report");
732                }
733                case NID.kDummy: {
734                    // 7z 9.20 asserts the content is all zeros and ignores the property
735                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
736                    
737                    if (skipBytesFully(header, size) < size) {
738                        throw new IOException("Incomplete kDummy property");
739                    }
740                    break;
741                }
742
743                default: {
744                    // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287
745                    if (skipBytesFully(header, size) < size) {
746                        throw new IOException("Incomplete property of type " + propertyType);
747                    }
748                    break;
749                }
750            }
751        }
752        int nonEmptyFileCounter = 0;
753        int emptyFileCounter = 0;
754        for (int i = 0; i < files.length; i++) {
755            files[i].setHasStream(isEmptyStream == null ? true : !isEmptyStream.get(i));
756            if (files[i].hasStream()) {
757                files[i].setDirectory(false);
758                files[i].setAntiItem(false);
759                files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter));
760                files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]);
761                files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]);
762                ++nonEmptyFileCounter;
763            } else {
764                files[i].setDirectory(isEmptyFile == null ? true : !isEmptyFile.get(emptyFileCounter));
765                files[i].setAntiItem(isAnti == null ? false : isAnti.get(emptyFileCounter));
766                files[i].setHasCrc(false);
767                files[i].setSize(0);
768                ++emptyFileCounter;
769            }
770        }
771        archive.files = files;
772        calculateStreamMap(archive);
773    }
774    
775    private void calculateStreamMap(final Archive archive) throws IOException {
776        final StreamMap streamMap = new StreamMap();
777        
778        int nextFolderPackStreamIndex = 0;
779        final int numFolders = archive.folders != null ? archive.folders.length : 0;
780        streamMap.folderFirstPackStreamIndex = new int[numFolders];
781        for (int i = 0; i < numFolders; i++) {
782            streamMap.folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex;
783            nextFolderPackStreamIndex += archive.folders[i].packedStreams.length;
784        }
785        
786        long nextPackStreamOffset = 0;
787        final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0;
788        streamMap.packStreamOffsets = new long[numPackSizes];
789        for (int i = 0; i < numPackSizes; i++) {
790            streamMap.packStreamOffsets[i] = nextPackStreamOffset;
791            nextPackStreamOffset += archive.packSizes[i]; 
792        }
793        
794        streamMap.folderFirstFileIndex = new int[numFolders];
795        streamMap.fileFolderIndex = new int[archive.files.length];
796        int nextFolderIndex = 0;
797        int nextFolderUnpackStreamIndex = 0;
798        for (int i = 0; i < archive.files.length; i++) {
799            if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) {
800                streamMap.fileFolderIndex[i] = -1;
801                continue;
802            }
803            if (nextFolderUnpackStreamIndex == 0) {
804                for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) {
805                    streamMap.folderFirstFileIndex[nextFolderIndex] = i;
806                    if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) {
807                        break;
808                    }
809                }
810                if (nextFolderIndex >= archive.folders.length) {
811                    throw new IOException("Too few folders in archive");
812                }
813            }
814            streamMap.fileFolderIndex[i] = nextFolderIndex;
815            if (!archive.files[i].hasStream()) {
816                continue;
817            }
818            ++nextFolderUnpackStreamIndex;
819            if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) {
820                ++nextFolderIndex;
821                nextFolderUnpackStreamIndex = 0;
822            }
823        }
824        
825        archive.streamMap = streamMap;
826    }
827    
828    private void buildDecodingStream() throws IOException {
829        final int folderIndex = archive.streamMap.fileFolderIndex[currentEntryIndex];
830        if (folderIndex < 0) {
831            deferredBlockStreams.clear();
832            // TODO: previously it'd return an empty stream?
833            // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0);
834            return;
835        }
836        final SevenZArchiveEntry file = archive.files[currentEntryIndex];
837        if (currentFolderIndex == folderIndex) {
838            // (COMPRESS-320).
839            // The current entry is within the same (potentially opened) folder. The
840            // previous stream has to be fully decoded before we can start reading
841            // but don't do it eagerly -- if the user skips over the entire folder nothing
842            // is effectively decompressed.
843
844            file.setContentMethods(archive.files[currentEntryIndex - 1].getContentMethods());
845        } else {
846            // We're opening a new folder. Discard any queued streams/ folder stream.
847            currentFolderIndex = folderIndex;
848            deferredBlockStreams.clear();
849            if (currentFolderInputStream != null) {
850                currentFolderInputStream.close();
851                currentFolderInputStream = null;
852            }
853            
854            final Folder folder = archive.folders[folderIndex];
855            final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex];
856            final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos +
857                    archive.streamMap.packStreamOffsets[firstPackStreamIndex];
858            currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file);
859        }
860
861        InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize());
862        if (file.getHasCrc()) {
863            fileStream = new CRC32VerifyingInputStream(fileStream, file.getSize(), file.getCrcValue());
864        }
865        
866        deferredBlockStreams.add(fileStream);
867    }
868
869    private InputStream buildDecoderStack(final Folder folder, final long folderOffset,
870                final int firstPackStreamIndex, SevenZArchiveEntry entry) throws IOException {
871        file.seek(folderOffset);
872        InputStream inputStreamStack =
873            new BufferedInputStream(
874              new BoundedRandomAccessFileInputStream(file,
875                  archive.packSizes[firstPackStreamIndex]));
876        LinkedList<SevenZMethodConfiguration> methods = new LinkedList<SevenZMethodConfiguration>();
877        for (final Coder coder : folder.getOrderedCoders()) {
878            if (coder.numInStreams != 1 || coder.numOutStreams != 1) {
879                throw new IOException("Multi input/output stream coders are not yet supported");
880            }
881            SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId);
882            inputStreamStack = Coders.addDecoder(fileName, inputStreamStack,
883                    folder.getUnpackSizeForCoder(coder), coder, password);
884            methods.addFirst(new SevenZMethodConfiguration(method,
885                     Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack)));
886        }
887        entry.setContentMethods(methods);
888        if (folder.hasCrc) {
889            return new CRC32VerifyingInputStream(inputStreamStack,
890                    folder.getUnpackSize(), folder.crc);
891        } else {
892            return inputStreamStack;
893        }
894    }
895    
896    /**
897     * Reads a byte of data.
898     * 
899     * @return the byte read, or -1 if end of input is reached
900     * @throws IOException
901     *             if an I/O error has occurred
902     */
903    public int read() throws IOException {
904        return getCurrentStream().read();
905    }
906    
907    private InputStream getCurrentStream() throws IOException {
908        if (deferredBlockStreams.isEmpty()) {
909            throw new IllegalStateException("No current 7z entry (call getNextEntry() first).");
910        }
911        
912        while (deferredBlockStreams.size() > 1) {
913            // In solid compression mode we need to decompress all leading folder'
914            // streams to get access to an entry. We defer this until really needed
915            // so that entire blocks can be skipped without wasting time for decompression.
916            InputStream stream = deferredBlockStreams.remove(0);
917            IOUtils.skip(stream, Long.MAX_VALUE);
918            stream.close();
919        }
920
921        return deferredBlockStreams.get(0);
922    }
923
924    /**
925     * Reads data into an array of bytes.
926     * 
927     * @param b the array to write data to
928     * @return the number of bytes read, or -1 if end of input is reached
929     * @throws IOException
930     *             if an I/O error has occurred
931     */
932    public int read(byte[] b) throws IOException {
933        return read(b, 0, b.length);
934    }
935    
936    /**
937     * Reads data into an array of bytes.
938     * 
939     * @param b the array to write data to
940     * @param off offset into the buffer to start filling at
941     * @param len of bytes to read
942     * @return the number of bytes read, or -1 if end of input is reached
943     * @throws IOException
944     *             if an I/O error has occurred
945     */
946    public int read(byte[] b, int off, int len) throws IOException {
947        return getCurrentStream().read(b, off, len);
948    }
949    
950    private static long readUint64(final DataInput in) throws IOException {
951        // long rather than int as it might get shifted beyond the range of an int
952        long firstByte = in.readUnsignedByte();
953        int mask = 0x80;
954        long value = 0;
955        for (int i = 0; i < 8; i++) {
956            if ((firstByte & mask) == 0) {
957                return value | ((firstByte & (mask - 1)) << (8 * i));
958            }
959            long nextByte = in.readUnsignedByte();
960            value |= nextByte << (8 * i);
961            mask >>>= 1;
962        }
963        return value;
964    }
965
966    /**
967     * Checks if the signature matches what is expected for a 7z file.
968     *
969     * @param signature
970     *            the bytes to check
971     * @param length
972     *            the number of bytes to check
973     * @return true, if this is the signature of a 7z archive.
974     * @since 1.8
975     */
976    public static boolean matches(byte[] signature, int length) {
977        if (length < sevenZSignature.length) {
978            return false;
979        }
980
981        for (int i = 0; i < sevenZSignature.length; i++) {
982            if (signature[i] != sevenZSignature[i]) {
983                return false;
984            }
985        }
986        return true;
987    }
988
989    private static long skipBytesFully(DataInput input, long bytesToSkip) throws IOException {
990        if (bytesToSkip < 1) {
991            return 0;
992        }
993        long skipped = 0;
994        while (bytesToSkip > Integer.MAX_VALUE) {
995            long skippedNow = skipBytesFully(input, Integer.MAX_VALUE);
996            if (skippedNow == 0) {
997                return skipped;
998            }
999            skipped += skippedNow;
1000            bytesToSkip -= skippedNow;
1001        }
1002        while (bytesToSkip > 0) {
1003            int skippedNow = input.skipBytes((int) bytesToSkip);
1004            if (skippedNow == 0) {
1005                return skipped;
1006            }
1007            skipped += skippedNow;
1008            bytesToSkip -= skippedNow;
1009        }
1010        return skipped;
1011    }
1012    
1013    @Override
1014    public String toString() {
1015      return archive.toString();
1016    }
1017}