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}