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