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.DataInputStream; 024import java.io.File; 025import java.io.FilterInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.nio.ByteBuffer; 029import java.nio.ByteOrder; 030import java.nio.CharBuffer; 031import java.nio.channels.SeekableByteChannel; 032import java.nio.charset.StandardCharsets; 033import java.nio.charset.CharsetEncoder; 034import java.nio.file.Files; 035import java.nio.file.StandardOpenOption; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.BitSet; 039import java.util.EnumSet; 040import java.util.LinkedList; 041import java.util.zip.CRC32; 042 043import org.apache.commons.compress.utils.BoundedInputStream; 044import org.apache.commons.compress.utils.CRC32VerifyingInputStream; 045import org.apache.commons.compress.utils.CharsetNames; 046import org.apache.commons.compress.utils.IOUtils; 047import org.apache.commons.compress.utils.InputStreamStatistics; 048 049/** 050 * Reads a 7z file, using SeekableByteChannel under 051 * the covers. 052 * <p> 053 * The 7z file format is a flexible container 054 * that can contain many compression and 055 * encryption types, but at the moment only 056 * only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256 057 * are supported. 058 * <p> 059 * The format is very Windows/Intel specific, 060 * so it uses little-endian byte order, 061 * doesn't store user/group or permission bits, 062 * and represents times using NTFS timestamps 063 * (100 nanosecond units since 1 January 1601). 064 * Hence the official tools recommend against 065 * using it for backup purposes on *nix, and 066 * recommend .tar.7z or .tar.lzma or .tar.xz 067 * instead. 068 * <p> 069 * Both the header and file contents may be 070 * compressed and/or encrypted. With both 071 * encrypted, neither file names nor file 072 * contents can be read, but the use of 073 * encryption isn't plausibly deniable. 074 * 075 * <p>Multi volume archives can be read by concatenating the parts in 076 * correct order - either manually or by using {link 077 * org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel} 078 * for example.</p> 079 * 080 * @NotThreadSafe 081 * @since 1.6 082 */ 083public class SevenZFile implements Closeable { 084 static final int SIGNATURE_HEADER_SIZE = 32; 085 086 private static final String DEFAULT_FILE_NAME = "unknown archive"; 087 088 private final String fileName; 089 private SeekableByteChannel channel; 090 private final Archive archive; 091 private int currentEntryIndex = -1; 092 private int currentFolderIndex = -1; 093 private InputStream currentFolderInputStream = null; 094 private byte[] password; 095 private final SevenZFileOptions options; 096 097 private long compressedBytesReadFromCurrentEntry; 098 private long uncompressedBytesReadFromCurrentEntry; 099 100 private final ArrayList<InputStream> deferredBlockStreams = new ArrayList<>(); 101 102 // shared with SevenZOutputFile and tests, neither mutates it 103 static final byte[] sevenZSignature = { //NOSONAR 104 (byte)'7', (byte)'z', (byte)0xBC, (byte)0xAF, (byte)0x27, (byte)0x1C 105 }; 106 107 /** 108 * Reads a file as 7z archive 109 * 110 * @param fileName the file to read 111 * @param password optional password if the archive is encrypted 112 * @throws IOException if reading the archive fails 113 * @since 1.17 114 */ 115 public SevenZFile(final File fileName, final char[] password) throws IOException { 116 this(fileName, password, SevenZFileOptions.DEFAULT); 117 } 118 119 /** 120 * Reads a file as 7z archive with additional options. 121 * 122 * @param fileName the file to read 123 * @param password optional password if the archive is encrypted 124 * @param options the options to apply 125 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 126 * @since 1.19 127 */ 128 public SevenZFile(final File fileName, final char[] password, SevenZFileOptions options) throws IOException { 129 this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), // NOSONAR 130 fileName.getAbsolutePath(), utf16Decode(password), true, options); 131 } 132 133 /** 134 * Reads a file as 7z archive 135 * 136 * @param fileName the file to read 137 * @param password optional password if the archive is encrypted - 138 * the byte array is supposed to be the UTF16-LE encoded 139 * representation of the password. 140 * @throws IOException if reading the archive fails 141 * @deprecated use the char[]-arg version for the password instead 142 */ 143 @Deprecated 144 public SevenZFile(final File fileName, final byte[] password) throws IOException { 145 this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), 146 fileName.getAbsolutePath(), password, true, SevenZFileOptions.DEFAULT); 147 } 148 149 /** 150 * Reads a SeekableByteChannel as 7z archive 151 * 152 * <p>{@link 153 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 154 * allows you to read from an in-memory archive.</p> 155 * 156 * @param channel the channel to read 157 * @throws IOException if reading the archive fails 158 * @since 1.13 159 */ 160 public SevenZFile(final SeekableByteChannel channel) throws IOException { 161 this(channel, SevenZFileOptions.DEFAULT); 162 } 163 164 /** 165 * Reads a SeekableByteChannel as 7z archive with addtional options. 166 * 167 * <p>{@link 168 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 169 * allows you to read from an in-memory archive.</p> 170 * 171 * @param channel the channel to read 172 * @param options the options to apply 173 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 174 * @since 1.19 175 */ 176 public SevenZFile(final SeekableByteChannel channel, SevenZFileOptions options) throws IOException { 177 this(channel, DEFAULT_FILE_NAME, (char[]) null, options); 178 } 179 180 /** 181 * Reads a SeekableByteChannel as 7z archive 182 * 183 * <p>{@link 184 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 185 * allows you to read from an in-memory archive.</p> 186 * 187 * @param channel the channel to read 188 * @param password optional password if the archive is encrypted 189 * @throws IOException if reading the archive fails 190 * @since 1.17 191 */ 192 public SevenZFile(final SeekableByteChannel channel, 193 final char[] password) throws IOException { 194 this(channel, password, SevenZFileOptions.DEFAULT); 195 } 196 197 /** 198 * Reads a SeekableByteChannel as 7z archive with additional options. 199 * 200 * <p>{@link 201 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 202 * allows you to read from an in-memory archive.</p> 203 * 204 * @param channel the channel to read 205 * @param password optional password if the archive is encrypted 206 * @param options the options to apply 207 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 208 * @since 1.19 209 */ 210 public SevenZFile(final SeekableByteChannel channel, final char[] password, final SevenZFileOptions options) 211 throws IOException { 212 this(channel, DEFAULT_FILE_NAME, password, options); 213 } 214 215 /** 216 * Reads a SeekableByteChannel as 7z archive 217 * 218 * <p>{@link 219 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 220 * allows you to read from an in-memory archive.</p> 221 * 222 * @param channel the channel to read 223 * @param fileName name of the archive - only used for error reporting 224 * @param password optional password if the archive is encrypted 225 * @throws IOException if reading the archive fails 226 * @since 1.17 227 */ 228 public SevenZFile(final SeekableByteChannel channel, String fileName, 229 final char[] password) throws IOException { 230 this(channel, fileName, password, SevenZFileOptions.DEFAULT); 231 } 232 233 /** 234 * Reads a SeekableByteChannel as 7z archive with addtional options. 235 * 236 * <p>{@link 237 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 238 * allows you to read from an in-memory archive.</p> 239 * 240 * @param channel the channel to read 241 * @param fileName name of the archive - only used for error reporting 242 * @param password optional password if the archive is encrypted 243 * @param options the options to apply 244 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 245 * @since 1.19 246 */ 247 public SevenZFile(final SeekableByteChannel channel, String fileName, final char[] password, 248 final SevenZFileOptions options) throws IOException { 249 this(channel, fileName, utf16Decode(password), false, options); 250 } 251 252 /** 253 * Reads a SeekableByteChannel as 7z archive 254 * 255 * <p>{@link 256 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 257 * allows you to read from an in-memory archive.</p> 258 * 259 * @param channel the channel to read 260 * @param fileName name of the archive - only used for error reporting 261 * @throws IOException if reading the archive fails 262 * @since 1.17 263 */ 264 public SevenZFile(final SeekableByteChannel channel, String fileName) 265 throws IOException { 266 this(channel, fileName, SevenZFileOptions.DEFAULT); 267 } 268 269 /** 270 * Reads a SeekableByteChannel as 7z archive with additional options. 271 * 272 * <p>{@link 273 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 274 * allows you to read from an in-memory archive.</p> 275 * 276 * @param channel the channel to read 277 * @param fileName name of the archive - only used for error reporting 278 * @param options the options to apply 279 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 280 * @since 1.19 281 */ 282 public SevenZFile(final SeekableByteChannel channel, String fileName, final SevenZFileOptions options) 283 throws IOException { 284 this(channel, fileName, null, false, options); 285 } 286 287 /** 288 * Reads a SeekableByteChannel as 7z archive 289 * 290 * <p>{@link 291 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 292 * allows you to read from an in-memory archive.</p> 293 * 294 * @param channel the channel to read 295 * @param password optional password if the archive is encrypted - 296 * the byte array is supposed to be the UTF16-LE encoded 297 * representation of the password. 298 * @throws IOException if reading the archive fails 299 * @since 1.13 300 * @deprecated use the char[]-arg version for the password instead 301 */ 302 @Deprecated 303 public SevenZFile(final SeekableByteChannel channel, 304 final byte[] password) throws IOException { 305 this(channel, DEFAULT_FILE_NAME, password); 306 } 307 308 /** 309 * Reads a SeekableByteChannel as 7z archive 310 * 311 * <p>{@link 312 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 313 * allows you to read from an in-memory archive.</p> 314 * 315 * @param channel the channel to read 316 * @param fileName name of the archive - only used for error reporting 317 * @param password optional password if the archive is encrypted - 318 * the byte array is supposed to be the UTF16-LE encoded 319 * representation of the password. 320 * @throws IOException if reading the archive fails 321 * @since 1.13 322 * @deprecated use the char[]-arg version for the password instead 323 */ 324 @Deprecated 325 public SevenZFile(final SeekableByteChannel channel, String fileName, 326 final byte[] password) throws IOException { 327 this(channel, fileName, password, false, SevenZFileOptions.DEFAULT); 328 } 329 330 private SevenZFile(final SeekableByteChannel channel, String filename, 331 final byte[] password, boolean closeOnError, SevenZFileOptions options) throws IOException { 332 boolean succeeded = false; 333 this.channel = channel; 334 this.fileName = filename; 335 this.options = options; 336 try { 337 archive = readHeaders(password); 338 if (password != null) { 339 this.password = Arrays.copyOf(password, password.length); 340 } else { 341 this.password = null; 342 } 343 succeeded = true; 344 } finally { 345 if (!succeeded && closeOnError) { 346 this.channel.close(); 347 } 348 } 349 } 350 351 /** 352 * Reads a file as unencrypted 7z archive 353 * 354 * @param fileName the file to read 355 * @throws IOException if reading the archive fails 356 */ 357 public SevenZFile(final File fileName) throws IOException { 358 this(fileName, SevenZFileOptions.DEFAULT); 359 } 360 361 /** 362 * Reads a file as unencrypted 7z archive 363 * 364 * @param fileName the file to read 365 * @param options the options to apply 366 * @throws IOException if reading the archive fails or the memory limit (if set) is too small 367 * @since 1.19 368 */ 369 public SevenZFile(final File fileName, final SevenZFileOptions options) throws IOException { 370 this(fileName, (char[]) null, options); 371 } 372 373 /** 374 * Closes the archive. 375 * @throws IOException if closing the file fails 376 */ 377 @Override 378 public void close() throws IOException { 379 if (channel != null) { 380 try { 381 channel.close(); 382 } finally { 383 channel = null; 384 if (password != null) { 385 Arrays.fill(password, (byte) 0); 386 } 387 password = null; 388 } 389 } 390 } 391 392 /** 393 * Returns the next Archive Entry in this archive. 394 * 395 * @return the next entry, 396 * or {@code null} if there are no more entries 397 * @throws IOException if the next entry could not be read 398 */ 399 public SevenZArchiveEntry getNextEntry() throws IOException { 400 if (currentEntryIndex >= archive.files.length - 1) { 401 return null; 402 } 403 ++currentEntryIndex; 404 final SevenZArchiveEntry entry = archive.files[currentEntryIndex]; 405 if (entry.getName() == null && options.getUseDefaultNameForUnnamedEntries()) { 406 entry.setName(getDefaultName()); 407 } 408 buildDecodingStream(currentEntryIndex); 409 uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0; 410 return entry; 411 } 412 413 /** 414 * Returns meta-data of all archive entries. 415 * 416 * <p>This method only provides meta-data, the entries can not be 417 * used to read the contents, you still need to process all 418 * entries in order using {@link #getNextEntry} for that.</p> 419 * 420 * <p>The content methods are only available for entries that have 421 * already been reached via {@link #getNextEntry}.</p> 422 * 423 * @return meta-data of all archive entries. 424 * @since 1.11 425 */ 426 public Iterable<SevenZArchiveEntry> getEntries() { 427 return Arrays.asList(archive.files); 428 } 429 430 private Archive readHeaders(final byte[] password) throws IOException { 431 ByteBuffer buf = ByteBuffer.allocate(12 /* signature + 2 bytes version + 4 bytes CRC */) 432 .order(ByteOrder.LITTLE_ENDIAN); 433 readFully(buf); 434 final byte[] signature = new byte[6]; 435 buf.get(signature); 436 if (!Arrays.equals(signature, sevenZSignature)) { 437 throw new IOException("Bad 7z signature"); 438 } 439 // 7zFormat.txt has it wrong - it's first major then minor 440 final byte archiveVersionMajor = buf.get(); 441 final byte archiveVersionMinor = buf.get(); 442 if (archiveVersionMajor != 0) { 443 throw new IOException(String.format("Unsupported 7z version (%d,%d)", 444 archiveVersionMajor, archiveVersionMinor)); 445 } 446 447 boolean headerLooksValid = false; // See https://www.7-zip.org/recover.html - "There is no correct End Header at the end of archive" 448 final long startHeaderCrc = 0xffffFFFFL & buf.getInt(); 449 if (startHeaderCrc == 0) { 450 // This is an indication of a corrupt header - peek the next 20 bytes 451 long currentPosition = channel.position(); 452 ByteBuffer peekBuf = ByteBuffer.allocate(20); 453 readFully(peekBuf); 454 channel.position(currentPosition); 455 // Header invalid if all data is 0 456 while (peekBuf.hasRemaining()) { 457 if (peekBuf.get()!=0) { 458 headerLooksValid = true; 459 break; 460 } 461 } 462 } else { 463 headerLooksValid = true; 464 } 465 466 if (headerLooksValid) { 467 final StartHeader startHeader = readStartHeader(startHeaderCrc); 468 return initializeArchive(startHeader, password, true); 469 } else { 470 // No valid header found - probably first file of multipart archive was removed too early. Scan for end header. 471 return tryToLocateEndHeader(password); 472 } 473 } 474 475 private Archive tryToLocateEndHeader(final byte[] password) throws IOException { 476 ByteBuffer nidBuf = ByteBuffer.allocate(1); 477 final long searchLimit = 1024l * 1024 * 1; 478 // Main header, plus bytes that readStartHeader would read 479 final long previousDataSize = channel.position() + 20; 480 final long minPos; 481 // Determine minimal position - can't start before current position 482 if (channel.position() + searchLimit > channel.size()) { 483 minPos = channel.position(); 484 } else { 485 minPos = channel.size() - searchLimit; 486 } 487 long pos = channel.size() - 1; 488 // Loop: Try from end of archive 489 while (pos > minPos) { 490 pos--; 491 channel.position(pos); 492 nidBuf.rewind(); 493 channel.read(nidBuf); 494 int nid = nidBuf.array()[0]; 495 // First indicator: Byte equals one of these header identifiers 496 if (nid == NID.kEncodedHeader || nid == NID.kHeader) { 497 try { 498 // Try to initialize Archive structure from here 499 final StartHeader startHeader = new StartHeader(); 500 startHeader.nextHeaderOffset = pos - previousDataSize; 501 startHeader.nextHeaderSize = channel.size() - pos; 502 Archive result = initializeArchive(startHeader, password, false); 503 // Sanity check: There must be some data... 504 if (result.packSizes != null && result.files.length > 0) { 505 return result; 506 } 507 } catch (Exception ignore) { 508 // Wrong guess... 509 } 510 } 511 } 512 throw new IOException("Start header corrupt and unable to guess end header"); 513 } 514 515 private Archive initializeArchive(StartHeader startHeader, final byte[] password, boolean verifyCrc) throws IOException { 516 assertFitsIntoInt("nextHeaderSize", startHeader.nextHeaderSize); 517 final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize; 518 channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset); 519 ByteBuffer buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN); 520 readFully(buf); 521 if (verifyCrc) { 522 final CRC32 crc = new CRC32(); 523 crc.update(buf.array()); 524 if (startHeader.nextHeaderCrc != crc.getValue()) { 525 throw new IOException("NextHeader CRC mismatch"); 526 } 527 } 528 529 Archive archive = new Archive(); 530 int nid = getUnsignedByte(buf); 531 if (nid == NID.kEncodedHeader) { 532 buf = readEncodedHeader(buf, archive, password); 533 // Archive gets rebuilt with the new header 534 archive = new Archive(); 535 nid = getUnsignedByte(buf); 536 } 537 if (nid == NID.kHeader) { 538 readHeader(buf, archive); 539 } else { 540 throw new IOException("Broken or unsupported archive: no Header"); 541 } 542 return archive; 543 } 544 545 private StartHeader readStartHeader(final long startHeaderCrc) throws IOException { 546 final StartHeader startHeader = new StartHeader(); 547 // using Stream rather than ByteBuffer for the benefit of the 548 // built-in CRC check 549 try (DataInputStream dataInputStream = new DataInputStream(new CRC32VerifyingInputStream( 550 new BoundedSeekableByteChannelInputStream(channel, 20), 20, startHeaderCrc))) { 551 startHeader.nextHeaderOffset = Long.reverseBytes(dataInputStream.readLong()); 552 startHeader.nextHeaderSize = Long.reverseBytes(dataInputStream.readLong()); 553 startHeader.nextHeaderCrc = 0xffffFFFFL & Integer.reverseBytes(dataInputStream.readInt()); 554 return startHeader; 555 } 556 } 557 558 private void readHeader(final ByteBuffer header, final Archive archive) throws IOException { 559 int nid = getUnsignedByte(header); 560 561 if (nid == NID.kArchiveProperties) { 562 readArchiveProperties(header); 563 nid = getUnsignedByte(header); 564 } 565 566 if (nid == NID.kAdditionalStreamsInfo) { 567 throw new IOException("Additional streams unsupported"); 568 //nid = header.readUnsignedByte(); 569 } 570 571 if (nid == NID.kMainStreamsInfo) { 572 readStreamsInfo(header, archive); 573 nid = getUnsignedByte(header); 574 } 575 576 if (nid == NID.kFilesInfo) { 577 readFilesInfo(header, archive); 578 nid = getUnsignedByte(header); 579 } 580 581 if (nid != NID.kEnd) { 582 throw new IOException("Badly terminated header, found " + nid); 583 } 584 } 585 586 private void readArchiveProperties(final ByteBuffer input) throws IOException { 587 // FIXME: the reference implementation just throws them away? 588 int nid = getUnsignedByte(input); 589 while (nid != NID.kEnd) { 590 final long propertySize = readUint64(input); 591 assertFitsIntoInt("propertySize", propertySize); 592 final byte[] property = new byte[(int)propertySize]; 593 input.get(property); 594 nid = getUnsignedByte(input); 595 } 596 } 597 598 private ByteBuffer readEncodedHeader(final ByteBuffer header, final Archive archive, 599 final byte[] password) throws IOException { 600 readStreamsInfo(header, archive); 601 602 // FIXME: merge with buildDecodingStream()/buildDecoderStack() at some stage? 603 final Folder folder = archive.folders[0]; 604 final int firstPackStreamIndex = 0; 605 final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + 606 0; 607 608 channel.position(folderOffset); 609 InputStream inputStreamStack = new BoundedSeekableByteChannelInputStream(channel, 610 archive.packSizes[firstPackStreamIndex]); 611 for (final Coder coder : folder.getOrderedCoders()) { 612 if (coder.numInStreams != 1 || coder.numOutStreams != 1) { 613 throw new IOException("Multi input/output stream coders are not yet supported"); 614 } 615 inputStreamStack = Coders.addDecoder(fileName, inputStreamStack, //NOSONAR 616 folder.getUnpackSizeForCoder(coder), coder, password, options.getMaxMemoryLimitInKb()); 617 } 618 if (folder.hasCrc) { 619 inputStreamStack = new CRC32VerifyingInputStream(inputStreamStack, 620 folder.getUnpackSize(), folder.crc); 621 } 622 assertFitsIntoInt("unpackSize", folder.getUnpackSize()); 623 final byte[] nextHeader = new byte[(int)folder.getUnpackSize()]; 624 try (DataInputStream nextHeaderInputStream = new DataInputStream(inputStreamStack)) { 625 nextHeaderInputStream.readFully(nextHeader); 626 } 627 return ByteBuffer.wrap(nextHeader).order(ByteOrder.LITTLE_ENDIAN); 628 } 629 630 private void readStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException { 631 int nid = getUnsignedByte(header); 632 633 if (nid == NID.kPackInfo) { 634 readPackInfo(header, archive); 635 nid = getUnsignedByte(header); 636 } 637 638 if (nid == NID.kUnpackInfo) { 639 readUnpackInfo(header, archive); 640 nid = getUnsignedByte(header); 641 } else { 642 // archive without unpack/coders info 643 archive.folders = new Folder[0]; 644 } 645 646 if (nid == NID.kSubStreamsInfo) { 647 readSubStreamsInfo(header, archive); 648 nid = getUnsignedByte(header); 649 } 650 651 if (nid != NID.kEnd) { 652 throw new IOException("Badly terminated StreamsInfo"); 653 } 654 } 655 656 private void readPackInfo(final ByteBuffer header, final Archive archive) throws IOException { 657 archive.packPos = readUint64(header); 658 final long numPackStreams = readUint64(header); 659 assertFitsIntoInt("numPackStreams", numPackStreams); 660 final int numPackStreamsInt = (int) numPackStreams; 661 int nid = getUnsignedByte(header); 662 if (nid == NID.kSize) { 663 archive.packSizes = new long[numPackStreamsInt]; 664 for (int i = 0; i < archive.packSizes.length; i++) { 665 archive.packSizes[i] = readUint64(header); 666 } 667 nid = getUnsignedByte(header); 668 } 669 670 if (nid == NID.kCRC) { 671 archive.packCrcsDefined = readAllOrBits(header, numPackStreamsInt); 672 archive.packCrcs = new long[numPackStreamsInt]; 673 for (int i = 0; i < numPackStreamsInt; i++) { 674 if (archive.packCrcsDefined.get(i)) { 675 archive.packCrcs[i] = 0xffffFFFFL & header.getInt(); 676 } 677 } 678 679 nid = getUnsignedByte(header); 680 } 681 682 if (nid != NID.kEnd) { 683 throw new IOException("Badly terminated PackInfo (" + nid + ")"); 684 } 685 } 686 687 private void readUnpackInfo(final ByteBuffer header, final Archive archive) throws IOException { 688 int nid = getUnsignedByte(header); 689 if (nid != NID.kFolder) { 690 throw new IOException("Expected kFolder, got " + nid); 691 } 692 final long numFolders = readUint64(header); 693 assertFitsIntoInt("numFolders", numFolders); 694 final int numFoldersInt = (int) numFolders; 695 final Folder[] folders = new Folder[numFoldersInt]; 696 archive.folders = folders; 697 final int external = getUnsignedByte(header); 698 if (external != 0) { 699 throw new IOException("External unsupported"); 700 } 701 for (int i = 0; i < numFoldersInt; i++) { 702 folders[i] = readFolder(header); 703 } 704 705 nid = getUnsignedByte(header); 706 if (nid != NID.kCodersUnpackSize) { 707 throw new IOException("Expected kCodersUnpackSize, got " + nid); 708 } 709 for (final Folder folder : folders) { 710 assertFitsIntoInt("totalOutputStreams", folder.totalOutputStreams); 711 folder.unpackSizes = new long[(int)folder.totalOutputStreams]; 712 for (int i = 0; i < folder.totalOutputStreams; i++) { 713 folder.unpackSizes[i] = readUint64(header); 714 } 715 } 716 717 nid = getUnsignedByte(header); 718 if (nid == NID.kCRC) { 719 final BitSet crcsDefined = readAllOrBits(header, numFoldersInt); 720 for (int i = 0; i < numFoldersInt; i++) { 721 if (crcsDefined.get(i)) { 722 folders[i].hasCrc = true; 723 folders[i].crc = 0xffffFFFFL & header.getInt(); 724 } else { 725 folders[i].hasCrc = false; 726 } 727 } 728 729 nid = getUnsignedByte(header); 730 } 731 732 if (nid != NID.kEnd) { 733 throw new IOException("Badly terminated UnpackInfo"); 734 } 735 } 736 737 private void readSubStreamsInfo(final ByteBuffer header, final Archive archive) throws IOException { 738 for (final Folder folder : archive.folders) { 739 folder.numUnpackSubStreams = 1; 740 } 741 int totalUnpackStreams = archive.folders.length; 742 743 int nid = getUnsignedByte(header); 744 if (nid == NID.kNumUnpackStream) { 745 totalUnpackStreams = 0; 746 for (final Folder folder : archive.folders) { 747 final long numStreams = readUint64(header); 748 assertFitsIntoInt("numStreams", numStreams); 749 folder.numUnpackSubStreams = (int)numStreams; 750 totalUnpackStreams += numStreams; 751 } 752 nid = getUnsignedByte(header); 753 } 754 755 final SubStreamsInfo subStreamsInfo = new SubStreamsInfo(); 756 subStreamsInfo.unpackSizes = new long[totalUnpackStreams]; 757 subStreamsInfo.hasCrc = new BitSet(totalUnpackStreams); 758 subStreamsInfo.crcs = new long[totalUnpackStreams]; 759 760 int nextUnpackStream = 0; 761 for (final Folder folder : archive.folders) { 762 if (folder.numUnpackSubStreams == 0) { 763 continue; 764 } 765 long sum = 0; 766 if (nid == NID.kSize) { 767 for (int i = 0; i < folder.numUnpackSubStreams - 1; i++) { 768 final long size = readUint64(header); 769 subStreamsInfo.unpackSizes[nextUnpackStream++] = size; 770 sum += size; 771 } 772 } 773 subStreamsInfo.unpackSizes[nextUnpackStream++] = folder.getUnpackSize() - sum; 774 } 775 if (nid == NID.kSize) { 776 nid = getUnsignedByte(header); 777 } 778 779 int numDigests = 0; 780 for (final Folder folder : archive.folders) { 781 if (folder.numUnpackSubStreams != 1 || !folder.hasCrc) { 782 numDigests += folder.numUnpackSubStreams; 783 } 784 } 785 786 if (nid == NID.kCRC) { 787 final BitSet hasMissingCrc = readAllOrBits(header, numDigests); 788 final long[] missingCrcs = new long[numDigests]; 789 for (int i = 0; i < numDigests; i++) { 790 if (hasMissingCrc.get(i)) { 791 missingCrcs[i] = 0xffffFFFFL & header.getInt(); 792 } 793 } 794 int nextCrc = 0; 795 int nextMissingCrc = 0; 796 for (final Folder folder: archive.folders) { 797 if (folder.numUnpackSubStreams == 1 && folder.hasCrc) { 798 subStreamsInfo.hasCrc.set(nextCrc, true); 799 subStreamsInfo.crcs[nextCrc] = folder.crc; 800 ++nextCrc; 801 } else { 802 for (int i = 0; i < folder.numUnpackSubStreams; i++) { 803 subStreamsInfo.hasCrc.set(nextCrc, hasMissingCrc.get(nextMissingCrc)); 804 subStreamsInfo.crcs[nextCrc] = missingCrcs[nextMissingCrc]; 805 ++nextCrc; 806 ++nextMissingCrc; 807 } 808 } 809 } 810 811 nid = getUnsignedByte(header); 812 } 813 814 if (nid != NID.kEnd) { 815 throw new IOException("Badly terminated SubStreamsInfo"); 816 } 817 818 archive.subStreamsInfo = subStreamsInfo; 819 } 820 821 private Folder readFolder(final ByteBuffer header) throws IOException { 822 final Folder folder = new Folder(); 823 824 final long numCoders = readUint64(header); 825 assertFitsIntoInt("numCoders", numCoders); 826 final Coder[] coders = new Coder[(int)numCoders]; 827 long totalInStreams = 0; 828 long totalOutStreams = 0; 829 for (int i = 0; i < coders.length; i++) { 830 coders[i] = new Coder(); 831 final int bits = getUnsignedByte(header); 832 final int idSize = bits & 0xf; 833 final boolean isSimple = (bits & 0x10) == 0; 834 final boolean hasAttributes = (bits & 0x20) != 0; 835 final boolean moreAlternativeMethods = (bits & 0x80) != 0; 836 837 coders[i].decompressionMethodId = new byte[idSize]; 838 header.get(coders[i].decompressionMethodId); 839 if (isSimple) { 840 coders[i].numInStreams = 1; 841 coders[i].numOutStreams = 1; 842 } else { 843 coders[i].numInStreams = readUint64(header); 844 coders[i].numOutStreams = readUint64(header); 845 } 846 totalInStreams += coders[i].numInStreams; 847 totalOutStreams += coders[i].numOutStreams; 848 if (hasAttributes) { 849 final long propertiesSize = readUint64(header); 850 assertFitsIntoInt("propertiesSize", propertiesSize); 851 coders[i].properties = new byte[(int)propertiesSize]; 852 header.get(coders[i].properties); 853 } 854 // would need to keep looping as above: 855 while (moreAlternativeMethods) { 856 throw new IOException("Alternative methods are unsupported, please report. " + // NOSONAR 857 "The reference implementation doesn't support them either."); 858 } 859 } 860 folder.coders = coders; 861 assertFitsIntoInt("totalInStreams", totalInStreams); 862 folder.totalInputStreams = totalInStreams; 863 assertFitsIntoInt("totalOutStreams", totalOutStreams); 864 folder.totalOutputStreams = totalOutStreams; 865 866 if (totalOutStreams == 0) { 867 throw new IOException("Total output streams can't be 0"); 868 } 869 final long numBindPairs = totalOutStreams - 1; 870 assertFitsIntoInt("numBindPairs", numBindPairs); 871 final BindPair[] bindPairs = new BindPair[(int)numBindPairs]; 872 for (int i = 0; i < bindPairs.length; i++) { 873 bindPairs[i] = new BindPair(); 874 bindPairs[i].inIndex = readUint64(header); 875 bindPairs[i].outIndex = readUint64(header); 876 } 877 folder.bindPairs = bindPairs; 878 879 if (totalInStreams < numBindPairs) { 880 throw new IOException("Total input streams can't be less than the number of bind pairs"); 881 } 882 final long numPackedStreams = totalInStreams - numBindPairs; 883 assertFitsIntoInt("numPackedStreams", numPackedStreams); 884 final long packedStreams[] = new long[(int)numPackedStreams]; 885 if (numPackedStreams == 1) { 886 int i; 887 for (i = 0; i < (int)totalInStreams; i++) { 888 if (folder.findBindPairForInStream(i) < 0) { 889 break; 890 } 891 } 892 if (i == (int)totalInStreams) { 893 throw new IOException("Couldn't find stream's bind pair index"); 894 } 895 packedStreams[0] = i; 896 } else { 897 for (int i = 0; i < (int)numPackedStreams; i++) { 898 packedStreams[i] = readUint64(header); 899 } 900 } 901 folder.packedStreams = packedStreams; 902 903 return folder; 904 } 905 906 private BitSet readAllOrBits(final ByteBuffer header, final int size) throws IOException { 907 final int areAllDefined = getUnsignedByte(header); 908 final BitSet bits; 909 if (areAllDefined != 0) { 910 bits = new BitSet(size); 911 for (int i = 0; i < size; i++) { 912 bits.set(i, true); 913 } 914 } else { 915 bits = readBits(header, size); 916 } 917 return bits; 918 } 919 920 private BitSet readBits(final ByteBuffer header, final int size) throws IOException { 921 final BitSet bits = new BitSet(size); 922 int mask = 0; 923 int cache = 0; 924 for (int i = 0; i < size; i++) { 925 if (mask == 0) { 926 mask = 0x80; 927 cache = getUnsignedByte(header); 928 } 929 bits.set(i, (cache & mask) != 0); 930 mask >>>= 1; 931 } 932 return bits; 933 } 934 935 private void readFilesInfo(final ByteBuffer header, final Archive archive) throws IOException { 936 final long numFiles = readUint64(header); 937 assertFitsIntoInt("numFiles", numFiles); 938 final SevenZArchiveEntry[] files = new SevenZArchiveEntry[(int)numFiles]; 939 for (int i = 0; i < files.length; i++) { 940 files[i] = new SevenZArchiveEntry(); 941 } 942 BitSet isEmptyStream = null; 943 BitSet isEmptyFile = null; 944 BitSet isAnti = null; 945 while (true) { 946 final int propertyType = getUnsignedByte(header); 947 if (propertyType == 0) { 948 break; 949 } 950 final long size = readUint64(header); 951 switch (propertyType) { 952 case NID.kEmptyStream: { 953 isEmptyStream = readBits(header, files.length); 954 break; 955 } 956 case NID.kEmptyFile: { 957 if (isEmptyStream == null) { // protect against NPE 958 throw new IOException("Header format error: kEmptyStream must appear before kEmptyFile"); 959 } 960 isEmptyFile = readBits(header, isEmptyStream.cardinality()); 961 break; 962 } 963 case NID.kAnti: { 964 if (isEmptyStream == null) { // protect against NPE 965 throw new IOException("Header format error: kEmptyStream must appear before kAnti"); 966 } 967 isAnti = readBits(header, isEmptyStream.cardinality()); 968 break; 969 } 970 case NID.kName: { 971 final int external = getUnsignedByte(header); 972 if (external != 0) { 973 throw new IOException("Not implemented"); 974 } 975 if (((size - 1) & 1) != 0) { 976 throw new IOException("File names length invalid"); 977 } 978 assertFitsIntoInt("file names length", size - 1); 979 final byte[] names = new byte[(int)(size - 1)]; 980 header.get(names); 981 int nextFile = 0; 982 int nextName = 0; 983 for (int i = 0; i < names.length; i += 2) { 984 if (names[i] == 0 && names[i+1] == 0) { 985 files[nextFile++].setName(new String(names, nextName, i-nextName, CharsetNames.UTF_16LE)); 986 nextName = i + 2; 987 } 988 } 989 if (nextName != names.length || nextFile != files.length) { 990 throw new IOException("Error parsing file names"); 991 } 992 break; 993 } 994 case NID.kCTime: { 995 final BitSet timesDefined = readAllOrBits(header, files.length); 996 final int external = getUnsignedByte(header); 997 if (external != 0) { 998 throw new IOException("Unimplemented"); 999 } 1000 for (int i = 0; i < files.length; i++) { 1001 files[i].setHasCreationDate(timesDefined.get(i)); 1002 if (files[i].getHasCreationDate()) { 1003 files[i].setCreationDate(header.getLong()); 1004 } 1005 } 1006 break; 1007 } 1008 case NID.kATime: { 1009 final BitSet timesDefined = readAllOrBits(header, files.length); 1010 final int external = getUnsignedByte(header); 1011 if (external != 0) { 1012 throw new IOException("Unimplemented"); 1013 } 1014 for (int i = 0; i < files.length; i++) { 1015 files[i].setHasAccessDate(timesDefined.get(i)); 1016 if (files[i].getHasAccessDate()) { 1017 files[i].setAccessDate(header.getLong()); 1018 } 1019 } 1020 break; 1021 } 1022 case NID.kMTime: { 1023 final BitSet timesDefined = readAllOrBits(header, files.length); 1024 final int external = getUnsignedByte(header); 1025 if (external != 0) { 1026 throw new IOException("Unimplemented"); 1027 } 1028 for (int i = 0; i < files.length; i++) { 1029 files[i].setHasLastModifiedDate(timesDefined.get(i)); 1030 if (files[i].getHasLastModifiedDate()) { 1031 files[i].setLastModifiedDate(header.getLong()); 1032 } 1033 } 1034 break; 1035 } 1036 case NID.kWinAttributes: { 1037 final BitSet attributesDefined = readAllOrBits(header, files.length); 1038 final int external = getUnsignedByte(header); 1039 if (external != 0) { 1040 throw new IOException("Unimplemented"); 1041 } 1042 for (int i = 0; i < files.length; i++) { 1043 files[i].setHasWindowsAttributes(attributesDefined.get(i)); 1044 if (files[i].getHasWindowsAttributes()) { 1045 files[i].setWindowsAttributes(header.getInt()); 1046 } 1047 } 1048 break; 1049 } 1050 case NID.kStartPos: { 1051 throw new IOException("kStartPos is unsupported, please report"); 1052 } 1053 case NID.kDummy: { 1054 // 7z 9.20 asserts the content is all zeros and ignores the property 1055 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287 1056 1057 if (skipBytesFully(header, size) < size) { 1058 throw new IOException("Incomplete kDummy property"); 1059 } 1060 break; 1061 } 1062 1063 default: { 1064 // Compress up to 1.8.1 would throw an exception, now we ignore it (see COMPRESS-287 1065 if (skipBytesFully(header, size) < size) { 1066 throw new IOException("Incomplete property of type " + propertyType); 1067 } 1068 break; 1069 } 1070 } 1071 } 1072 int nonEmptyFileCounter = 0; 1073 int emptyFileCounter = 0; 1074 for (int i = 0; i < files.length; i++) { 1075 files[i].setHasStream(isEmptyStream == null || !isEmptyStream.get(i)); 1076 if (files[i].hasStream()) { 1077 if (archive.subStreamsInfo == null) { 1078 throw new IOException("Archive contains file with streams but no subStreamsInfo"); 1079 } 1080 files[i].setDirectory(false); 1081 files[i].setAntiItem(false); 1082 files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter)); 1083 files[i].setCrcValue(archive.subStreamsInfo.crcs[nonEmptyFileCounter]); 1084 files[i].setSize(archive.subStreamsInfo.unpackSizes[nonEmptyFileCounter]); 1085 ++nonEmptyFileCounter; 1086 } else { 1087 files[i].setDirectory(isEmptyFile == null || !isEmptyFile.get(emptyFileCounter)); 1088 files[i].setAntiItem(isAnti != null && isAnti.get(emptyFileCounter)); 1089 files[i].setHasCrc(false); 1090 files[i].setSize(0); 1091 ++emptyFileCounter; 1092 } 1093 } 1094 archive.files = files; 1095 calculateStreamMap(archive); 1096 } 1097 1098 private void calculateStreamMap(final Archive archive) throws IOException { 1099 final StreamMap streamMap = new StreamMap(); 1100 1101 int nextFolderPackStreamIndex = 0; 1102 final int numFolders = archive.folders != null ? archive.folders.length : 0; 1103 streamMap.folderFirstPackStreamIndex = new int[numFolders]; 1104 for (int i = 0; i < numFolders; i++) { 1105 streamMap.folderFirstPackStreamIndex[i] = nextFolderPackStreamIndex; 1106 nextFolderPackStreamIndex += archive.folders[i].packedStreams.length; 1107 } 1108 1109 long nextPackStreamOffset = 0; 1110 final int numPackSizes = archive.packSizes != null ? archive.packSizes.length : 0; 1111 streamMap.packStreamOffsets = new long[numPackSizes]; 1112 for (int i = 0; i < numPackSizes; i++) { 1113 streamMap.packStreamOffsets[i] = nextPackStreamOffset; 1114 nextPackStreamOffset += archive.packSizes[i]; 1115 } 1116 1117 streamMap.folderFirstFileIndex = new int[numFolders]; 1118 streamMap.fileFolderIndex = new int[archive.files.length]; 1119 int nextFolderIndex = 0; 1120 int nextFolderUnpackStreamIndex = 0; 1121 for (int i = 0; i < archive.files.length; i++) { 1122 if (!archive.files[i].hasStream() && nextFolderUnpackStreamIndex == 0) { 1123 streamMap.fileFolderIndex[i] = -1; 1124 continue; 1125 } 1126 if (nextFolderUnpackStreamIndex == 0) { 1127 for (; nextFolderIndex < archive.folders.length; ++nextFolderIndex) { 1128 streamMap.folderFirstFileIndex[nextFolderIndex] = i; 1129 if (archive.folders[nextFolderIndex].numUnpackSubStreams > 0) { 1130 break; 1131 } 1132 } 1133 if (nextFolderIndex >= archive.folders.length) { 1134 throw new IOException("Too few folders in archive"); 1135 } 1136 } 1137 streamMap.fileFolderIndex[i] = nextFolderIndex; 1138 if (!archive.files[i].hasStream()) { 1139 continue; 1140 } 1141 ++nextFolderUnpackStreamIndex; 1142 if (nextFolderUnpackStreamIndex >= archive.folders[nextFolderIndex].numUnpackSubStreams) { 1143 ++nextFolderIndex; 1144 nextFolderUnpackStreamIndex = 0; 1145 } 1146 } 1147 1148 archive.streamMap = streamMap; 1149 } 1150 1151 private void buildDecodingStream(int entryIndex) throws IOException { 1152 if (archive.streamMap == null) { 1153 throw new IOException("Archive doesn't contain stream information to read entries"); 1154 } 1155 final int folderIndex = archive.streamMap.fileFolderIndex[entryIndex]; 1156 if (folderIndex < 0) { 1157 deferredBlockStreams.clear(); 1158 // TODO: previously it'd return an empty stream? 1159 // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0); 1160 return; 1161 } 1162 final SevenZArchiveEntry file = archive.files[entryIndex]; 1163 boolean isInSameFolder = false; 1164 final Folder folder = archive.folders[folderIndex]; 1165 final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; 1166 final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + 1167 archive.streamMap.packStreamOffsets[firstPackStreamIndex]; 1168 if (currentFolderIndex == folderIndex) { 1169 // (COMPRESS-320). 1170 // The current entry is within the same (potentially opened) folder. The 1171 // previous stream has to be fully decoded before we can start reading 1172 // but don't do it eagerly -- if the user skips over the entire folder nothing 1173 // is effectively decompressed. 1174 1175 file.setContentMethods(archive.files[entryIndex - 1].getContentMethods()); 1176 1177 // if this is called in a random access, then the content methods of previous entry may be null 1178 // the content methods should be set to methods of the first entry as it must not be null, 1179 // and the content methods would only be set if the content methods was not set 1180 if(currentEntryIndex != entryIndex && file.getContentMethods() == null) { 1181 int folderFirstFileIndex = archive.streamMap.folderFirstFileIndex[folderIndex]; 1182 SevenZArchiveEntry folderFirstFile = archive.files[folderFirstFileIndex]; 1183 file.setContentMethods(folderFirstFile.getContentMethods()); 1184 } 1185 isInSameFolder = true; 1186 } else { 1187 // We're opening a new folder. Discard any queued streams/ folder stream. 1188 currentFolderIndex = folderIndex; 1189 deferredBlockStreams.clear(); 1190 if (currentFolderInputStream != null) { 1191 currentFolderInputStream.close(); 1192 currentFolderInputStream = null; 1193 } 1194 1195 currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file); 1196 } 1197 1198 // if this mothod is called in a random access, then some entries need to be skipped, 1199 // if the entry of entryIndex is in the same folder of the currentFolderIndex, 1200 // then the entries between (currentEntryIndex + 1) and (entryIndex - 1) need to be skipped, 1201 // otherwise it's a new folder, and the entries between firstFileInFolderIndex and (entryIndex - 1) need to be skipped 1202 if(currentEntryIndex != entryIndex) { 1203 int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[folderIndex]; 1204 if(isInSameFolder) { 1205 if(currentEntryIndex < entryIndex) { 1206 // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped 1207 filesToSkipStartIndex = currentEntryIndex + 1; 1208 } else { 1209 // the entry is in the same folder of current entry, but it has already been read before, we need to reset 1210 // the position of the currentFolderInputStream to the beginning of folder, and then skip the files 1211 // from the start entry of the folder again 1212 deferredBlockStreams.clear(); 1213 channel.position(folderOffset); 1214 } 1215 } 1216 1217 for(int i = filesToSkipStartIndex;i < entryIndex;i++) { 1218 SevenZArchiveEntry fileToSkip = archive.files[i]; 1219 InputStream fileStreamToSkip = new BoundedInputStream(currentFolderInputStream, fileToSkip.getSize()); 1220 if (fileToSkip.getHasCrc()) { 1221 fileStreamToSkip = new CRC32VerifyingInputStream(fileStreamToSkip, fileToSkip.getSize(), fileToSkip.getCrcValue()); 1222 } 1223 deferredBlockStreams.add(fileStreamToSkip); 1224 1225 // set the content methods as well, it equals to file.getContentMethods() because they are in same folder 1226 fileToSkip.setContentMethods(file.getContentMethods()); 1227 } 1228 } 1229 1230 InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize()); 1231 if (file.getHasCrc()) { 1232 fileStream = new CRC32VerifyingInputStream(fileStream, file.getSize(), file.getCrcValue()); 1233 } 1234 1235 deferredBlockStreams.add(fileStream); 1236 } 1237 1238 private InputStream buildDecoderStack(final Folder folder, final long folderOffset, 1239 final int firstPackStreamIndex, final SevenZArchiveEntry entry) throws IOException { 1240 channel.position(folderOffset); 1241 InputStream inputStreamStack = new FilterInputStream(new BufferedInputStream( 1242 new BoundedSeekableByteChannelInputStream(channel, 1243 archive.packSizes[firstPackStreamIndex]))) { 1244 @Override 1245 public int read() throws IOException { 1246 final int r = in.read(); 1247 if (r >= 0) { 1248 count(1); 1249 } 1250 return r; 1251 } 1252 @Override 1253 public int read(final byte[] b) throws IOException { 1254 return read(b, 0, b.length); 1255 } 1256 @Override 1257 public int read(final byte[] b, final int off, final int len) throws IOException { 1258 if (len == 0) { 1259 return 0; 1260 } 1261 final int r = in.read(b, off, len); 1262 if (r >= 0) { 1263 count(r); 1264 } 1265 return r; 1266 } 1267 private void count(int c) { 1268 compressedBytesReadFromCurrentEntry += c; 1269 } 1270 }; 1271 final LinkedList<SevenZMethodConfiguration> methods = new LinkedList<>(); 1272 for (final Coder coder : folder.getOrderedCoders()) { 1273 if (coder.numInStreams != 1 || coder.numOutStreams != 1) { 1274 throw new IOException("Multi input/output stream coders are not yet supported"); 1275 } 1276 final SevenZMethod method = SevenZMethod.byId(coder.decompressionMethodId); 1277 inputStreamStack = Coders.addDecoder(fileName, inputStreamStack, 1278 folder.getUnpackSizeForCoder(coder), coder, password, options.getMaxMemoryLimitInKb()); 1279 methods.addFirst(new SevenZMethodConfiguration(method, 1280 Coders.findByMethod(method).getOptionsFromCoder(coder, inputStreamStack))); 1281 } 1282 entry.setContentMethods(methods); 1283 if (folder.hasCrc) { 1284 return new CRC32VerifyingInputStream(inputStreamStack, 1285 folder.getUnpackSize(), folder.crc); 1286 } 1287 return inputStreamStack; 1288 } 1289 1290 /** 1291 * Reads a byte of data. 1292 * 1293 * @return the byte read, or -1 if end of input is reached 1294 * @throws IOException 1295 * if an I/O error has occurred 1296 */ 1297 public int read() throws IOException { 1298 int b = getCurrentStream().read(); 1299 if (b >= 0) { 1300 uncompressedBytesReadFromCurrentEntry++; 1301 } 1302 return b; 1303 } 1304 1305 private InputStream getCurrentStream() throws IOException { 1306 if (archive.files[currentEntryIndex].getSize() == 0) { 1307 return new ByteArrayInputStream(new byte[0]); 1308 } 1309 if (deferredBlockStreams.isEmpty()) { 1310 throw new IllegalStateException("No current 7z entry (call getNextEntry() first)."); 1311 } 1312 1313 while (deferredBlockStreams.size() > 1) { 1314 // In solid compression mode we need to decompress all leading folder' 1315 // streams to get access to an entry. We defer this until really needed 1316 // so that entire blocks can be skipped without wasting time for decompression. 1317 try (final InputStream stream = deferredBlockStreams.remove(0)) { 1318 IOUtils.skip(stream, Long.MAX_VALUE); 1319 } 1320 compressedBytesReadFromCurrentEntry = 0; 1321 } 1322 1323 return deferredBlockStreams.get(0); 1324 } 1325 1326 /** 1327 * Returns an InputStream for reading the contents of the given entry. 1328 * 1329 * <p>For archives using solid compression randomly accessing 1330 * entries will be significantly slower than reading the archive 1331 * sequentiallly.</p> 1332 * 1333 * @param entry the entry to get the stream for. 1334 * @return a stream to read the entry from. 1335 * @throws IOException if unable to create an input stream from the zipentry 1336 * @since Compress 1.20 1337 */ 1338 public InputStream getInputStream(SevenZArchiveEntry entry) throws IOException { 1339 int entryIndex = -1; 1340 for (int i = 0; i < this.archive.files.length;i++) { 1341 if (entry == this.archive.files[i]) { 1342 entryIndex = i; 1343 break; 1344 } 1345 } 1346 1347 if (entryIndex < 0) { 1348 throw new IllegalArgumentException("Can not find " + entry.getName() + " in " + this.fileName); 1349 } 1350 1351 buildDecodingStream(entryIndex); 1352 currentEntryIndex = entryIndex; 1353 currentFolderIndex = archive.streamMap.fileFolderIndex[entryIndex]; 1354 return getCurrentStream(); 1355 } 1356 1357 /** 1358 * Reads data into an array of bytes. 1359 * 1360 * @param b the array to write data to 1361 * @return the number of bytes read, or -1 if end of input is reached 1362 * @throws IOException 1363 * if an I/O error has occurred 1364 */ 1365 public int read(final byte[] b) throws IOException { 1366 return read(b, 0, b.length); 1367 } 1368 1369 /** 1370 * Reads data into an array of bytes. 1371 * 1372 * @param b the array to write data to 1373 * @param off offset into the buffer to start filling at 1374 * @param len of bytes to read 1375 * @return the number of bytes read, or -1 if end of input is reached 1376 * @throws IOException 1377 * if an I/O error has occurred 1378 */ 1379 public int read(final byte[] b, final int off, final int len) throws IOException { 1380 if (len == 0) { 1381 return 0; 1382 } 1383 int cnt = getCurrentStream().read(b, off, len); 1384 if (cnt > 0) { 1385 uncompressedBytesReadFromCurrentEntry += cnt; 1386 } 1387 return cnt; 1388 } 1389 1390 /** 1391 * Provides statistics for bytes read from the current entry. 1392 * 1393 * @return statistics for bytes read from the current entry 1394 * @since 1.17 1395 */ 1396 public InputStreamStatistics getStatisticsForCurrentEntry() { 1397 return new InputStreamStatistics() { 1398 @Override 1399 public long getCompressedCount() { 1400 return compressedBytesReadFromCurrentEntry; 1401 } 1402 @Override 1403 public long getUncompressedCount() { 1404 return uncompressedBytesReadFromCurrentEntry; 1405 } 1406 }; 1407 } 1408 1409 private static long readUint64(final ByteBuffer in) throws IOException { 1410 // long rather than int as it might get shifted beyond the range of an int 1411 final long firstByte = getUnsignedByte(in); 1412 int mask = 0x80; 1413 long value = 0; 1414 for (int i = 0; i < 8; i++) { 1415 if ((firstByte & mask) == 0) { 1416 return value | ((firstByte & (mask - 1)) << (8 * i)); 1417 } 1418 final long nextByte = getUnsignedByte(in); 1419 value |= nextByte << (8 * i); 1420 mask >>>= 1; 1421 } 1422 return value; 1423 } 1424 1425 private static int getUnsignedByte(ByteBuffer buf) { 1426 return buf.get() & 0xff; 1427 } 1428 1429 /** 1430 * Checks if the signature matches what is expected for a 7z file. 1431 * 1432 * @param signature 1433 * the bytes to check 1434 * @param length 1435 * the number of bytes to check 1436 * @return true, if this is the signature of a 7z archive. 1437 * @since 1.8 1438 */ 1439 public static boolean matches(final byte[] signature, final int length) { 1440 if (length < sevenZSignature.length) { 1441 return false; 1442 } 1443 1444 for (int i = 0; i < sevenZSignature.length; i++) { 1445 if (signature[i] != sevenZSignature[i]) { 1446 return false; 1447 } 1448 } 1449 return true; 1450 } 1451 1452 private static long skipBytesFully(final ByteBuffer input, long bytesToSkip) throws IOException { 1453 if (bytesToSkip < 1) { 1454 return 0; 1455 } 1456 int current = input.position(); 1457 int maxSkip = input.remaining(); 1458 if (maxSkip < bytesToSkip) { 1459 bytesToSkip = maxSkip; 1460 } 1461 input.position(current + (int) bytesToSkip); 1462 return bytesToSkip; 1463 } 1464 1465 private void readFully(ByteBuffer buf) throws IOException { 1466 buf.rewind(); 1467 IOUtils.readFully(channel, buf); 1468 buf.flip(); 1469 } 1470 1471 @Override 1472 public String toString() { 1473 return archive.toString(); 1474 } 1475 1476 /** 1477 * Derives a default file name from the archive name - if known. 1478 * 1479 * <p>This implements the same heuristics the 7z tools use. In 1480 * 7z's case if an archive contains entries without a name - 1481 * i.e. {@link SevenZArchiveEntry#getName} returns {@code null} - 1482 * then its command line and GUI tools will use this default name 1483 * when extracting the entries.</p> 1484 * 1485 * @return null if the name of the archive is unknown. Otherwise 1486 * if the name of the archive has got any extension, it is 1487 * stripped and the remainder returned. Finally if the name of the 1488 * archive hasn't got any extension then a {@code ~} character is 1489 * appended to the archive name. 1490 * 1491 * @since 1.19 1492 */ 1493 public String getDefaultName() { 1494 if (DEFAULT_FILE_NAME.equals(fileName) || fileName == null) { 1495 return null; 1496 } 1497 1498 final String lastSegment = new File(fileName).getName(); 1499 final int dotPos = lastSegment.lastIndexOf("."); 1500 if (dotPos > 0) { // if the file starts with a dot then this is not an extension 1501 return lastSegment.substring(0, dotPos); 1502 } 1503 return lastSegment + "~"; 1504 } 1505 1506 private static final CharsetEncoder PASSWORD_ENCODER = StandardCharsets.UTF_16LE.newEncoder(); 1507 1508 private static byte[] utf16Decode(char[] chars) throws IOException { 1509 if (chars == null) { 1510 return null; 1511 } 1512 ByteBuffer encoded = PASSWORD_ENCODER.encode(CharBuffer.wrap(chars)); 1513 if (encoded.hasArray()) { 1514 return encoded.array(); 1515 } 1516 byte[] e = new byte[encoded.remaining()]; 1517 encoded.get(e); 1518 return e; 1519 } 1520 1521 private static void assertFitsIntoInt(String what, long value) throws IOException { 1522 if (value > Integer.MAX_VALUE) { 1523 throw new IOException("Cannot handle " + what + value); 1524 } 1525 } 1526}