001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.compress.archivers.zip; 020 021import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD; 022import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT; 023import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD; 024import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC; 025 026import java.io.ByteArrayInputStream; 027import java.io.ByteArrayOutputStream; 028import java.io.EOFException; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.PushbackInputStream; 032import java.math.BigInteger; 033import java.nio.Buffer; 034import java.nio.ByteBuffer; 035import java.util.Arrays; 036import java.util.Objects; 037import java.util.zip.CRC32; 038import java.util.zip.DataFormatException; 039import java.util.zip.Inflater; 040import java.util.zip.ZipEntry; 041import java.util.zip.ZipException; 042 043import org.apache.commons.compress.archivers.ArchiveEntry; 044import org.apache.commons.compress.archivers.ArchiveInputStream; 045import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 046import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream; 047import org.apache.commons.compress.utils.ArchiveUtils; 048import org.apache.commons.compress.utils.IOUtils; 049import org.apache.commons.compress.utils.InputStreamStatistics; 050 051/** 052 * Implements an input stream that can read Zip archives. 053 * 054 * <p>As of Apache Commons Compress it transparently supports Zip64 055 * extensions and thus individual entries and archives larger than 4 056 * GB or with more than 65536 entries.</p> 057 * 058 * <p>The {@link ZipFile} class is preferred when reading from files 059 * as {@link ZipArchiveInputStream} is limited by not being able to 060 * read the central directory header before returning entries. In 061 * particular {@link ZipArchiveInputStream}</p> 062 * 063 * <ul> 064 * 065 * <li>may return entries that are not part of the central directory 066 * at all and shouldn't be considered part of the archive.</li> 067 * 068 * <li>may return several entries with the same name.</li> 069 * 070 * <li>will not return internal or external attributes.</li> 071 * 072 * <li>may return incomplete extra field data.</li> 073 * 074 * <li>may return unknown sizes and CRC values for entries until the 075 * next entry has been reached if the archive uses the data 076 * descriptor feature.</li> 077 * 078 * </ul> 079 * 080 * @see ZipFile 081 * @NotThreadSafe 082 */ 083public class ZipArchiveInputStream extends ArchiveInputStream implements InputStreamStatistics { 084 085 /** The zip encoding to use for file names and the file comment. */ 086 private final ZipEncoding zipEncoding; 087 088 // the provided encoding (for unit tests) 089 final String encoding; 090 091 /** Whether to look for and use Unicode extra fields. */ 092 private final boolean useUnicodeExtraFields; 093 094 /** Wrapped stream, will always be a PushbackInputStream. */ 095 private final InputStream inputStream; 096 097 /** Inflater used for all deflated entries. */ 098 private final Inflater inf = new Inflater(true); 099 100 /** Buffer used to read from the wrapped stream. */ 101 private final ByteBuffer buf = ByteBuffer.allocate(ZipArchiveOutputStream.BUFFER_SIZE); 102 103 /** The entry that is currently being read. */ 104 private CurrentEntry current; 105 106 /** Whether the stream has been closed. */ 107 private boolean closed; 108 109 /** Whether the stream has reached the central directory - and thus found all entries. */ 110 private boolean hitCentralDirectory; 111 112 /** 113 * When reading a stored entry that uses the data descriptor this 114 * stream has to read the full entry and caches it. This is the 115 * cache. 116 */ 117 private ByteArrayInputStream lastStoredEntry; 118 119 /** 120 * Whether the stream will try to read STORED entries that use a data descriptor. 121 * Setting it to true means we will not stop reading a entry with the compressed 122 * size, instead we will stoping reading a entry when a data descriptor is met(by 123 * finding the Data Descriptor Signature). This will completely break down in some 124 * cases - like JARs in WARs. 125 * <p> 126 * See also : 127 * https://issues.apache.org/jira/projects/COMPRESS/issues/COMPRESS-555 128 * https://github.com/apache/commons-compress/pull/137#issuecomment-690835644 129 */ 130 private final boolean allowStoredEntriesWithDataDescriptor; 131 132 /** Count decompressed bytes for current entry */ 133 private long uncompressedCount; 134 135 /** Whether the stream will try to skip the zip split signature(08074B50) at the beginning **/ 136 private final boolean skipSplitSig; 137 138 private static final int LFH_LEN = 30; 139 /* 140 local file header signature WORD 141 version needed to extract SHORT 142 general purpose bit flag SHORT 143 compression method SHORT 144 last mod file time SHORT 145 last mod file date SHORT 146 crc-32 WORD 147 compressed size WORD 148 uncompressed size WORD 149 file name length SHORT 150 extra field length SHORT 151 */ 152 153 private static final int CFH_LEN = 46; 154 /* 155 central file header signature WORD 156 version made by SHORT 157 version needed to extract SHORT 158 general purpose bit flag SHORT 159 compression method SHORT 160 last mod file time SHORT 161 last mod file date SHORT 162 crc-32 WORD 163 compressed size WORD 164 uncompressed size WORD 165 file name length SHORT 166 extra field length SHORT 167 file comment length SHORT 168 disk number start SHORT 169 internal file attributes SHORT 170 external file attributes WORD 171 relative offset of local header WORD 172 */ 173 174 private static final long TWO_EXP_32 = ZIP64_MAGIC + 1; 175 176 // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) 177 private final byte[] lfhBuf = new byte[LFH_LEN]; 178 private final byte[] skipBuf = new byte[1024]; 179 private final byte[] shortBuf = new byte[SHORT]; 180 private final byte[] wordBuf = new byte[WORD]; 181 private final byte[] twoDwordBuf = new byte[2 * DWORD]; 182 183 private int entriesRead; 184 185 /** 186 * Create an instance using UTF-8 encoding 187 * @param inputStream the stream to wrap 188 */ 189 public ZipArchiveInputStream(final InputStream inputStream) { 190 this(inputStream, ZipEncodingHelper.UTF8); 191 } 192 193 /** 194 * Create an instance using the specified encoding 195 * @param inputStream the stream to wrap 196 * @param encoding the encoding to use for file names, use null 197 * for the platform's default encoding 198 * @since 1.5 199 */ 200 public ZipArchiveInputStream(final InputStream inputStream, final String encoding) { 201 this(inputStream, encoding, true); 202 } 203 204 /** 205 * Create an instance using the specified encoding 206 * @param inputStream the stream to wrap 207 * @param encoding the encoding to use for file names, use null 208 * for the platform's default encoding 209 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 210 * Extra Fields (if present) to set the file names. 211 */ 212 public ZipArchiveInputStream(final InputStream inputStream, final String encoding, final boolean useUnicodeExtraFields) { 213 this(inputStream, encoding, useUnicodeExtraFields, false); 214 } 215 216 /** 217 * Create an instance using the specified encoding 218 * @param inputStream the stream to wrap 219 * @param encoding the encoding to use for file names, use null 220 * for the platform's default encoding 221 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 222 * Extra Fields (if present) to set the file names. 223 * @param allowStoredEntriesWithDataDescriptor whether the stream 224 * will try to read STORED entries that use a data descriptor 225 * @since 1.1 226 */ 227 public ZipArchiveInputStream(final InputStream inputStream, 228 final String encoding, 229 final boolean useUnicodeExtraFields, 230 final boolean allowStoredEntriesWithDataDescriptor) { 231 this(inputStream, encoding, useUnicodeExtraFields, allowStoredEntriesWithDataDescriptor, false); 232 } 233 234 /** 235 * Create an instance using the specified encoding 236 * @param inputStream the stream to wrap 237 * @param encoding the encoding to use for file names, use null 238 * for the platform's default encoding 239 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 240 * Extra Fields (if present) to set the file names. 241 * @param allowStoredEntriesWithDataDescriptor whether the stream 242 * will try to read STORED entries that use a data descriptor 243 * @param skipSplitSig Whether the stream will try to skip the zip 244 * split signature(08074B50) at the beginning. You will need to 245 * set this to true if you want to read a split archive. 246 * @since 1.20 247 */ 248 public ZipArchiveInputStream(final InputStream inputStream, 249 final String encoding, 250 final boolean useUnicodeExtraFields, 251 final boolean allowStoredEntriesWithDataDescriptor, 252 final boolean skipSplitSig) { 253 this.encoding = encoding; 254 zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 255 this.useUnicodeExtraFields = useUnicodeExtraFields; 256 this.inputStream = new PushbackInputStream(inputStream, buf.capacity()); 257 this.allowStoredEntriesWithDataDescriptor = allowStoredEntriesWithDataDescriptor; 258 this.skipSplitSig = skipSplitSig; 259 // haven't read anything so far 260 ((Buffer)buf).limit(0); 261 } 262 263 public ZipArchiveEntry getNextZipEntry() throws IOException { 264 uncompressedCount = 0; 265 266 boolean firstEntry = true; 267 if (closed || hitCentralDirectory) { 268 return null; 269 } 270 if (current != null) { 271 closeEntry(); 272 firstEntry = false; 273 } 274 275 final long currentHeaderOffset = getBytesRead(); 276 try { 277 if (firstEntry) { 278 // split archives have a special signature before the 279 // first local file header - look for it and fail with 280 // the appropriate error message if this is a split 281 // archive. 282 readFirstLocalFileHeader(); 283 } else { 284 readFully(lfhBuf); 285 } 286 } catch (final EOFException e) { //NOSONAR 287 return null; 288 } 289 290 final ZipLong sig = new ZipLong(lfhBuf); 291 if (!sig.equals(ZipLong.LFH_SIG)) { 292 if (sig.equals(ZipLong.CFH_SIG) || sig.equals(ZipLong.AED_SIG) || isApkSigningBlock(lfhBuf)) { 293 hitCentralDirectory = true; 294 skipRemainderOfArchive(); 295 return null; 296 } 297 throw new ZipException(String.format("Unexpected record signature: 0x%x", sig.getValue())); 298 } 299 300 int off = WORD; 301 current = new CurrentEntry(); 302 303 final int versionMadeBy = ZipShort.getValue(lfhBuf, off); 304 off += SHORT; 305 current.entry.setPlatform((versionMadeBy >> ZipFile.BYTE_SHIFT) & ZipFile.NIBLET_MASK); 306 307 final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(lfhBuf, off); 308 final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames(); 309 final ZipEncoding entryEncoding = hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; 310 current.hasDataDescriptor = gpFlag.usesDataDescriptor(); 311 current.entry.setGeneralPurposeBit(gpFlag); 312 313 off += SHORT; 314 315 current.entry.setMethod(ZipShort.getValue(lfhBuf, off)); 316 off += SHORT; 317 318 final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(lfhBuf, off)); 319 current.entry.setTime(time); 320 off += WORD; 321 322 ZipLong size = null, cSize = null; 323 if (!current.hasDataDescriptor) { 324 current.entry.setCrc(ZipLong.getValue(lfhBuf, off)); 325 off += WORD; 326 327 cSize = new ZipLong(lfhBuf, off); 328 off += WORD; 329 330 size = new ZipLong(lfhBuf, off); 331 off += WORD; 332 } else { 333 off += 3 * WORD; 334 } 335 336 final int fileNameLen = ZipShort.getValue(lfhBuf, off); 337 338 off += SHORT; 339 340 final int extraLen = ZipShort.getValue(lfhBuf, off); 341 off += SHORT; // NOSONAR - assignment as documentation 342 343 final byte[] fileName = readRange(fileNameLen); 344 current.entry.setName(entryEncoding.decode(fileName), fileName); 345 if (hasUTF8Flag) { 346 current.entry.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG); 347 } 348 349 final byte[] extraData = readRange(extraLen); 350 try { 351 current.entry.setExtra(extraData); 352 } catch (RuntimeException ex) { 353 final ZipException z = new ZipException("Invalid extra data in entry " + current.entry.getName()); 354 z.initCause(ex); 355 throw z; 356 } 357 358 if (!hasUTF8Flag && useUnicodeExtraFields) { 359 ZipUtil.setNameAndCommentFromExtraFields(current.entry, fileName, null); 360 } 361 362 processZip64Extra(size, cSize); 363 364 current.entry.setLocalHeaderOffset(currentHeaderOffset); 365 current.entry.setDataOffset(getBytesRead()); 366 current.entry.setStreamContiguous(true); 367 368 final ZipMethod m = ZipMethod.getMethodByCode(current.entry.getMethod()); 369 if (current.entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN) { 370 if (ZipUtil.canHandleEntryData(current.entry) && m != ZipMethod.STORED && m != ZipMethod.DEFLATED) { 371 final InputStream bis = new BoundedInputStream(inputStream, current.entry.getCompressedSize()); 372 switch (m) { 373 case UNSHRINKING: 374 current.inputStream = new UnshrinkingInputStream(bis); 375 break; 376 case IMPLODING: 377 try { 378 current.inputStream = new ExplodingInputStream( 379 current.entry.getGeneralPurposeBit().getSlidingDictionarySize(), 380 current.entry.getGeneralPurposeBit().getNumberOfShannonFanoTrees(), 381 bis); 382 } catch (final IllegalArgumentException ex) { 383 throw new IOException("bad IMPLODE data", ex); 384 } 385 break; 386 case BZIP2: 387 current.inputStream = new BZip2CompressorInputStream(bis); 388 break; 389 case ENHANCED_DEFLATED: 390 current.inputStream = new Deflate64CompressorInputStream(bis); 391 break; 392 default: 393 // we should never get here as all supported methods have been covered 394 // will cause an error when read is invoked, don't throw an exception here so people can 395 // skip unsupported entries 396 break; 397 } 398 } 399 } else if (m == ZipMethod.ENHANCED_DEFLATED) { 400 current.inputStream = new Deflate64CompressorInputStream(inputStream); 401 } 402 403 entriesRead++; 404 return current.entry; 405 } 406 407 /** 408 * Fills the given array with the first local file header and 409 * deals with splitting/spanning markers that may prefix the first 410 * LFH. 411 */ 412 private void readFirstLocalFileHeader() throws IOException { 413 readFully(lfhBuf); 414 final ZipLong sig = new ZipLong(lfhBuf); 415 416 if (!skipSplitSig && sig.equals(ZipLong.DD_SIG)) { 417 throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.SPLITTING); 418 } 419 420 // the split zip signature(08074B50) should only be skipped when the skipSplitSig is set 421 if (sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER) || sig.equals(ZipLong.DD_SIG)) { 422 // Just skip over the marker. 423 final byte[] missedLfhBytes = new byte[4]; 424 readFully(missedLfhBytes); 425 System.arraycopy(lfhBuf, 4, lfhBuf, 0, LFH_LEN - 4); 426 System.arraycopy(missedLfhBytes, 0, lfhBuf, LFH_LEN - 4, 4); 427 } 428 } 429 430 /** 431 * Records whether a Zip64 extra is present and sets the size 432 * information from it if sizes are 0xFFFFFFFF and the entry 433 * doesn't use a data descriptor. 434 */ 435 private void processZip64Extra(final ZipLong size, final ZipLong cSize) throws ZipException { 436 final ZipExtraField extra = 437 current.entry.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); 438 if (extra != null && !(extra instanceof Zip64ExtendedInformationExtraField)) { 439 throw new ZipException("archive contains unparseable zip64 extra field"); 440 } 441 final Zip64ExtendedInformationExtraField z64 = 442 (Zip64ExtendedInformationExtraField) extra; 443 current.usesZip64 = z64 != null; 444 if (!current.hasDataDescriptor) { 445 if (z64 != null // same as current.usesZip64 but avoids NPE warning 446 && (ZipLong.ZIP64_MAGIC.equals(cSize) || ZipLong.ZIP64_MAGIC.equals(size)) ) { 447 if (z64.getCompressedSize() == null || z64.getSize() == null) { 448 // avoid NPE if it's a corrupted zip archive 449 throw new ZipException("archive contains corrupted zip64 extra field"); 450 } 451 long s = z64.getCompressedSize().getLongValue(); 452 if (s < 0) { 453 throw new ZipException("broken archive, entry with negative compressed size"); 454 } 455 current.entry.setCompressedSize(s); 456 s = z64.getSize().getLongValue(); 457 if (s < 0) { 458 throw new ZipException("broken archive, entry with negative size"); 459 } 460 current.entry.setSize(s); 461 } else if (cSize != null && size != null) { 462 if (cSize.getValue() < 0) { 463 throw new ZipException("broken archive, entry with negative compressed size"); 464 } 465 current.entry.setCompressedSize(cSize.getValue()); 466 if (size.getValue() < 0) { 467 throw new ZipException("broken archive, entry with negative size"); 468 } 469 current.entry.setSize(size.getValue()); 470 } 471 } 472 } 473 474 @Override 475 public ArchiveEntry getNextEntry() throws IOException { 476 return getNextZipEntry(); 477 } 478 479 /** 480 * Whether this class is able to read the given entry. 481 * 482 * <p>May return false if it is set up to use encryption or a 483 * compression method that hasn't been implemented yet.</p> 484 * @since 1.1 485 */ 486 @Override 487 public boolean canReadEntryData(final ArchiveEntry ae) { 488 if (ae instanceof ZipArchiveEntry) { 489 final ZipArchiveEntry ze = (ZipArchiveEntry) ae; 490 return ZipUtil.canHandleEntryData(ze) 491 && supportsDataDescriptorFor(ze) 492 && supportsCompressedSizeFor(ze); 493 } 494 return false; 495 } 496 497 @Override 498 public int read(final byte[] buffer, final int offset, final int length) throws IOException { 499 if (length == 0) { 500 return 0; 501 } 502 if (closed) { 503 throw new IOException("The stream is closed"); 504 } 505 506 if (current == null) { 507 return -1; 508 } 509 510 // avoid int overflow, check null buffer 511 if (offset > buffer.length || length < 0 || offset < 0 || buffer.length - offset < length) { 512 throw new ArrayIndexOutOfBoundsException(); 513 } 514 515 ZipUtil.checkRequestedFeatures(current.entry); 516 if (!supportsDataDescriptorFor(current.entry)) { 517 throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.DATA_DESCRIPTOR, 518 current.entry); 519 } 520 if (!supportsCompressedSizeFor(current.entry)) { 521 throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.UNKNOWN_COMPRESSED_SIZE, 522 current.entry); 523 } 524 525 final int read; 526 if (current.entry.getMethod() == ZipArchiveOutputStream.STORED) { 527 read = readStored(buffer, offset, length); 528 } else if (current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED) { 529 read = readDeflated(buffer, offset, length); 530 } else if (current.entry.getMethod() == ZipMethod.UNSHRINKING.getCode() 531 || current.entry.getMethod() == ZipMethod.IMPLODING.getCode() 532 || current.entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() 533 || current.entry.getMethod() == ZipMethod.BZIP2.getCode()) { 534 read = current.inputStream.read(buffer, offset, length); 535 } else { 536 throw new UnsupportedZipFeatureException(ZipMethod.getMethodByCode(current.entry.getMethod()), 537 current.entry); 538 } 539 540 if (read >= 0) { 541 current.crc.update(buffer, offset, read); 542 uncompressedCount += read; 543 } 544 545 return read; 546 } 547 548 /** 549 * @since 1.17 550 */ 551 @SuppressWarnings("resource") // checkInputStream() does not allocate. 552 @Override 553 public long getCompressedCount() { 554 final int method = current.entry.getMethod(); 555 if (method == ZipArchiveOutputStream.STORED) { 556 return current.bytesRead; 557 } 558 if (method == ZipArchiveOutputStream.DEFLATED) { 559 return getBytesInflated(); 560 } 561 if (method == ZipMethod.UNSHRINKING.getCode() 562 || method == ZipMethod.IMPLODING.getCode() 563 || method == ZipMethod.ENHANCED_DEFLATED.getCode() 564 || method == ZipMethod.BZIP2.getCode()) { 565 return ((InputStreamStatistics) current.checkInputStream()).getCompressedCount(); 566 } 567 return -1; 568 } 569 570 /** 571 * @since 1.17 572 */ 573 @Override 574 public long getUncompressedCount() { 575 return uncompressedCount; 576 } 577 578 /** 579 * Implementation of read for STORED entries. 580 */ 581 private int readStored(final byte[] buffer, final int offset, final int length) throws IOException { 582 583 if (current.hasDataDescriptor) { 584 if (lastStoredEntry == null) { 585 readStoredEntry(); 586 } 587 return lastStoredEntry.read(buffer, offset, length); 588 } 589 590 final long csize = current.entry.getSize(); 591 if (current.bytesRead >= csize) { 592 return -1; 593 } 594 595 if (buf.position() >= buf.limit()) { 596 ((Buffer)buf).position(0); 597 final int l = inputStream.read(buf.array()); 598 if (l == -1) { 599 ((Buffer)buf).limit(0); 600 throw new IOException("Truncated ZIP file"); 601 } 602 ((Buffer)buf).limit(l); 603 604 count(l); 605 current.bytesReadFromStream += l; 606 } 607 608 int toRead = Math.min(buf.remaining(), length); 609 if ((csize - current.bytesRead) < toRead) { 610 // if it is smaller than toRead then it fits into an int 611 toRead = (int) (csize - current.bytesRead); 612 } 613 buf.get(buffer, offset, toRead); 614 current.bytesRead += toRead; 615 return toRead; 616 } 617 618 /** 619 * Implementation of read for DEFLATED entries. 620 */ 621 private int readDeflated(final byte[] buffer, final int offset, final int length) throws IOException { 622 final int read = readFromInflater(buffer, offset, length); 623 if (read <= 0) { 624 if (inf.finished()) { 625 return -1; 626 } 627 if (inf.needsDictionary()) { 628 throw new ZipException("This archive needs a preset dictionary" 629 + " which is not supported by Commons" 630 + " Compress."); 631 } 632 if (read == -1) { 633 throw new IOException("Truncated ZIP file"); 634 } 635 } 636 return read; 637 } 638 639 /** 640 * Potentially reads more bytes to fill the inflater's buffer and 641 * reads from it. 642 */ 643 private int readFromInflater(final byte[] buffer, final int offset, final int length) throws IOException { 644 int read = 0; 645 do { 646 if (inf.needsInput()) { 647 final int l = fill(); 648 if (l > 0) { 649 current.bytesReadFromStream += buf.limit(); 650 } else if (l == -1) { 651 return -1; 652 } else { 653 break; 654 } 655 } 656 try { 657 read = inf.inflate(buffer, offset, length); 658 } catch (final DataFormatException e) { 659 throw (IOException) new ZipException(e.getMessage()).initCause(e); 660 } 661 } while (read == 0 && inf.needsInput()); 662 return read; 663 } 664 665 @Override 666 public void close() throws IOException { 667 if (!closed) { 668 closed = true; 669 try { 670 inputStream.close(); 671 } finally { 672 inf.end(); 673 } 674 } 675 } 676 677 /** 678 * Skips over and discards value bytes of data from this input 679 * stream. 680 * 681 * <p>This implementation may end up skipping over some smaller 682 * number of bytes, possibly 0, if and only if it reaches the end 683 * of the underlying stream.</p> 684 * 685 * <p>The actual number of bytes skipped is returned.</p> 686 * 687 * @param value the number of bytes to be skipped. 688 * @return the actual number of bytes skipped. 689 * @throws IOException - if an I/O error occurs. 690 * @throws IllegalArgumentException - if value is negative. 691 */ 692 @Override 693 public long skip(final long value) throws IOException { 694 if (value >= 0) { 695 long skipped = 0; 696 while (skipped < value) { 697 final long rem = value - skipped; 698 final int x = read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); 699 if (x == -1) { 700 return skipped; 701 } 702 skipped += x; 703 } 704 return skipped; 705 } 706 throw new IllegalArgumentException(); 707 } 708 709 /** 710 * Checks if the signature matches what is expected for a zip file. 711 * Does not currently handle self-extracting zips which may have arbitrary 712 * leading content. 713 * 714 * @param signature the bytes to check 715 * @param length the number of bytes to check 716 * @return true, if this stream is a zip archive stream, false otherwise 717 */ 718 public static boolean matches(final byte[] signature, final int length) { 719 if (length < ZipArchiveOutputStream.LFH_SIG.length) { 720 return false; 721 } 722 723 return checksig(signature, ZipArchiveOutputStream.LFH_SIG) // normal file 724 || checksig(signature, ZipArchiveOutputStream.EOCD_SIG) // empty zip 725 || checksig(signature, ZipArchiveOutputStream.DD_SIG) // split zip 726 || checksig(signature, ZipLong.SINGLE_SEGMENT_SPLIT_MARKER.getBytes()); 727 } 728 729 private static boolean checksig(final byte[] signature, final byte[] expected) { 730 for (int i = 0; i < expected.length; i++) { 731 if (signature[i] != expected[i]) { 732 return false; 733 } 734 } 735 return true; 736 } 737 738 /** 739 * Closes the current ZIP archive entry and positions the underlying 740 * stream to the beginning of the next entry. All per-entry variables 741 * and data structures are cleared. 742 * <p> 743 * If the compressed size of this entry is included in the entry header, 744 * then any outstanding bytes are simply skipped from the underlying 745 * stream without uncompressing them. This allows an entry to be safely 746 * closed even if the compression method is unsupported. 747 * <p> 748 * In case we don't know the compressed size of this entry or have 749 * already buffered too much data from the underlying stream to support 750 * uncompression, then the uncompression process is completed and the 751 * end position of the stream is adjusted based on the result of that 752 * process. 753 * 754 * @throws IOException if an error occurs 755 */ 756 private void closeEntry() throws IOException { 757 if (closed) { 758 throw new IOException("The stream is closed"); 759 } 760 if (current == null) { 761 return; 762 } 763 764 // Ensure all entry bytes are read 765 if (currentEntryHasOutstandingBytes()) { 766 drainCurrentEntryData(); 767 } else { 768 // this is guaranteed to exhaust the stream 769 skip(Long.MAX_VALUE); //NOSONAR 770 771 final long inB = current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED 772 ? getBytesInflated() : current.bytesRead; 773 774 // this is at most a single read() operation and can't 775 // exceed the range of int 776 final int diff = (int) (current.bytesReadFromStream - inB); 777 778 // Pushback any required bytes 779 if (diff > 0) { 780 pushback(buf.array(), buf.limit() - diff, diff); 781 current.bytesReadFromStream -= diff; 782 } 783 784 // Drain remainder of entry if not all data bytes were required 785 if (currentEntryHasOutstandingBytes()) { 786 drainCurrentEntryData(); 787 } 788 } 789 790 if (lastStoredEntry == null && current.hasDataDescriptor) { 791 readDataDescriptor(); 792 } 793 794 inf.reset(); 795 ((Buffer)buf).clear().flip(); 796 current = null; 797 lastStoredEntry = null; 798 } 799 800 /** 801 * If the compressed size of the current entry is included in the entry header 802 * and there are any outstanding bytes in the underlying stream, then 803 * this returns true. 804 * 805 * @return true, if current entry is determined to have outstanding bytes, false otherwise 806 */ 807 private boolean currentEntryHasOutstandingBytes() { 808 return current.bytesReadFromStream <= current.entry.getCompressedSize() 809 && !current.hasDataDescriptor; 810 } 811 812 /** 813 * Read all data of the current entry from the underlying stream 814 * that hasn't been read, yet. 815 */ 816 private void drainCurrentEntryData() throws IOException { 817 long remaining = current.entry.getCompressedSize() - current.bytesReadFromStream; 818 while (remaining > 0) { 819 final long n = inputStream.read(buf.array(), 0, (int) Math.min(buf.capacity(), remaining)); 820 if (n < 0) { 821 throw new EOFException("Truncated ZIP entry: " 822 + ArchiveUtils.sanitize(current.entry.getName())); 823 } 824 count(n); 825 remaining -= n; 826 } 827 } 828 829 /** 830 * Get the number of bytes Inflater has actually processed. 831 * 832 * <p>for Java < Java7 the getBytes* methods in 833 * Inflater/Deflater seem to return unsigned ints rather than 834 * longs that start over with 0 at 2^32.</p> 835 * 836 * <p>The stream knows how many bytes it has read, but not how 837 * many the Inflater actually consumed - it should be between the 838 * total number of bytes read for the entry and the total number 839 * minus the last read operation. Here we just try to make the 840 * value close enough to the bytes we've read by assuming the 841 * number of bytes consumed must be smaller than (or equal to) the 842 * number of bytes read but not smaller by more than 2^32.</p> 843 */ 844 private long getBytesInflated() { 845 long inB = inf.getBytesRead(); 846 if (current.bytesReadFromStream >= TWO_EXP_32) { 847 while (inB + TWO_EXP_32 <= current.bytesReadFromStream) { 848 inB += TWO_EXP_32; 849 } 850 } 851 return inB; 852 } 853 854 private int fill() throws IOException { 855 if (closed) { 856 throw new IOException("The stream is closed"); 857 } 858 final int length = inputStream.read(buf.array()); 859 if (length > 0) { 860 ((Buffer)buf).limit(length); 861 count(buf.limit()); 862 inf.setInput(buf.array(), 0, buf.limit()); 863 } 864 return length; 865 } 866 867 private void readFully(final byte[] b) throws IOException { 868 readFully(b, 0); 869 } 870 871 private void readFully(final byte[] b, final int off) throws IOException { 872 final int len = b.length - off; 873 final int count = IOUtils.readFully(inputStream, b, off, len); 874 count(count); 875 if (count < len) { 876 throw new EOFException(); 877 } 878 } 879 880 private byte[] readRange(int len) throws IOException { 881 final byte[] ret = IOUtils.readRange(inputStream, len); 882 count(ret.length); 883 if (ret.length < len) { 884 throw new EOFException(); 885 } 886 return ret; 887 } 888 889 private void readDataDescriptor() throws IOException { 890 readFully(wordBuf); 891 ZipLong val = new ZipLong(wordBuf); 892 if (ZipLong.DD_SIG.equals(val)) { 893 // data descriptor with signature, skip sig 894 readFully(wordBuf); 895 val = new ZipLong(wordBuf); 896 } 897 current.entry.setCrc(val.getValue()); 898 899 // if there is a ZIP64 extra field, sizes are eight bytes 900 // each, otherwise four bytes each. Unfortunately some 901 // implementations - namely Java7 - use eight bytes without 902 // using a ZIP64 extra field - 903 // https://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588 904 905 // just read 16 bytes and check whether bytes nine to twelve 906 // look like one of the signatures of what could follow a data 907 // descriptor (ignoring archive decryption headers for now). 908 // If so, push back eight bytes and assume sizes are four 909 // bytes, otherwise sizes are eight bytes each. 910 readFully(twoDwordBuf); 911 final ZipLong potentialSig = new ZipLong(twoDwordBuf, DWORD); 912 if (potentialSig.equals(ZipLong.CFH_SIG) || potentialSig.equals(ZipLong.LFH_SIG)) { 913 pushback(twoDwordBuf, DWORD, DWORD); 914 long size = ZipLong.getValue(twoDwordBuf); 915 if (size < 0) { 916 throw new ZipException("broken archive, entry with negative compressed size"); 917 } 918 current.entry.setCompressedSize(size); 919 size = ZipLong.getValue(twoDwordBuf, WORD); 920 if (size < 0) { 921 throw new ZipException("broken archive, entry with negative size"); 922 } 923 current.entry.setSize(size); 924 } else { 925 long size = ZipEightByteInteger.getLongValue(twoDwordBuf); 926 if (size < 0) { 927 throw new ZipException("broken archive, entry with negative compressed size"); 928 } 929 current.entry.setCompressedSize(size); 930 size = ZipEightByteInteger.getLongValue(twoDwordBuf, DWORD); 931 if (size < 0) { 932 throw new ZipException("broken archive, entry with negative size"); 933 } 934 current.entry.setSize(size); 935 } 936 } 937 938 /** 939 * Whether this entry requires a data descriptor this library can work with. 940 * 941 * @return true if allowStoredEntriesWithDataDescriptor is true, 942 * the entry doesn't require any data descriptor or the method is 943 * DEFLATED or ENHANCED_DEFLATED. 944 */ 945 private boolean supportsDataDescriptorFor(final ZipArchiveEntry entry) { 946 return !entry.getGeneralPurposeBit().usesDataDescriptor() 947 || (allowStoredEntriesWithDataDescriptor && entry.getMethod() == ZipEntry.STORED) 948 || entry.getMethod() == ZipEntry.DEFLATED 949 || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode(); 950 } 951 952 /** 953 * Whether the compressed size for the entry is either known or 954 * not required by the compression method being used. 955 */ 956 private boolean supportsCompressedSizeFor(final ZipArchiveEntry entry) { 957 return entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN 958 || entry.getMethod() == ZipEntry.DEFLATED 959 || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() 960 || (entry.getGeneralPurposeBit().usesDataDescriptor() 961 && allowStoredEntriesWithDataDescriptor 962 && entry.getMethod() == ZipEntry.STORED); 963 } 964 965 private static final String USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER = 966 " while reading a stored entry using data descriptor. Either the archive is broken" 967 + " or it can not be read using ZipArchiveInputStream and you must use ZipFile." 968 + " A common cause for this is a ZIP archive containing a ZIP archive." 969 + " See http://commons.apache.org/proper/commons-compress/zip.html#ZipArchiveInputStream_vs_ZipFile"; 970 971 /** 972 * Caches a stored entry that uses the data descriptor. 973 * 974 * <ul> 975 * <li>Reads a stored entry until the signature of a local file 976 * header, central directory header or data descriptor has been 977 * found.</li> 978 * <li>Stores all entry data in lastStoredEntry.</p> 979 * <li>Rewinds the stream to position at the data 980 * descriptor.</li> 981 * <li>reads the data descriptor</li> 982 * </ul> 983 * 984 * <p>After calling this method the entry should know its size, 985 * the entry's data is cached and the stream is positioned at the 986 * next local file or central directory header.</p> 987 */ 988 private void readStoredEntry() throws IOException { 989 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 990 int off = 0; 991 boolean done = false; 992 993 // length of DD without signature 994 final int ddLen = current.usesZip64 ? WORD + 2 * DWORD : 3 * WORD; 995 996 while (!done) { 997 final int r = inputStream.read(buf.array(), off, ZipArchiveOutputStream.BUFFER_SIZE - off); 998 if (r <= 0) { 999 // read the whole archive without ever finding a 1000 // central directory 1001 throw new IOException("Truncated ZIP file"); 1002 } 1003 if (r + off < 4) { 1004 // buffer too small to check for a signature, loop 1005 off += r; 1006 continue; 1007 } 1008 1009 done = bufferContainsSignature(bos, off, r, ddLen); 1010 if (!done) { 1011 off = cacheBytesRead(bos, off, r, ddLen); 1012 } 1013 } 1014 if (current.entry.getCompressedSize() != current.entry.getSize()) { 1015 throw new ZipException("compressed and uncompressed size don't match" 1016 + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); 1017 } 1018 final byte[] b = bos.toByteArray(); 1019 if (b.length != current.entry.getSize()) { 1020 throw new ZipException("actual and claimed size don't match" 1021 + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); 1022 } 1023 lastStoredEntry = new ByteArrayInputStream(b); 1024 } 1025 1026 private static final byte[] LFH = ZipLong.LFH_SIG.getBytes(); 1027 private static final byte[] CFH = ZipLong.CFH_SIG.getBytes(); 1028 private static final byte[] DD = ZipLong.DD_SIG.getBytes(); 1029 1030 /** 1031 * Checks whether the current buffer contains the signature of a 1032 * "data descriptor", "local file header" or 1033 * "central directory entry". 1034 * 1035 * <p>If it contains such a signature, reads the data descriptor 1036 * and positions the stream right after the data descriptor.</p> 1037 */ 1038 private boolean bufferContainsSignature(final ByteArrayOutputStream bos, final int offset, final int lastRead, final int expectedDDLen) 1039 throws IOException { 1040 1041 boolean done = false; 1042 for (int i = 0; !done && i < offset + lastRead - 4; i++) { 1043 if (buf.array()[i] == LFH[0] && buf.array()[i + 1] == LFH[1]) { 1044 int expectDDPos = i; 1045 if (i >= expectedDDLen && 1046 (buf.array()[i + 2] == LFH[2] && buf.array()[i + 3] == LFH[3]) 1047 || (buf.array()[i + 2] == CFH[2] && buf.array()[i + 3] == CFH[3])) { 1048 // found a LFH or CFH: 1049 expectDDPos = i - expectedDDLen; 1050 done = true; 1051 } 1052 else if (buf.array()[i + 2] == DD[2] && buf.array()[i + 3] == DD[3]) { 1053 // found DD: 1054 done = true; 1055 } 1056 if (done) { 1057 // * push back bytes read in excess as well as the data 1058 // descriptor 1059 // * copy the remaining bytes to cache 1060 // * read data descriptor 1061 pushback(buf.array(), expectDDPos, offset + lastRead - expectDDPos); 1062 bos.write(buf.array(), 0, expectDDPos); 1063 readDataDescriptor(); 1064 } 1065 } 1066 } 1067 return done; 1068 } 1069 1070 /** 1071 * If the last read bytes could hold a data descriptor and an 1072 * incomplete signature then save the last bytes to the front of 1073 * the buffer and cache everything in front of the potential data 1074 * descriptor into the given ByteArrayOutputStream. 1075 * 1076 * <p>Data descriptor plus incomplete signature (3 bytes in the 1077 * worst case) can be 20 bytes max.</p> 1078 */ 1079 private int cacheBytesRead(final ByteArrayOutputStream bos, int offset, final int lastRead, final int expecteDDLen) { 1080 final int cacheable = offset + lastRead - expecteDDLen - 3; 1081 if (cacheable > 0) { 1082 bos.write(buf.array(), 0, cacheable); 1083 System.arraycopy(buf.array(), cacheable, buf.array(), 0, expecteDDLen + 3); 1084 offset = expecteDDLen + 3; 1085 } else { 1086 offset += lastRead; 1087 } 1088 return offset; 1089 } 1090 1091 private void pushback(final byte[] buf, final int offset, final int length) throws IOException { 1092 ((PushbackInputStream) inputStream).unread(buf, offset, length); 1093 pushedBackBytes(length); 1094 } 1095 1096 // End of Central Directory Record 1097 // end of central dir signature WORD 1098 // number of this disk SHORT 1099 // number of the disk with the 1100 // start of the central directory SHORT 1101 // total number of entries in the 1102 // central directory on this disk SHORT 1103 // total number of entries in 1104 // the central directory SHORT 1105 // size of the central directory WORD 1106 // offset of start of central 1107 // directory with respect to 1108 // the starting disk number WORD 1109 // .ZIP file comment length SHORT 1110 // .ZIP file comment up to 64KB 1111 // 1112 1113 /** 1114 * Reads the stream until it find the "End of central directory 1115 * record" and consumes it as well. 1116 */ 1117 private void skipRemainderOfArchive() throws IOException { 1118 // skip over central directory. One LFH has been read too much 1119 // already. The calculation discounts file names and extra 1120 // data so it will be too short. 1121 if (entriesRead > 0) { 1122 realSkip((long) entriesRead * CFH_LEN - LFH_LEN); 1123 final boolean foundEocd = findEocdRecord(); 1124 if (foundEocd) { 1125 realSkip((long) ZipFile.MIN_EOCD_SIZE - WORD /* signature */ - SHORT /* comment len */); 1126 readFully(shortBuf); 1127 // file comment 1128 final int commentLen = ZipShort.getValue(shortBuf); 1129 if (commentLen >= 0) { 1130 realSkip(commentLen); 1131 return; 1132 } 1133 } 1134 } 1135 throw new IOException("Truncated ZIP file"); 1136 } 1137 1138 /** 1139 * Reads forward until the signature of the "End of central 1140 * directory" record is found. 1141 */ 1142 private boolean findEocdRecord() throws IOException { 1143 int currentByte = -1; 1144 boolean skipReadCall = false; 1145 while (skipReadCall || (currentByte = readOneByte()) > -1) { 1146 skipReadCall = false; 1147 if (!isFirstByteOfEocdSig(currentByte)) { 1148 continue; 1149 } 1150 currentByte = readOneByte(); 1151 if (currentByte != ZipArchiveOutputStream.EOCD_SIG[1]) { 1152 if (currentByte == -1) { 1153 break; 1154 } 1155 skipReadCall = isFirstByteOfEocdSig(currentByte); 1156 continue; 1157 } 1158 currentByte = readOneByte(); 1159 if (currentByte != ZipArchiveOutputStream.EOCD_SIG[2]) { 1160 if (currentByte == -1) { 1161 break; 1162 } 1163 skipReadCall = isFirstByteOfEocdSig(currentByte); 1164 continue; 1165 } 1166 currentByte = readOneByte(); 1167 if (currentByte == -1) { 1168 break; 1169 } 1170 if (currentByte == ZipArchiveOutputStream.EOCD_SIG[3]) { 1171 return true; 1172 } 1173 skipReadCall = isFirstByteOfEocdSig(currentByte); 1174 } 1175 return false; 1176 } 1177 1178 /** 1179 * Skips bytes by reading from the underlying stream rather than 1180 * the (potentially inflating) archive stream - which {@link 1181 * #skip} would do. 1182 * 1183 * Also updates bytes-read counter. 1184 */ 1185 private void realSkip(final long value) throws IOException { 1186 if (value >= 0) { 1187 long skipped = 0; 1188 while (skipped < value) { 1189 final long rem = value - skipped; 1190 final int x = inputStream.read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); 1191 if (x == -1) { 1192 return; 1193 } 1194 count(x); 1195 skipped += x; 1196 } 1197 return; 1198 } 1199 throw new IllegalArgumentException(); 1200 } 1201 1202 /** 1203 * Reads bytes by reading from the underlying stream rather than 1204 * the (potentially inflating) archive stream - which {@link #read} would do. 1205 * 1206 * Also updates bytes-read counter. 1207 */ 1208 private int readOneByte() throws IOException { 1209 final int b = inputStream.read(); 1210 if (b != -1) { 1211 count(1); 1212 } 1213 return b; 1214 } 1215 1216 private boolean isFirstByteOfEocdSig(final int b) { 1217 return b == ZipArchiveOutputStream.EOCD_SIG[0]; 1218 } 1219 1220 private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] { 1221 'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2', 1222 }; 1223 private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); 1224 1225 /** 1226 * Checks whether this might be an APK Signing Block. 1227 * 1228 * <p>Unfortunately the APK signing block does not start with some kind of signature, it rather ends with one. It 1229 * starts with a length, so what we do is parse the suspect length, skip ahead far enough, look for the signature 1230 * and if we've found it, return true.</p> 1231 * 1232 * @param suspectLocalFileHeader the bytes read from the underlying stream in the expectation that they would hold 1233 * the local file header of the next entry. 1234 * 1235 * @return true if this looks like a APK signing block 1236 * 1237 * @see <a href="https://source.android.com/security/apksigning/v2">https://source.android.com/security/apksigning/v2</a> 1238 */ 1239 private boolean isApkSigningBlock(final byte[] suspectLocalFileHeader) throws IOException { 1240 // length of block excluding the size field itself 1241 final BigInteger len = ZipEightByteInteger.getValue(suspectLocalFileHeader); 1242 // LFH has already been read and all but the first eight bytes contain (part of) the APK signing block, 1243 // also subtract 16 bytes in order to position us at the magic string 1244 BigInteger toSkip = len.add(BigInteger.valueOf(DWORD - suspectLocalFileHeader.length 1245 - (long) APK_SIGNING_BLOCK_MAGIC.length)); 1246 final byte[] magic = new byte[APK_SIGNING_BLOCK_MAGIC.length]; 1247 1248 try { 1249 if (toSkip.signum() < 0) { 1250 // suspectLocalFileHeader contains the start of suspect magic string 1251 final int off = suspectLocalFileHeader.length + toSkip.intValue(); 1252 // length was shorter than magic length 1253 if (off < DWORD) { 1254 return false; 1255 } 1256 final int bytesInBuffer = Math.abs(toSkip.intValue()); 1257 System.arraycopy(suspectLocalFileHeader, off, magic, 0, Math.min(bytesInBuffer, magic.length)); 1258 if (bytesInBuffer < magic.length) { 1259 readFully(magic, bytesInBuffer); 1260 } 1261 } else { 1262 while (toSkip.compareTo(LONG_MAX) > 0) { 1263 realSkip(Long.MAX_VALUE); 1264 toSkip = toSkip.add(LONG_MAX.negate()); 1265 } 1266 realSkip(toSkip.longValue()); 1267 readFully(magic); 1268 } 1269 } catch (final EOFException ex) { //NOSONAR 1270 // length was invalid 1271 return false; 1272 } 1273 return Arrays.equals(magic, APK_SIGNING_BLOCK_MAGIC); 1274 } 1275 1276 /** 1277 * Structure collecting information for the entry that is 1278 * currently being read. 1279 */ 1280 private static final class CurrentEntry { 1281 1282 /** 1283 * Current ZIP entry. 1284 */ 1285 private final ZipArchiveEntry entry = new ZipArchiveEntry(); 1286 1287 /** 1288 * Does the entry use a data descriptor? 1289 */ 1290 private boolean hasDataDescriptor; 1291 1292 /** 1293 * Does the entry have a ZIP64 extended information extra field. 1294 */ 1295 private boolean usesZip64; 1296 1297 /** 1298 * Number of bytes of entry content read by the client if the 1299 * entry is STORED. 1300 */ 1301 private long bytesRead; 1302 1303 /** 1304 * Number of bytes of entry content read from the stream. 1305 * 1306 * <p>This may be more than the actual entry's length as some 1307 * stuff gets buffered up and needs to be pushed back when the 1308 * end of the entry has been reached.</p> 1309 */ 1310 private long bytesReadFromStream; 1311 1312 /** 1313 * The checksum calculated as the current entry is read. 1314 */ 1315 private final CRC32 crc = new CRC32(); 1316 1317 /** 1318 * The input stream decompressing the data for shrunk and imploded entries. 1319 */ 1320 private InputStream inputStream; 1321 1322 @SuppressWarnings("unchecked") // Caller beware 1323 private <T extends InputStream> T checkInputStream() { 1324 return (T) Objects.requireNonNull(inputStream, "inputStream"); 1325 } 1326 } 1327 1328 /** 1329 * Bounded input stream adapted from commons-io 1330 */ 1331 private class BoundedInputStream extends InputStream { 1332 1333 /** the wrapped input stream */ 1334 private final InputStream in; 1335 1336 /** the max length to provide */ 1337 private final long max; 1338 1339 /** the number of bytes already returned */ 1340 private long pos; 1341 1342 /** 1343 * Creates a new {@code BoundedInputStream} that wraps the given input 1344 * stream and limits it to a certain size. 1345 * 1346 * @param in The wrapped input stream 1347 * @param size The maximum number of bytes to return 1348 */ 1349 public BoundedInputStream(final InputStream in, final long size) { 1350 this.max = size; 1351 this.in = in; 1352 } 1353 1354 @Override 1355 public int read() throws IOException { 1356 if (max >= 0 && pos >= max) { 1357 return -1; 1358 } 1359 final int result = in.read(); 1360 pos++; 1361 count(1); 1362 current.bytesReadFromStream++; 1363 return result; 1364 } 1365 1366 @Override 1367 public int read(final byte[] b) throws IOException { 1368 return this.read(b, 0, b.length); 1369 } 1370 1371 @Override 1372 public int read(final byte[] b, final int off, final int len) throws IOException { 1373 if (len == 0) { 1374 return 0; 1375 } 1376 if (max >= 0 && pos >= max) { 1377 return -1; 1378 } 1379 final long maxRead = max >= 0 ? Math.min(len, max - pos) : len; 1380 final int bytesRead = in.read(b, off, (int) maxRead); 1381 1382 if (bytesRead == -1) { 1383 return -1; 1384 } 1385 1386 pos += bytesRead; 1387 count(bytesRead); 1388 current.bytesReadFromStream += bytesRead; 1389 return bytesRead; 1390 } 1391 1392 @Override 1393 public long skip(final long n) throws IOException { 1394 final long toSkip = max >= 0 ? Math.min(n, max - pos) : n; 1395 final long skippedBytes = IOUtils.skip(in, toSkip); 1396 pos += skippedBytes; 1397 return skippedBytes; 1398 } 1399 1400 @Override 1401 public int available() throws IOException { 1402 if (max >= 0 && pos >= max) { 1403 return 0; 1404 } 1405 return in.available(); 1406 } 1407 } 1408}