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