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