| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| package org.apache.commons.compress.archivers.sevenz; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.Closeable; |
| import java.io.DataInput; |
| import java.io.DataInputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.RandomAccessFile; |
| import java.util.Arrays; |
| import java.util.BitSet; |
| import java.util.LinkedList; |
| import java.util.zip.CRC32; |
| |
| import org.apache.commons.compress.utils.BoundedInputStream; |
| import org.apache.commons.compress.utils.CRC32VerifyingInputStream; |
| import org.apache.commons.compress.utils.CharsetNames; |
| import org.apache.commons.compress.utils.IOUtils; |
| |
| /** |
| * Reads a 7z file, using RandomAccessFile under |
| * the covers. |
| * <p> |
| * The 7z file format is a flexible container |
| * that can contain many compression and |
| * encryption types, but at the moment only |
| * only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256 |
| * are supported. |
| * <p> |
| * The format is very Windows/Intel specific, |
| * so it uses little-endian byte order, |
| * doesn't store user/group or permission bits, |
| * and represents times using NTFS timestamps |
| * (100 nanosecond units since 1 January 1601). |
| * Hence the official tools recommend against |
| * using it for backup purposes on *nix, and |
| * recommend .tar.7z or .tar.lzma or .tar.xz |
| * instead. |
| * <p> |
| * Both the header and file contents may be |
| * compressed and/or encrypted. With both |
| * encrypted, neither file names nor file |
| * contents can be read, but the use of |
| * encryption isn't plausibly deniable. |
| * |
| * @NotThreadSafe |
| * @since 1.6 |
| */ |
| public class SevenZFile implements Closeable { |
| static final int SIGNATURE_HEADER_SIZE = 32; |
| |
| private RandomAccessFile file; |
| private final Archive archive; |
| private int currentEntryIndex = -1; |
| private int currentFolderIndex = -1; |
| private InputStream currentFolderInputStream = null; |
| private InputStream currentEntryInputStream = null; |
| private byte[] password; |
| |
| static final byte[] sevenZSignature = { |
| (byte)'7', (byte)'z', (byte)0xBC, (byte)0xAF, (byte)0x27, (byte)0x1C |
| }; |
| |
| /** |
| * Reads a file as 7z archive |
| * |
| * @param filename the file to read |
| * @param password optional password if the archive is encrypted - |
| * the byte array is supposed to be the UTF16-LE encoded |
| * representation of the password. |
| * @throws IOException if reading the archive fails |
| */ |
| public SevenZFile(final File filename, final byte[] password) throws IOException { |
| boolean succeeded = false; |
| this.file = new RandomAccessFile(filename, "r"); |
| try { |
| archive = readHeaders(password); |
| if (password != null) { |
| this.password = new byte[password.length]; |
| System.arraycopy(password, 0, this.password, 0, password.length); |
| } else { |
| this.password = null; |
| } |
| succeeded = true; |
| } finally { |
| if (!succeeded) { |
| this.file.close(); |
| } |
| } |
| } |
| |
| /** |
| * Reads a file as unecrypted 7z archive |
| * |
| * @param filename the file to read |
| * @throws IOException if reading the archive fails |
| */ |
| public SevenZFile(final File filename) throws IOException { |
| this(filename, null); |
| } |
| |
| /** |
| * Closes the archive. |
| * @throws IOException if closing the file fails |
| */ |
| public void close() throws IOException { |
| if (file != null) { |
| try { |
| file.close(); |
| } finally { |
| file = null; |
| if (password != null) { |
| Arrays.fill(password, (byte) 0); |
| } |
| password = null; |
| } |
| } |
| } |
| |
| /** |
| * Returns the next Archive Entry in this archive. |
| * |
| * @return the next entry, |
| * or {@code null} if there are no more entries |
| * @throws IOException if the next entry could not be read |
| */ |
| public SevenZArchiveEntry getNextEntry() throws IOException { |
| if (currentEntryIndex >= archive.files.length - 1) { |
| return null; |
| } |
| ++currentEntryIndex; |
| final SevenZArchiveEntry entry = archive.files[currentEntryIndex]; |
| buildDecodingStream(); |
| return entry; |
| } |
| |
| private Archive readHeaders(byte[] password) throws IOException { |
| final byte[] signature = new byte[6]; |
| file.readFully(signature); |
| if (!Arrays.equals(signature, sevenZSignature)) { |
| throw new IOException("Bad 7z signature"); |
| } |
| // 7zFormat.txt has it wrong - it's first major then minor |
| final byte archiveVersionMajor = file.readByte(); |
| final byte archiveVersionMinor = file.readByte(); |
| if (archiveVersionMajor != 0) { |
| throw new IOException(String.format("Unsupported 7z version (%d,%d)", |
| Byte.valueOf(archiveVersionMajor), Byte.valueOf(archiveVersionMinor))); |
| } |
| |
| final long startHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(file.readInt()); |
| final StartHeader startHeader = readStartHeader(startHeaderCrc); |
| |
| final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize; |
| if (nextHeaderSizeInt != startHeader.nextHeaderSize) { |
| throw new IOException("cannot handle nextHeaderSize " + startHeader.nextHeaderSize); |
| } |
| file.seek(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset); |
| final byte[] nextHeader = new byte[nextHeaderSizeInt]; |
| file.readFully(nextHeader); |
| final CRC32 crc = new CRC32(); |
| crc.update(nextHeader); |
| if (startHeader.nextHeaderCrc != crc.getValue()) { |
| throw new IOException("NextHeader CRC mismatch"); |
| } |
| |
| final ByteArrayInputStream byteStream = new ByteArrayInputStream(nextHeader); |
| DataInputStream nextHeaderInputStream = new DataInputStream( |
| byteStream); |
| Archive archive = new Archive(); |
| int nid = nextHeaderInputStream.readUnsignedByte(); |
| if (nid == NID.kEncodedHeader) { |
| nextHeaderInputStream = |
| readEncodedHeader(nextHeaderInputStream, archive, password); |
| // Archive gets rebuilt with the new header |
| archive = new Archive(); |
| nid = nextHeaderInputStream.readUnsignedByte(); |
| } |
| if (nid == NID.kHeader) { |
| readHeader(nextHeaderInputStream, archive); |
| nextHeaderInputStream.close(); |
| } else { |
| throw new IOException("Broken or unsupported archive: no Header"); |
| } |
| return archive; |
| } |
| |
| private StartHeader readStartHeader(final long startHeaderCrc) throws IOException { |
| final StartHeader startHeader = new StartHeader(); |
| DataInputStream dataInputStream = null; |
| try { |
| dataInputStream = new DataInputStream(new CRC32VerifyingInputStream( |
| new BoundedRandomAccessFileInputStream(file, 20), 20, startHeaderCrc)); |
| startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong()); |
| startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong()); |
| startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt()); |
| return startHeader; |
| } finally { |
| if (dataInputStream != null) { |
| dataInputStream.close(); |
| } |
| } |
| } |
| |
| private void readHeader(final DataInput header, final Archive archive) throws IOException { |
| int nid = header.readUnsignedByte(); |
| |
| if (nid == NID.kArchiveProperties) { |
| readArchiveProperties(header); |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid == NID.kAdditionalStreamsInfo) { |
| throw new IOException("Additional streams unsupported"); |
| //nid = header.readUnsignedByte(); |
| } |
| |
| if (nid == NID.kMainStreamsInfo) { |
| readStreamsInfo(header, archive); |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid == NID.kFilesInfo) { |
| readFilesInfo(header, archive); |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid != NID.kEnd) { |
| throw new IOException("Badly terminated header"); |
| } |
| } |
| |
| private void readArchiveProperties(final DataInput input) throws IOException { |
| // FIXME: the reference implementation just throws them away? |
| int nid = input.readUnsignedByte(); |
| while (nid != NID.kEnd) { |
| final long propertySize = readUint64(input); |
| final byte[] property = new byte[(int)propertySize]; |
| input.readFully(property); |
| nid = input.readUnsignedByte(); |
| } |
| } |
| |
| private DataInputStream readEncodedHeader(final DataInputStream header, final Archive archive, |
| byte[] password) throws IOException { |
| readStreamsInfo(header, archive); |
| |
| // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage? |
| final Folder folder = archive.folders[0]; |
| final int firstPackStreamIndex = 0; |
| final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + |
| 0; |
| |
| file.seek(folderOffset); |
| InputStream inputStreamStack = new BoundedRandomAccessFileInputStream(file, |
| archive.packSizes[firstPackStreamIndex]); |
| for (final Coder coder : folder.getOrderedCoders()) { |
| if (coder.numInStreams != 1 || coder.numOutStreams != 1) { |
| throw new IOException("Multi input/output stream coders are not yet supported"); |
| } |
| inputStreamStack = Coders.addDecoder(inputStreamStack, coder, password); |
| } |
| if (folder.hasCrc) { |
| inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack, |
| folder.getUnpackSize(), folder.crc); |
| } |
| final byte[] nextHeader = new byte[(int)folder.getUnpackSize()]; |
| final DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack); |
| try { |
| nextHeaderInputStream.readFully(nextHeader); |
| } finally { |
| nextHeaderInputStream.close(); |
| } |
| return new DataInputStream(new ByteArrayInputStream(nextHeader)); |
| } |
| |
| private void readStreamsInfo(final DataInput header, final Archive archive) throws IOException { |
| int nid = header.readUnsignedByte(); |
| |
| if (nid == NID.kPackInfo) { |
| readPackInfo(header, archive); |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid == NID.kUnpackInfo) { |
| readUnpackInfo(header, archive); |
| nid = header.readUnsignedByte(); |
| } else { |
| // archive without unpack/coders info |
| archive.folders = new Folder[0]; |
| } |
| |
| if (nid == NID.kSubStreamsInfo) { |
| readSubStreamsInfo(header, archive); |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid != NID.kEnd) { |
| throw new IOException("Badly terminated StreamsInfo"); |
| } |
| } |
| |
| private void readPackInfo(final DataInput header, final Archive archive) throws IOException { |
| archive.packPos = readUint64(header); |
| final long numPackStreams = readUint64(header); |
| int nid = header.readUnsignedByte(); |
| if (nid == NID.kSize) { |
| archive.packSizes = new long[(int)numPackStreams]; |
| for (int i = 0; i < archive.packSizes.length; i++) { |
| archive.packSizes[i] = readUint64(header); |
| } |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid == NID.kCRC) { |
| archive.packCrcsDefined = readAllOrBits(header, (int)numPackStreams); |
| archive.packCrcs = new long[(int)numPackStreams]; |
| for (int i = 0; i < (int)numPackStreams; i++) { |
| if (archive.packCrcsDefined.get(i)) { |
| archive.packCrcs[i] = 0xffffFFFFL & Integer.reverseBytes(header.readInt()); |
| } |
| } |
| |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid != NID.kEnd) { |
| throw new IOException("Badly terminated PackInfo (" + nid + ")"); |
| } |
| } |
| |
| private void readUnpackInfo(final DataInput header, final Archive archive) throws IOException { |
| int nid = header.readUnsignedByte(); |
| if (nid != NID.kFolder) { |
| throw new IOException("Expected kFolder, got " + nid); |
| } |
| final long numFolders = readUint64(header); |
| final Folder[] folders = new Folder[(int)numFolders]; |
| archive.folders = folders; |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("External unsupported"); |
| } else { |
| for (int i = 0; i < (int)numFolders; i++) { |
| folders[i] = readFolder(header); |
| } |
| } |
| |
| nid = header.readUnsignedByte(); |
| if (nid != NID.kCodersUnpackSize) { |
| throw new IOException("Expected kCodersUnpackSize, got " + nid); |
| } |
| for (final Folder folder : folders) { |
| folder.unpackSizes = new long[(int)folder.totalOutputStreams]; |
| for (int i = 0; i < folder.totalOutputStreams; i++) { |
| folder.unpackSizes[i] = readUint64(header); |
| } |
| } |
| |
| nid = header.readUnsignedByte(); |
| if (nid == NID.kCRC) { |
| final BitSet crcsDefined = readAllOrBits(header, (int)numFolders); |
| for (int i = 0; i < (int)numFolders; i++) { |
| if (crcsDefined.get(i)) { |
| folders[i].hasCrc = true; |
| folders[i].crc = 0xffffFFFFL & Integer.reverseBytes(header.readInt()); |
| } else { |
| folders[i].hasCrc = false; |
| } |
| } |
| |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid != NID.kEnd) { |
| throw new IOException("Badly terminated UnpackInfo"); |
| } |
| } |
| |
| private void readSubStreamsInfo(final DataInput header, final Archive archive) throws IOException { |
| for (final Folder folder : archive.folders) { |
| folder.numUnpackSubStreams = 1; |
| } |
| int totalUnpackStreams = archive.folders.length; |
| |
| int nid = header.readUnsignedByte(); |
| if (nid == NID.kNumUnpackStream) { |
| totalUnpackStreams = 0; |
| for (final Folder folder : archive.folders) { |
| final long numStreams = readUint64(header); |
| folder.numUnpackSubStreams = (int)numStreams; |
| totalUnpackStreams += numStreams; |
| } |
| nid = header.readUnsignedByte(); |
| } |
| |
| final SubStreamsInfo subStreamsInfo = new SubStreamsInfo(); |
| subStreamsInfo.unpackSizes = new long[totalUnpackStreams]; |
| subStreamsInfo.hasCrc = new BitSet(totalUnpackStreams); |
| subStreamsInfo.crcs = new long[totalUnpackStreams]; |
| |
| int nextUnpackStream = 0; |
| for (final Folder folder : archive.folders) { |
| if (folder.numUnpackSubStreams == 0) { |
| continue; |
| } |
| long sum = 0; |
| if (nid == NID.kSize) { |
| for (int i = 0; i < folder.numUnpackSubStreams - 1; i++) { |
| final long size = readUint64(header); |
| subStreamsInfo.unpackSizes[nextUnpackStream++] = size; |
| sum += size; |
| } |
| } |
| subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum; |
| } |
| if (nid == NID.kSize) { |
| nid = header.readUnsignedByte(); |
| } |
| |
| int numDigests = 0; |
| for (final Folder folder : archive.folders) { |
| if (folder.numUnpackSubStreams != 1 || !folder.hasCrc) { |
| numDigests += folder.numUnpackSubStreams; |
| } |
| } |
| |
| if (nid == NID.kCRC) { |
| final BitSet hasMissingCrc = readAllOrBits(header, numDigests); |
| final long[] missingCrcs = new long[numDigests]; |
| for (int i = 0; i < numDigests; i++) { |
| if (hasMissingCrc.get(i)) { |
| missingCrcs[i] = 0xffffFFFFL & Integer.reverseBytes(header.readInt()); |
| } |
| } |
| int nextCrc = 0; |
| int nextMissingCrc = 0; |
| for (final Folder folder: archive.folders) { |
| if (folder.numUnpackSubStreams == 1 && folder.hasCrc) { |
| subStreamsInfo.hasCrc.set(nextCrc, true); |
| subStreamsInfo.crcs[nextCrc] = folder.crc; |
| ++nextCrc; |
| } else { |
| for (int i = 0; i < folder.numUnpackSubStreams; i++) { |
| subStreamsInfo.hasCrc.set(nextCrc, hasMissingCrc.get(nextMissingCrc)); |
| subStreamsInfo.crcs[nextCrc] = missingCrcs[nextMissingCrc]; |
| ++nextCrc; |
| ++nextMissingCrc; |
| } |
| } |
| } |
| |
| nid = header.readUnsignedByte(); |
| } |
| |
| if (nid != NID.kEnd) { |
| throw new IOException("Badly terminated SubStreamsInfo"); |
| } |
| |
| archive.subStreamsInfo = subStreamsInfo; |
| } |
| |
| private Folder readFolder(final DataInput header) throws IOException { |
| final Folder folder = new Folder(); |
| |
| final long numCoders = readUint64(header); |
| final Coder[] coders = new Coder[(int)numCoders]; |
| long totalInStreams = 0; |
| long totalOutStreams = 0; |
| for (int i = 0; i < coders.length; i++) { |
| coders[i] = new Coder(); |
| int bits = header.readUnsignedByte(); |
| final int idSize = bits & 0xf; |
| final boolean isSimple = (bits & 0x10) == 0; |
| final boolean hasAttributes = (bits & 0x20) != 0; |
| final boolean moreAlternativeMethods = (bits & 0x80) != 0; |
| |
| coders[i].decompressionMethodId = new byte[idSize]; |
| header.readFully(coders[i].decompressionMethodId); |
| if (isSimple) { |
| coders[i].numInStreams = 1; |
| coders[i].numOutStreams = 1; |
| } else { |
| coders[i].numInStreams = readUint64(header); |
| coders[i].numOutStreams = readUint64(header); |
| } |
| totalInStreams += coders[i].numInStreams; |
| totalOutStreams += coders[i].numOutStreams; |
| if (hasAttributes) { |
| final long propertiesSize = readUint64(header); |
| coders[i].properties = new byte[(int)propertiesSize]; |
| header.readFully(coders[i].properties); |
| } |
| // would need to keep looping as above: |
| while (moreAlternativeMethods) { |
| throw new IOException("Alternative methods are unsupported, please report. " + |
| "The reference implementation doesn't support them either."); |
| } |
| } |
| folder.coders = coders; |
| folder.totalInputStreams = totalInStreams; |
| folder.totalOutputStreams = totalOutStreams; |
| |
| if (totalOutStreams == 0) { |
| throw new IOException("Total output streams can't be 0"); |
| } |
| final long numBindPairs = totalOutStreams - 1; |
| final BindPair[] bindPairs = new BindPair[(int)numBindPairs]; |
| for (int i = 0; i < bindPairs.length; i++) { |
| bindPairs[i] = new BindPair(); |
| bindPairs[i].inIndex = readUint64(header); |
| bindPairs[i].outIndex = readUint64(header); |
| } |
| folder.bindPairs = bindPairs; |
| |
| if (totalInStreams < numBindPairs) { |
| throw new IOException("Total input streams can't be less than the number of bind pairs"); |
| } |
| final long numPackedStreams = totalInStreams - numBindPairs; |
| final long packedStreams[] = new long[(int)numPackedStreams]; |
| if (numPackedStreams == 1) { |
| int i; |
| for (i = 0; i < (int)totalInStreams; i++) { |
| if (folder.findBindPairForInStream(i) < 0) { |
| break; |
| } |
| } |
| if (i == (int)totalInStreams) { |
| throw new IOException("Couldn't find stream's bind pair index"); |
| } |
| packedStreams[0] = i; |
| } else { |
| for (int i = 0; i < (int)numPackedStreams; i++) { |
| packedStreams[i] = readUint64(header); |
| } |
| } |
| folder.packedStreams = packedStreams; |
| |
| return folder; |
| } |
| |
| private BitSet readAllOrBits(final DataInput header, final int size) throws IOException { |
| final int areAllDefined = header.readUnsignedByte(); |
| final BitSet bits; |
| if (areAllDefined != 0) { |
| bits = new BitSet(size); |
| for (int i = 0; i < size; i++) { |
| bits.set(i, true); |
| } |
| } else { |
| bits = readBits(header, size); |
| } |
| return bits; |
| } |
| |
| private BitSet readBits(final DataInput header, final int size) throws IOException { |
| final BitSet bits = new BitSet(size); |
| int mask = 0; |
| int cache = 0; |
| for (int i = 0; i < size; i++) { |
| if (mask == 0) { |
| mask = 0x80; |
| cache = header.readUnsignedByte(); |
| } |
| bits.set(i, (cache & mask) != 0); |
| mask >>>= 1; |
| } |
| return bits; |
| } |
| |
| private void readFilesInfo(final DataInput header, final Archive archive) throws IOException { |
| final long numFiles = readUint64(header); |
| final SevenZArchiveEntry[] files = new SevenZArchiveEntry[(int)numFiles]; |
| for (int i = 0; i < files.length; i++) { |
| files[i] = new SevenZArchiveEntry(); |
| } |
| BitSet isEmptyStream = null; |
| BitSet isEmptyFile = null; |
| BitSet isAnti = null; |
| while (true) { |
| final int propertyType = header.readUnsignedByte(); |
| if (propertyType == 0) { |
| break; |
| } |
| long size = readUint64(header); |
| switch (propertyType) { |
| case NID.kEmptyStream: { |
| isEmptyStream = readBits(header, files.length); |
| break; |
| } |
| case NID.kEmptyFile: { |
| if (isEmptyStream == null) { // protect against NPE |
| throw new IOException("Header format error: kEmptyStream must appear before kEmptyFile"); |
| } |
| isEmptyFile = readBits(header, isEmptyStream.cardinality()); |
| break; |
| } |
| case NID.kAnti: { |
| if (isEmptyStream == null) { // protect against NPE |
| throw new IOException("Header format error: kEmptyStream must appear before kAnti"); |
| } |
| isAnti = readBits(header, isEmptyStream.cardinality()); |
| break; |
| } |
| case NID.kName: { |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("Not implemented"); |
| } else { |
| if (((size - 1) & 1) != 0) { |
| throw new IOException("File names length invalid"); |
| } |
| final byte[] names = new byte[(int)(size - 1)]; |
| header.readFully(names); |
| int nextFile = 0; |
| int nextName = 0; |
| for (int i = 0; i < names.length; i += 2) { |
| if (names[i] == 0 && names[i+1] == 0) { |
| files[nextFile++].setName(new String(names, nextName, i-nextName, CharsetNames.UTF_16LE)); |
| nextName = i + 2; |
| } |
| } |
| if (nextName != names.length || nextFile != files.length) { |
| throw new IOException("Error parsing file names"); |
| } |
| } |
| break; |
| } |
| case NID.kCTime: { |
| final BitSet timesDefined = readAllOrBits(header, files.length); |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("Unimplemented"); |
| } else { |
| for (int i = 0; i < files.length; i++) { |
| files[i].setHasCreationDate(timesDefined.get(i)); |
| if (files[i].getHasCreationDate()) { |
| files[i].setCreationDate(Long.reverseBytes(header.readLong())); |
| } |
| } |
| } |
| break; |
| } |
| case NID.kATime: { |
| final BitSet timesDefined = readAllOrBits(header, files.length); |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("Unimplemented"); |
| } else { |
| for (int i = 0; i < files.length; i++) { |
| files[i].setHasAccessDate(timesDefined.get(i)); |
| if (files[i].getHasAccessDate()) { |
| files[i].setAccessDate(Long.reverseBytes(header.readLong())); |
| } |
| } |
| } |
| break; |
| } |
| case NID.kMTime: { |
| final BitSet timesDefined = readAllOrBits(header, files.length); |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("Unimplemented"); |
| } else { |
| for (int i = 0; i < files.length; i++) { |
| files[i].setHasLastModifiedDate(timesDefined.get(i)); |
| if (files[i].getHasLastModifiedDate()) { |
| files[i].setLastModifiedDate(Long.reverseBytes(header.readLong())); |
| } |
| } |
| } |
| break; |
| } |
| case NID.kWinAttributes: { |
| final BitSet attributesDefined = readAllOrBits(header, files.length); |
| final int external = header.readUnsignedByte(); |
| if (external != 0) { |
| throw new IOException("Unimplemented"); |
| } else { |
| for (int i = 0; i < files.length; i++) { |
| files[i].setHasWindowsAttributes(attributesDefined.get(i)); |
| if (files[i].getHasWindowsAttributes()) { |
| files[i].setWindowsAttributes(Integer.reverseBytes(header.readInt())); |
| } |
| } |
| } |
| break; |
| } |
| case NID.kStartPos: { |
| throw new IOException("kStartPos is unsupported, please report"); |
| } |
| case NID.kDummy: { |
| throw new IOException("kDummy is unsupported, please report"); |
| } |
| |
| default: { |
| throw new IOException("Unknown property " + propertyType); |
| // FIXME: Should actually: |
| //header.skipBytes((int)size); |
| } |
| } |
| } |
| int nonEmptyFileCounter = 0; |
| int emptyFileCounter = 0; |
| for (int i = 0; i < files.length; i++) { |
| files[i].setHasStream(isEmptyStream == null ? true : !isEmptyStream.get(i)); |
| if (files[i].hasStream()) { |
| files[i].setDirectory(false); |
| files[i].setAntiItem(false); |
| files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter)); |
| files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]); |
| files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]); |
| ++nonEmptyFileCounter; |
| } else { |
| files[i].setDirectory(isEmptyFile == null ? true : !isEmptyFile.get(emptyFileCounter)); |
| files[i].setAntiItem(isAnti == null ? false : isAnti.get(emptyFileCounter)); |
| files[i].setHasCrc(false); |
| files[i].setSize(0); |
| ++emptyFileCounter; |
| } |
| } |
| archive.files = files; |
| calculateStreamMap(archive); |
| } |
| |
| private void calculateStreamMap(final Archive archive) throws IOException { |
| final StreamMap streamMap = new StreamMap(); |
| |
| int nextFolderPackStreamIndex = 0; |
| final int numFolders = archive.folders != null ? archive.folders.length : 0; |
| streamMap.folderFirstPackStreamIndex = new int[numFolders]; |
| for (int i = 0; i < numFolders; i++) { |
| streamMap.folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex; |
| nextFolderPackStreamIndex += archive.folders[i].packedStreams.length; |
| } |
| |
| long nextPackStreamOffset = 0; |
| final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0; |
| streamMap.packStreamOffsets = new long[numPackSizes]; |
| for (int i = 0; i < numPackSizes; i++) { |
| streamMap.packStreamOffsets[i] = nextPackStreamOffset; |
| nextPackStreamOffset += archive.packSizes[i]; |
| } |
| |
| streamMap.folderFirstFileIndex = new int[numFolders]; |
| streamMap.fileFolderIndex = new int[archive.files.length]; |
| int nextFolderIndex = 0; |
| int nextFolderUnpackStreamIndex = 0; |
| for (int i = 0; i < archive.files.length; i++) { |
| if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) { |
| streamMap.fileFolderIndex[i] = -1; |
| continue; |
| } |
| if (nextFolderUnpackStreamIndex == 0) { |
| for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) { |
| streamMap.folderFirstFileIndex[nextFolderIndex] = i; |
| if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) { |
| break; |
| } |
| } |
| if (nextFolderIndex >= archive.folders.length) { |
| throw new IOException("Too few folders in archive"); |
| } |
| } |
| streamMap.fileFolderIndex[i] = nextFolderIndex; |
| if (!archive.files[i].hasStream()) { |
| continue; |
| } |
| ++nextFolderUnpackStreamIndex; |
| if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) { |
| ++nextFolderIndex; |
| nextFolderUnpackStreamIndex = 0; |
| } |
| } |
| |
| archive.streamMap = streamMap; |
| } |
| |
| private void buildDecodingStream() throws IOException { |
| final int folderIndex = archive.streamMap.fileFolderIndex[currentEntryIndex]; |
| if (folderIndex < 0) { |
| currentEntryInputStream = new BoundedInputStream( |
| new ByteArrayInputStream(new byte[0]), 0); |
| return; |
| } |
| final SevenZArchiveEntry file = archive.files[currentEntryIndex]; |
| if (currentFolderIndex == folderIndex) { |
| // need to advance the folder input stream past the current file |
| drainPreviousEntry(); |
| file.setContentMethods(archive.files[currentEntryIndex - 1].getContentMethods()); |
| } else { |
| currentFolderIndex = folderIndex; |
| if (currentFolderInputStream != null) { |
| currentFolderInputStream.close(); |
| currentFolderInputStream = null; |
| } |
| |
| final Folder folder = archive.folders[folderIndex]; |
| final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; |
| final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + |
| archive.streamMap.packStreamOffsets[firstPackStreamIndex]; |
| currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file); |
| } |
| final InputStream fileStream = new BoundedInputStream( |
| currentFolderInputStream, file.getSize()); |
| if (file.getHasCrc()) { |
| currentEntryInputStream = new CRC32VerifyingInputStream( |
| fileStream, file.getSize(), file.getCrcValue()); |
| } else { |
| currentEntryInputStream = fileStream; |
| } |
| |
| } |
| |
| private void drainPreviousEntry() throws IOException { |
| if (currentEntryInputStream != null) { |
| // return value ignored as IOUtils.skip ensures the stream is drained completely |
| IOUtils.skip(currentEntryInputStream, Long.MAX_VALUE); |
| currentEntryInputStream.close(); |
| currentEntryInputStream = null; |
| } |
| } |
| |
| private InputStream buildDecoderStack(final Folder folder, final long folderOffset, |
| final int firstPackStreamIndex, SevenZArchiveEntry entry) throws IOException { |
| file.seek(folderOffset); |
| InputStream inputStreamStack = new BoundedRandomAccessFileInputStream(file, |
| archive.packSizes[firstPackStreamIndex]); |
| LinkedList<SevenZMethodConfiguration> methods = new LinkedList<SevenZMethodConfiguration>(); |
| for (final Coder coder : folder.getOrderedCoders()) { |
| if (coder.numInStreams != 1 || coder.numOutStreams != 1) { |
| throw new IOException("Multi input/output stream coders are not yet supported"); |
| } |
| SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId); |
| inputStreamStack = Coders.addDecoder(inputStreamStack, coder, password); |
| methods.addFirst(new SevenZMethodConfiguration(method, |
| Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack))); |
| } |
| entry.setContentMethods(methods); |
| if (folder.hasCrc) { |
| return new CRC32VerifyingInputStream(inputStreamStack, |
| folder.getUnpackSize(), folder.crc); |
| } else { |
| return inputStreamStack; |
| } |
| } |
| |
| /** |
| * Reads a byte of data. |
| * |
| * @return the byte read, or -1 if end of input is reached |
| * @throws IOException |
| * if an I/O error has occurred |
| */ |
| public int read() throws IOException { |
| if (currentEntryInputStream == null) { |
| throw new IllegalStateException("No current 7z entry"); |
| } |
| return currentEntryInputStream.read(); |
| } |
| |
| /** |
| * Reads data into an array of bytes. |
| * |
| * @param b the array to write data to |
| * @return the number of bytes read, or -1 if end of input is reached |
| * @throws IOException |
| * if an I/O error has occurred |
| */ |
| public int read(byte[] b) throws IOException { |
| return read(b, 0, b.length); |
| } |
| |
| /** |
| * Reads data into an array of bytes. |
| * |
| * @param b the array to write data to |
| * @param off offset into the buffer to start filling at |
| * @param len of bytes to read |
| * @return the number of bytes read, or -1 if end of input is reached |
| * @throws IOException |
| * if an I/O error has occurred |
| */ |
| public int read(byte[] b, int off, int len) throws IOException { |
| if (currentEntryInputStream == null) { |
| throw new IllegalStateException("No current 7z entry"); |
| } |
| return currentEntryInputStream.read(b, off, len); |
| } |
| |
| private static long readUint64(final DataInput in) throws IOException { |
| // long rather than int as it might get shifted beyond the range of an int |
| long firstByte = in.readUnsignedByte(); |
| int mask = 0x80; |
| long value = 0; |
| for (int i = 0; i < 8; i++) { |
| if ((firstByte & mask) == 0) { |
| return value | ((firstByte & (mask - 1)) << (8 * i)); |
| } |
| long nextByte = in.readUnsignedByte(); |
| value |= nextByte << (8 * i); |
| mask >>>= 1; |
| } |
| return value; |
| } |
| |
| /** |
| * Checks if the signature matches what is expected for a 7z file. |
| * |
| * @param signature |
| * the bytes to check |
| * @param length |
| * the number of bytes to check |
| * @return true, if this is the signature of a 7z archive. |
| * @since 1.8 |
| */ |
| public static boolean matches(byte[] signature, int length) { |
| if (length < sevenZSignature.length) { |
| return false; |
| } |
| |
| for (int i = 0; i < sevenZSignature.length; i++) { |
| if (signature[i] != sevenZSignature[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |