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