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 java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.EOFException; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.PushbackInputStream; 027import java.math.BigInteger; 028import java.nio.ByteBuffer; 029import java.util.Arrays; 030import java.util.zip.CRC32; 031import java.util.zip.DataFormatException; 032import java.util.zip.Inflater; 033import java.util.zip.ZipEntry; 034import java.util.zip.ZipException; 035 036import org.apache.commons.compress.archivers.ArchiveEntry; 037import org.apache.commons.compress.archivers.ArchiveInputStream; 038import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 039import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream; 040import org.apache.commons.compress.utils.ArchiveUtils; 041import org.apache.commons.compress.utils.IOUtils; 042import org.apache.commons.compress.utils.InputStreamStatistics; 043 044import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD; 045import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT; 046import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD; 047import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC; 048 049/** 050 * Implements an input stream that can read Zip archives. 051 * 052 * <p>As of Apache Commons Compress it transparently supports Zip64 053 * extensions and thus individual entries and archives larger than 4 054 * GB or with more than 65536 entries.</p> 055 * 056 * <p>The {@link ZipFile} class is preferred when reading from files 057 * as {@link ZipArchiveInputStream} is limited by not being able to 058 * read the central directory header before returning entries. In 059 * particular {@link ZipArchiveInputStream}</p> 060 * 061 * <ul> 062 * 063 * <li>may return entries that are not part of the central directory 064 * at all and shouldn't be considered part of the archive.</li> 065 * 066 * <li>may return several entries with the same name.</li> 067 * 068 * <li>will not return internal or external attributes.</li> 069 * 070 * <li>may return incomplete extra field data.</li> 071 * 072 * <li>may return unknown sizes and CRC values for entries until the 073 * next entry has been reached if the archive uses the data 074 * descriptor feature.</li> 075 * 076 * </ul> 077 * 078 * @see ZipFile 079 * @NotThreadSafe 080 */ 081public class ZipArchiveInputStream extends ArchiveInputStream implements InputStreamStatistics { 082 083 /** The zip encoding to use for file names and the file comment. */ 084 private final ZipEncoding zipEncoding; 085 086 // the provided encoding (for unit tests) 087 final String encoding; 088 089 /** Whether to look for and use Unicode extra fields. */ 090 private final boolean useUnicodeExtraFields; 091 092 /** Wrapped stream, will always be a PushbackInputStream. */ 093 private final InputStream in; 094 095 /** Inflater used for all deflated entries. */ 096 private final Inflater inf = new Inflater(true); 097 098 /** Buffer used to read from the wrapped stream. */ 099 private final ByteBuffer buf = ByteBuffer.allocate(ZipArchiveOutputStream.BUFFER_SIZE); 100 101 /** The entry that is currently being read. */ 102 private CurrentEntry current; 103 104 /** Whether the stream has been closed. */ 105 private boolean closed; 106 107 /** Whether the stream has reached the central directory - and thus found all entries. */ 108 private boolean hitCentralDirectory; 109 110 /** 111 * When reading a stored entry that uses the data descriptor this 112 * stream has to read the full entry and caches it. This is the 113 * cache. 114 */ 115 private ByteArrayInputStream lastStoredEntry; 116 117 /** 118 * Whether the stream will try to read STORED entries that use a data descriptor. 119 * Setting it to true means we will not stop reading a entry with the compressed 120 * size, instead we will stoping reading a entry when a data descriptor is met(by 121 * finding the Data Descriptor Signature). This will completely break down in some 122 * cases - like JARs in WARs. 123 * <p> 124 * See also : 125 * https://issues.apache.org/jira/projects/COMPRESS/issues/COMPRESS-555 126 * https://github.com/apache/commons-compress/pull/137#issuecomment-690835644 127 */ 128 private boolean allowStoredEntriesWithDataDescriptor; 129 130 /** Count decompressed bytes for current entry */ 131 private long uncompressedCount; 132 133 /** Whether the stream will try to skip the zip split signature(08074B50) at the beginning **/ 134 private final boolean skipSplitSig; 135 136 private static final int LFH_LEN = 30; 137 /* 138 local file header signature WORD 139 version needed to extract SHORT 140 general purpose bit flag SHORT 141 compression method SHORT 142 last mod file time SHORT 143 last mod file date SHORT 144 crc-32 WORD 145 compressed size WORD 146 uncompressed size WORD 147 file name length SHORT 148 extra field length SHORT 149 */ 150 151 private static final int CFH_LEN = 46; 152 /* 153 central file header signature WORD 154 version made by SHORT 155 version needed to extract SHORT 156 general purpose bit flag SHORT 157 compression method SHORT 158 last mod file time SHORT 159 last mod file date SHORT 160 crc-32 WORD 161 compressed size WORD 162 uncompressed size WORD 163 file name length SHORT 164 extra field length SHORT 165 file comment length SHORT 166 disk number start SHORT 167 internal file attributes SHORT 168 external file attributes WORD 169 relative offset of local header WORD 170 */ 171 172 private static final long TWO_EXP_32 = ZIP64_MAGIC + 1; 173 174 // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) 175 private final byte[] lfhBuf = new byte[LFH_LEN]; 176 private final byte[] skipBuf = new byte[1024]; 177 private final byte[] shortBuf = new byte[SHORT]; 178 private final byte[] wordBuf = new byte[WORD]; 179 private final byte[] twoDwordBuf = new byte[2 * DWORD]; 180 181 private int entriesRead; 182 183 /** 184 * Create an instance using UTF-8 encoding 185 * @param inputStream the stream to wrap 186 */ 187 public ZipArchiveInputStream(final InputStream inputStream) { 188 this(inputStream, ZipEncodingHelper.UTF8); 189 } 190 191 /** 192 * Create an instance using the specified encoding 193 * @param inputStream the stream to wrap 194 * @param encoding the encoding to use for file names, use null 195 * for the platform's default encoding 196 * @since 1.5 197 */ 198 public ZipArchiveInputStream(final InputStream inputStream, final String encoding) { 199 this(inputStream, encoding, true); 200 } 201 202 /** 203 * Create an instance using the specified encoding 204 * @param inputStream the stream to wrap 205 * @param encoding the encoding to use for file names, use null 206 * for the platform's default encoding 207 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 208 * Extra Fields (if present) to set the file names. 209 */ 210 public ZipArchiveInputStream(final InputStream inputStream, final String encoding, final boolean useUnicodeExtraFields) { 211 this(inputStream, encoding, useUnicodeExtraFields, false); 212 } 213 214 /** 215 * Create an instance using the specified encoding 216 * @param inputStream the stream to wrap 217 * @param encoding the encoding to use for file names, use null 218 * for the platform's default encoding 219 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 220 * Extra Fields (if present) to set the file names. 221 * @param allowStoredEntriesWithDataDescriptor whether the stream 222 * will try to read STORED entries that use a data descriptor 223 * @since 1.1 224 */ 225 public ZipArchiveInputStream(final InputStream inputStream, 226 final String encoding, 227 final boolean useUnicodeExtraFields, 228 final boolean allowStoredEntriesWithDataDescriptor) { 229 this(inputStream, encoding, useUnicodeExtraFields, allowStoredEntriesWithDataDescriptor, false); 230 } 231 232 /** 233 * Create an instance using the specified encoding 234 * @param inputStream the stream to wrap 235 * @param encoding the encoding to use for file names, use null 236 * for the platform's default encoding 237 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 238 * Extra Fields (if present) to set the file names. 239 * @param allowStoredEntriesWithDataDescriptor whether the stream 240 * will try to read STORED entries that use a data descriptor 241 * @param skipSplitSig Whether the stream will try to skip the zip 242 * split signature(08074B50) at the beginning. You will need to 243 * set this to true if you want to read a split archive. 244 * @since 1.20 245 */ 246 public ZipArchiveInputStream(final InputStream inputStream, 247 final String encoding, 248 final boolean useUnicodeExtraFields, 249 final boolean allowStoredEntriesWithDataDescriptor, 250 final boolean skipSplitSig) { 251 this.encoding = encoding; 252 zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 253 this.useUnicodeExtraFields = useUnicodeExtraFields; 254 in = new PushbackInputStream(inputStream, buf.capacity()); 255 this.allowStoredEntriesWithDataDescriptor = 256 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(in, current.entry.getCompressedSize()); 371 switch (m) { 372 case UNSHRINKING: 373 current.in = new UnshrinkingInputStream(bis); 374 break; 375 case IMPLODING: 376 try { 377 current.in = 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.in = new BZip2CompressorInputStream(bis); 387 break; 388 case ENHANCED_DEFLATED: 389 current.in = 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.in = new Deflate64CompressorInputStream(in); 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.in.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 @Override 551 public long getCompressedCount() { 552 if (current.entry.getMethod() == ZipArchiveOutputStream.STORED) { 553 return current.bytesRead; 554 } 555 if (current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED) { 556 return getBytesInflated(); 557 } 558 if (current.entry.getMethod() == ZipMethod.UNSHRINKING.getCode()) { 559 return ((UnshrinkingInputStream) current.in).getCompressedCount(); 560 } 561 if (current.entry.getMethod() == ZipMethod.IMPLODING.getCode()) { 562 return ((ExplodingInputStream) current.in).getCompressedCount(); 563 } 564 if (current.entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode()) { 565 return ((Deflate64CompressorInputStream) current.in).getCompressedCount(); 566 } 567 if (current.entry.getMethod() == ZipMethod.BZIP2.getCode()) { 568 return ((BZip2CompressorInputStream) current.in).getCompressedCount(); 569 } 570 return -1; 571 } 572 573 /** 574 * @since 1.17 575 */ 576 @Override 577 public long getUncompressedCount() { 578 return uncompressedCount; 579 } 580 581 /** 582 * Implementation of read for STORED entries. 583 */ 584 private int readStored(final byte[] buffer, final int offset, final int length) throws IOException { 585 586 if (current.hasDataDescriptor) { 587 if (lastStoredEntry == null) { 588 readStoredEntry(); 589 } 590 return lastStoredEntry.read(buffer, offset, length); 591 } 592 593 final long csize = current.entry.getSize(); 594 if (current.bytesRead >= csize) { 595 return -1; 596 } 597 598 if (buf.position() >= buf.limit()) { 599 buf.position(0); 600 final int l = in.read(buf.array()); 601 if (l == -1) { 602 buf.limit(0); 603 throw new IOException("Truncated ZIP file"); 604 } 605 buf.limit(l); 606 607 count(l); 608 current.bytesReadFromStream += l; 609 } 610 611 int toRead = Math.min(buf.remaining(), length); 612 if ((csize - current.bytesRead) < toRead) { 613 // if it is smaller than toRead then it fits into an int 614 toRead = (int) (csize - current.bytesRead); 615 } 616 buf.get(buffer, offset, toRead); 617 current.bytesRead += toRead; 618 return toRead; 619 } 620 621 /** 622 * Implementation of read for DEFLATED entries. 623 */ 624 private int readDeflated(final byte[] buffer, final int offset, final int length) throws IOException { 625 final int read = readFromInflater(buffer, offset, length); 626 if (read <= 0) { 627 if (inf.finished()) { 628 return -1; 629 } 630 if (inf.needsDictionary()) { 631 throw new ZipException("This archive needs a preset dictionary" 632 + " which is not supported by Commons" 633 + " Compress."); 634 } 635 if (read == -1) { 636 throw new IOException("Truncated ZIP file"); 637 } 638 } 639 return read; 640 } 641 642 /** 643 * Potentially reads more bytes to fill the inflater's buffer and 644 * reads from it. 645 */ 646 private int readFromInflater(final byte[] buffer, final int offset, final int length) throws IOException { 647 int read = 0; 648 do { 649 if (inf.needsInput()) { 650 final int l = fill(); 651 if (l > 0) { 652 current.bytesReadFromStream += buf.limit(); 653 } else if (l == -1) { 654 return -1; 655 } else { 656 break; 657 } 658 } 659 try { 660 read = inf.inflate(buffer, offset, length); 661 } catch (final DataFormatException e) { 662 throw (IOException) new ZipException(e.getMessage()).initCause(e); 663 } 664 } while (read == 0 && inf.needsInput()); 665 return read; 666 } 667 668 @Override 669 public void close() throws IOException { 670 if (!closed) { 671 closed = true; 672 try { 673 in.close(); 674 } finally { 675 inf.end(); 676 } 677 } 678 } 679 680 /** 681 * Skips over and discards value bytes of data from this input 682 * stream. 683 * 684 * <p>This implementation may end up skipping over some smaller 685 * number of bytes, possibly 0, if and only if it reaches the end 686 * of the underlying stream.</p> 687 * 688 * <p>The actual number of bytes skipped is returned.</p> 689 * 690 * @param value the number of bytes to be skipped. 691 * @return the actual number of bytes skipped. 692 * @throws IOException - if an I/O error occurs. 693 * @throws IllegalArgumentException - if value is negative. 694 */ 695 @Override 696 public long skip(final long value) throws IOException { 697 if (value >= 0) { 698 long skipped = 0; 699 while (skipped < value) { 700 final long rem = value - skipped; 701 final int x = read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); 702 if (x == -1) { 703 return skipped; 704 } 705 skipped += x; 706 } 707 return skipped; 708 } 709 throw new IllegalArgumentException(); 710 } 711 712 /** 713 * Checks if the signature matches what is expected for a zip file. 714 * Does not currently handle self-extracting zips which may have arbitrary 715 * leading content. 716 * 717 * @param signature the bytes to check 718 * @param length the number of bytes to check 719 * @return true, if this stream is a zip archive stream, false otherwise 720 */ 721 public static boolean matches(final byte[] signature, final int length) { 722 if (length < ZipArchiveOutputStream.LFH_SIG.length) { 723 return false; 724 } 725 726 return checksig(signature, ZipArchiveOutputStream.LFH_SIG) // normal file 727 || checksig(signature, ZipArchiveOutputStream.EOCD_SIG) // empty zip 728 || checksig(signature, ZipArchiveOutputStream.DD_SIG) // split zip 729 || checksig(signature, ZipLong.SINGLE_SEGMENT_SPLIT_MARKER.getBytes()); 730 } 731 732 private static boolean checksig(final byte[] signature, final byte[] expected) { 733 for (int i = 0; i < expected.length; i++) { 734 if (signature[i] != expected[i]) { 735 return false; 736 } 737 } 738 return true; 739 } 740 741 /** 742 * Closes the current ZIP archive entry and positions the underlying 743 * stream to the beginning of the next entry. All per-entry variables 744 * and data structures are cleared. 745 * <p> 746 * If the compressed size of this entry is included in the entry header, 747 * then any outstanding bytes are simply skipped from the underlying 748 * stream without uncompressing them. This allows an entry to be safely 749 * closed even if the compression method is unsupported. 750 * <p> 751 * In case we don't know the compressed size of this entry or have 752 * already buffered too much data from the underlying stream to support 753 * uncompression, then the uncompression process is completed and the 754 * end position of the stream is adjusted based on the result of that 755 * process. 756 * 757 * @throws IOException if an error occurs 758 */ 759 private void closeEntry() throws IOException { 760 if (closed) { 761 throw new IOException("The stream is closed"); 762 } 763 if (current == null) { 764 return; 765 } 766 767 // Ensure all entry bytes are read 768 if (currentEntryHasOutstandingBytes()) { 769 drainCurrentEntryData(); 770 } else { 771 // this is guaranteed to exhaust the stream 772 skip(Long.MAX_VALUE); //NOSONAR 773 774 final long inB = current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED 775 ? getBytesInflated() : current.bytesRead; 776 777 // this is at most a single read() operation and can't 778 // exceed the range of int 779 final int diff = (int) (current.bytesReadFromStream - inB); 780 781 // Pushback any required bytes 782 if (diff > 0) { 783 pushback(buf.array(), buf.limit() - diff, diff); 784 current.bytesReadFromStream -= diff; 785 } 786 787 // Drain remainder of entry if not all data bytes were required 788 if (currentEntryHasOutstandingBytes()) { 789 drainCurrentEntryData(); 790 } 791 } 792 793 if (lastStoredEntry == null && current.hasDataDescriptor) { 794 readDataDescriptor(); 795 } 796 797 inf.reset(); 798 buf.clear().flip(); 799 current = null; 800 lastStoredEntry = null; 801 } 802 803 /** 804 * If the compressed size of the current entry is included in the entry header 805 * and there are any outstanding bytes in the underlying stream, then 806 * this returns true. 807 * 808 * @return true, if current entry is determined to have outstanding bytes, false otherwise 809 */ 810 private boolean currentEntryHasOutstandingBytes() { 811 return current.bytesReadFromStream <= current.entry.getCompressedSize() 812 && !current.hasDataDescriptor; 813 } 814 815 /** 816 * Read all data of the current entry from the underlying stream 817 * that hasn't been read, yet. 818 */ 819 private void drainCurrentEntryData() throws IOException { 820 long remaining = current.entry.getCompressedSize() - current.bytesReadFromStream; 821 while (remaining > 0) { 822 final long n = in.read(buf.array(), 0, (int) Math.min(buf.capacity(), remaining)); 823 if (n < 0) { 824 throw new EOFException("Truncated ZIP entry: " 825 + ArchiveUtils.sanitize(current.entry.getName())); 826 } 827 count(n); 828 remaining -= n; 829 } 830 } 831 832 /** 833 * Get the number of bytes Inflater has actually processed. 834 * 835 * <p>for Java < Java7 the getBytes* methods in 836 * Inflater/Deflater seem to return unsigned ints rather than 837 * longs that start over with 0 at 2^32.</p> 838 * 839 * <p>The stream knows how many bytes it has read, but not how 840 * many the Inflater actually consumed - it should be between the 841 * total number of bytes read for the entry and the total number 842 * minus the last read operation. Here we just try to make the 843 * value close enough to the bytes we've read by assuming the 844 * number of bytes consumed must be smaller than (or equal to) the 845 * number of bytes read but not smaller by more than 2^32.</p> 846 */ 847 private long getBytesInflated() { 848 long inB = inf.getBytesRead(); 849 if (current.bytesReadFromStream >= TWO_EXP_32) { 850 while (inB + TWO_EXP_32 <= current.bytesReadFromStream) { 851 inB += TWO_EXP_32; 852 } 853 } 854 return inB; 855 } 856 857 private int fill() throws IOException { 858 if (closed) { 859 throw new IOException("The stream is closed"); 860 } 861 final int length = in.read(buf.array()); 862 if (length > 0) { 863 buf.limit(length); 864 count(buf.limit()); 865 inf.setInput(buf.array(), 0, buf.limit()); 866 } 867 return length; 868 } 869 870 private void readFully(final byte[] b) throws IOException { 871 readFully(b, 0); 872 } 873 874 private void readFully(final byte[] b, final int off) throws IOException { 875 final int len = b.length - off; 876 final int count = IOUtils.readFully(in, b, off, len); 877 count(count); 878 if (count < len) { 879 throw new EOFException(); 880 } 881 } 882 883 private byte[] readRange(int len) throws IOException { 884 final byte[] ret = IOUtils.readRange(in, len); 885 count(ret.length); 886 if (ret.length < len) { 887 throw new EOFException(); 888 } 889 return ret; 890 } 891 892 private void readDataDescriptor() throws IOException { 893 readFully(wordBuf); 894 ZipLong val = new ZipLong(wordBuf); 895 if (ZipLong.DD_SIG.equals(val)) { 896 // data descriptor with signature, skip sig 897 readFully(wordBuf); 898 val = new ZipLong(wordBuf); 899 } 900 current.entry.setCrc(val.getValue()); 901 902 // if there is a ZIP64 extra field, sizes are eight bytes 903 // each, otherwise four bytes each. Unfortunately some 904 // implementations - namely Java7 - use eight bytes without 905 // using a ZIP64 extra field - 906 // https://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588 907 908 // just read 16 bytes and check whether bytes nine to twelve 909 // look like one of the signatures of what could follow a data 910 // descriptor (ignoring archive decryption headers for now). 911 // If so, push back eight bytes and assume sizes are four 912 // bytes, otherwise sizes are eight bytes each. 913 readFully(twoDwordBuf); 914 final ZipLong potentialSig = new ZipLong(twoDwordBuf, DWORD); 915 if (potentialSig.equals(ZipLong.CFH_SIG) || potentialSig.equals(ZipLong.LFH_SIG)) { 916 pushback(twoDwordBuf, DWORD, DWORD); 917 long size = ZipLong.getValue(twoDwordBuf); 918 if (size < 0) { 919 throw new ZipException("broken archive, entry with negative compressed size"); 920 } 921 current.entry.setCompressedSize(size); 922 size = ZipLong.getValue(twoDwordBuf, WORD); 923 if (size < 0) { 924 throw new ZipException("broken archive, entry with negative size"); 925 } 926 current.entry.setSize(size); 927 } else { 928 long size = ZipEightByteInteger.getLongValue(twoDwordBuf); 929 if (size < 0) { 930 throw new ZipException("broken archive, entry with negative compressed size"); 931 } 932 current.entry.setCompressedSize(size); 933 size = ZipEightByteInteger.getLongValue(twoDwordBuf, DWORD); 934 if (size < 0) { 935 throw new ZipException("broken archive, entry with negative size"); 936 } 937 current.entry.setSize(size); 938 } 939 } 940 941 /** 942 * Whether this entry requires a data descriptor this library can work with. 943 * 944 * @return true if allowStoredEntriesWithDataDescriptor is true, 945 * the entry doesn't require any data descriptor or the method is 946 * DEFLATED or ENHANCED_DEFLATED. 947 */ 948 private boolean supportsDataDescriptorFor(final ZipArchiveEntry entry) { 949 return !entry.getGeneralPurposeBit().usesDataDescriptor() 950 951 || (allowStoredEntriesWithDataDescriptor && entry.getMethod() == ZipEntry.STORED) 952 || entry.getMethod() == ZipEntry.DEFLATED 953 || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode(); 954 } 955 956 /** 957 * Whether the compressed size for the entry is either known or 958 * not required by the compression method being used. 959 */ 960 private boolean supportsCompressedSizeFor(final ZipArchiveEntry entry) { 961 return entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN 962 || entry.getMethod() == ZipEntry.DEFLATED 963 || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() 964 || (entry.getGeneralPurposeBit().usesDataDescriptor() 965 && allowStoredEntriesWithDataDescriptor 966 && entry.getMethod() == ZipEntry.STORED); 967 } 968 969 private static final String USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER = 970 " while reading a stored entry using data descriptor. Either the archive is broken" 971 + " or it can not be read using ZipArchiveInputStream and you must use ZipFile." 972 + " A common cause for this is a ZIP archive containing a ZIP archive." 973 + " See http://commons.apache.org/proper/commons-compress/zip.html#ZipArchiveInputStream_vs_ZipFile"; 974 975 /** 976 * Caches a stored entry that uses the data descriptor. 977 * 978 * <ul> 979 * <li>Reads a stored entry until the signature of a local file 980 * header, central directory header or data descriptor has been 981 * found.</li> 982 * <li>Stores all entry data in lastStoredEntry.</p> 983 * <li>Rewinds the stream to position at the data 984 * descriptor.</li> 985 * <li>reads the data descriptor</li> 986 * </ul> 987 * 988 * <p>After calling this method the entry should know its size, 989 * the entry's data is cached and the stream is positioned at the 990 * next local file or central directory header.</p> 991 */ 992 private void readStoredEntry() throws IOException { 993 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 994 int off = 0; 995 boolean done = false; 996 997 // length of DD without signature 998 final int ddLen = current.usesZip64 ? WORD + 2 * DWORD : 3 * WORD; 999 1000 while (!done) { 1001 final int r = in.read(buf.array(), off, ZipArchiveOutputStream.BUFFER_SIZE - off); 1002 if (r <= 0) { 1003 // read the whole archive without ever finding a 1004 // central directory 1005 throw new IOException("Truncated ZIP file"); 1006 } 1007 if (r + off < 4) { 1008 // buffer too small to check for a signature, loop 1009 off += r; 1010 continue; 1011 } 1012 1013 done = bufferContainsSignature(bos, off, r, ddLen); 1014 if (!done) { 1015 off = cacheBytesRead(bos, off, r, ddLen); 1016 } 1017 } 1018 if (current.entry.getCompressedSize() != current.entry.getSize()) { 1019 throw new ZipException("compressed and uncompressed size don't match" 1020 + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); 1021 } 1022 final byte[] b = bos.toByteArray(); 1023 if (b.length != current.entry.getSize()) { 1024 throw new ZipException("actual and claimed size don't match" 1025 + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); 1026 } 1027 lastStoredEntry = new ByteArrayInputStream(b); 1028 } 1029 1030 private static final byte[] LFH = ZipLong.LFH_SIG.getBytes(); 1031 private static final byte[] CFH = ZipLong.CFH_SIG.getBytes(); 1032 private static final byte[] DD = ZipLong.DD_SIG.getBytes(); 1033 1034 /** 1035 * Checks whether the current buffer contains the signature of a 1036 * "data descriptor", "local file header" or 1037 * "central directory entry". 1038 * 1039 * <p>If it contains such a signature, reads the data descriptor 1040 * and positions the stream right after the data descriptor.</p> 1041 */ 1042 private boolean bufferContainsSignature(final ByteArrayOutputStream bos, final int offset, final int lastRead, final int expectedDDLen) 1043 throws IOException { 1044 1045 boolean done = false; 1046 for (int i = 0; !done && i < offset + lastRead - 4; i++) { 1047 if (buf.array()[i] == LFH[0] && buf.array()[i + 1] == LFH[1]) { 1048 int expectDDPos = i; 1049 if (i >= expectedDDLen && 1050 (buf.array()[i + 2] == LFH[2] && buf.array()[i + 3] == LFH[3]) 1051 || (buf.array()[i + 2] == CFH[2] && buf.array()[i + 3] == CFH[3])) { 1052 // found a LFH or CFH: 1053 expectDDPos = i - expectedDDLen; 1054 done = true; 1055 } 1056 else if (buf.array()[i + 2] == DD[2] && buf.array()[i + 3] == DD[3]) { 1057 // found DD: 1058 done = true; 1059 } 1060 if (done) { 1061 // * push back bytes read in excess as well as the data 1062 // descriptor 1063 // * copy the remaining bytes to cache 1064 // * read data descriptor 1065 pushback(buf.array(), expectDDPos, offset + lastRead - expectDDPos); 1066 bos.write(buf.array(), 0, expectDDPos); 1067 readDataDescriptor(); 1068 } 1069 } 1070 } 1071 return done; 1072 } 1073 1074 /** 1075 * If the last read bytes could hold a data descriptor and an 1076 * incomplete signature then save the last bytes to the front of 1077 * the buffer and cache everything in front of the potential data 1078 * descriptor into the given ByteArrayOutputStream. 1079 * 1080 * <p>Data descriptor plus incomplete signature (3 bytes in the 1081 * worst case) can be 20 bytes max.</p> 1082 */ 1083 private int cacheBytesRead(final ByteArrayOutputStream bos, int offset, final int lastRead, final int expecteDDLen) { 1084 final int cacheable = offset + lastRead - expecteDDLen - 3; 1085 if (cacheable > 0) { 1086 bos.write(buf.array(), 0, cacheable); 1087 System.arraycopy(buf.array(), cacheable, buf.array(), 0, expecteDDLen + 3); 1088 offset = expecteDDLen + 3; 1089 } else { 1090 offset += lastRead; 1091 } 1092 return offset; 1093 } 1094 1095 private void pushback(final byte[] buf, final int offset, final int length) throws IOException { 1096 ((PushbackInputStream) in).unread(buf, offset, length); 1097 pushedBackBytes(length); 1098 } 1099 1100 // End of Central Directory Record 1101 // end of central dir signature WORD 1102 // number of this disk SHORT 1103 // number of the disk with the 1104 // start of the central directory SHORT 1105 // total number of entries in the 1106 // central directory on this disk SHORT 1107 // total number of entries in 1108 // the central directory SHORT 1109 // size of the central directory WORD 1110 // offset of start of central 1111 // directory with respect to 1112 // the starting disk number WORD 1113 // .ZIP file comment length SHORT 1114 // .ZIP file comment up to 64KB 1115 // 1116 1117 /** 1118 * Reads the stream until it find the "End of central directory 1119 * record" and consumes it as well. 1120 */ 1121 private void skipRemainderOfArchive() throws IOException { 1122 // skip over central directory. One LFH has been read too much 1123 // already. The calculation discounts file names and extra 1124 // data so it will be too short. 1125 if (entriesRead > 0) { 1126 realSkip((long) entriesRead * CFH_LEN - LFH_LEN); 1127 final boolean foundEocd = findEocdRecord(); 1128 if (foundEocd) { 1129 realSkip((long) ZipFile.MIN_EOCD_SIZE - WORD /* signature */ - SHORT /* comment len */); 1130 readFully(shortBuf); 1131 // file comment 1132 final int commentLen = ZipShort.getValue(shortBuf); 1133 if (commentLen >= 0) { 1134 realSkip(commentLen); 1135 return; 1136 } 1137 } 1138 } 1139 throw new IOException("Truncated ZIP file"); 1140 } 1141 1142 /** 1143 * Reads forward until the signature of the "End of central 1144 * directory" record is found. 1145 */ 1146 private boolean findEocdRecord() throws IOException { 1147 int currentByte = -1; 1148 boolean skipReadCall = false; 1149 while (skipReadCall || (currentByte = readOneByte()) > -1) { 1150 skipReadCall = false; 1151 if (!isFirstByteOfEocdSig(currentByte)) { 1152 continue; 1153 } 1154 currentByte = readOneByte(); 1155 if (currentByte != ZipArchiveOutputStream.EOCD_SIG[1]) { 1156 if (currentByte == -1) { 1157 break; 1158 } 1159 skipReadCall = isFirstByteOfEocdSig(currentByte); 1160 continue; 1161 } 1162 currentByte = readOneByte(); 1163 if (currentByte != ZipArchiveOutputStream.EOCD_SIG[2]) { 1164 if (currentByte == -1) { 1165 break; 1166 } 1167 skipReadCall = isFirstByteOfEocdSig(currentByte); 1168 continue; 1169 } 1170 currentByte = readOneByte(); 1171 if (currentByte == -1) { 1172 break; 1173 } 1174 if (currentByte == ZipArchiveOutputStream.EOCD_SIG[3]) { 1175 return true; 1176 } 1177 skipReadCall = isFirstByteOfEocdSig(currentByte); 1178 } 1179 return false; 1180 } 1181 1182 /** 1183 * Skips bytes by reading from the underlying stream rather than 1184 * the (potentially inflating) archive stream - which {@link 1185 * #skip} would do. 1186 * 1187 * Also updates bytes-read counter. 1188 */ 1189 private void realSkip(final long value) throws IOException { 1190 if (value >= 0) { 1191 long skipped = 0; 1192 while (skipped < value) { 1193 final long rem = value - skipped; 1194 final int x = in.read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); 1195 if (x == -1) { 1196 return; 1197 } 1198 count(x); 1199 skipped += x; 1200 } 1201 return; 1202 } 1203 throw new IllegalArgumentException(); 1204 } 1205 1206 /** 1207 * Reads bytes by reading from the underlying stream rather than 1208 * the (potentially inflating) archive stream - which {@link #read} would do. 1209 * 1210 * Also updates bytes-read counter. 1211 */ 1212 private int readOneByte() throws IOException { 1213 final int b = in.read(); 1214 if (b != -1) { 1215 count(1); 1216 } 1217 return b; 1218 } 1219 1220 private boolean isFirstByteOfEocdSig(final int b) { 1221 return b == ZipArchiveOutputStream.EOCD_SIG[0]; 1222 } 1223 1224 private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] { 1225 'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2', 1226 }; 1227 private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); 1228 1229 /** 1230 * Checks whether this might be an APK Signing Block. 1231 * 1232 * <p>Unfortunately the APK signing block does not start with some kind of signature, it rather ends with one. It 1233 * starts with a length, so what we do is parse the suspect length, skip ahead far enough, look for the signature 1234 * and if we've found it, return true.</p> 1235 * 1236 * @param suspectLocalFileHeader the bytes read from the underlying stream in the expectation that they would hold 1237 * the local file header of the next entry. 1238 * 1239 * @return true if this looks like a APK signing block 1240 * 1241 * @see <a href="https://source.android.com/security/apksigning/v2">https://source.android.com/security/apksigning/v2</a> 1242 */ 1243 private boolean isApkSigningBlock(final byte[] suspectLocalFileHeader) throws IOException { 1244 // length of block excluding the size field itself 1245 final BigInteger len = ZipEightByteInteger.getValue(suspectLocalFileHeader); 1246 // LFH has already been read and all but the first eight bytes contain (part of) the APK signing block, 1247 // also subtract 16 bytes in order to position us at the magic string 1248 BigInteger toSkip = len.add(BigInteger.valueOf(DWORD - suspectLocalFileHeader.length 1249 - (long) APK_SIGNING_BLOCK_MAGIC.length)); 1250 final byte[] magic = new byte[APK_SIGNING_BLOCK_MAGIC.length]; 1251 1252 try { 1253 if (toSkip.signum() < 0) { 1254 // suspectLocalFileHeader contains the start of suspect magic string 1255 final int off = suspectLocalFileHeader.length + toSkip.intValue(); 1256 // length was shorter than magic length 1257 if (off < DWORD) { 1258 return false; 1259 } 1260 final int bytesInBuffer = Math.abs(toSkip.intValue()); 1261 System.arraycopy(suspectLocalFileHeader, off, magic, 0, Math.min(bytesInBuffer, magic.length)); 1262 if (bytesInBuffer < magic.length) { 1263 readFully(magic, bytesInBuffer); 1264 } 1265 } else { 1266 while (toSkip.compareTo(LONG_MAX) > 0) { 1267 realSkip(Long.MAX_VALUE); 1268 toSkip = toSkip.add(LONG_MAX.negate()); 1269 } 1270 realSkip(toSkip.longValue()); 1271 readFully(magic); 1272 } 1273 } catch (final EOFException ex) { //NOSONAR 1274 // length was invalid 1275 return false; 1276 } 1277 return Arrays.equals(magic, APK_SIGNING_BLOCK_MAGIC); 1278 } 1279 1280 /** 1281 * Structure collecting information for the entry that is 1282 * currently being read. 1283 */ 1284 private static final class CurrentEntry { 1285 1286 /** 1287 * Current ZIP entry. 1288 */ 1289 private final ZipArchiveEntry entry = new ZipArchiveEntry(); 1290 1291 /** 1292 * Does the entry use a data descriptor? 1293 */ 1294 private boolean hasDataDescriptor; 1295 1296 /** 1297 * Does the entry have a ZIP64 extended information extra field. 1298 */ 1299 private boolean usesZip64; 1300 1301 /** 1302 * Number of bytes of entry content read by the client if the 1303 * entry is STORED. 1304 */ 1305 private long bytesRead; 1306 1307 /** 1308 * Number of bytes of entry content read from the stream. 1309 * 1310 * <p>This may be more than the actual entry's length as some 1311 * stuff gets buffered up and needs to be pushed back when the 1312 * end of the entry has been reached.</p> 1313 */ 1314 private long bytesReadFromStream; 1315 1316 /** 1317 * The checksum calculated as the current entry is read. 1318 */ 1319 private final CRC32 crc = new CRC32(); 1320 1321 /** 1322 * The input stream decompressing the data for shrunk and imploded entries. 1323 */ 1324 private InputStream in; 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</code> 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}