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