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