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