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