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