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