001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 *
017 */
018package org.apache.commons.compress.archivers.zip;
019
020import java.io.BufferedInputStream;
021import java.io.ByteArrayInputStream;
022import java.io.Closeable;
023import java.io.EOFException;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.SequenceInputStream;
028import java.nio.ByteBuffer;
029import java.nio.channels.FileChannel;
030import java.nio.channels.SeekableByteChannel;
031import java.nio.file.Files;
032import java.nio.file.StandardOpenOption;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.Enumeration;
037import java.util.EnumSet;
038import java.util.HashMap;
039import java.util.LinkedList;
040import java.util.List;
041import java.util.Map;
042import java.util.zip.Inflater;
043import java.util.zip.ZipException;
044
045import org.apache.commons.compress.archivers.EntryStreamOffsets;
046import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
047import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream;
048import org.apache.commons.compress.utils.CountingInputStream;
049import org.apache.commons.compress.utils.IOUtils;
050import org.apache.commons.compress.utils.InputStreamStatistics;
051
052import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
053import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT;
054import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
055import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC;
056import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT;
057
058/**
059 * Replacement for <code>java.util.ZipFile</code>.
060 *
061 * <p>This class adds support for file name encodings other than UTF-8
062 * (which is required to work on ZIP files created by native zip tools
063 * and is able to skip a preamble like the one found in self
064 * extracting archives.  Furthermore it returns instances of
065 * <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
066 * instead of <code>java.util.zip.ZipEntry</code>.</p>
067 *
068 * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
069 * have to reimplement all methods anyway.  Like
070 * <code>java.util.ZipFile</code>, it uses SeekableByteChannel under the
071 * covers and supports compressed and uncompressed entries.  As of
072 * Apache Commons Compress 1.3 it also transparently supports Zip64
073 * extensions and thus individual entries and archives larger than 4
074 * GB or with more than 65536 entries.</p>
075 *
076 * <p>The method signatures mimic the ones of
077 * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
078 *
079 * <ul>
080 *   <li>There is no getName method.</li>
081 *   <li>entries has been renamed to getEntries.</li>
082 *   <li>getEntries and getEntry return
083 *   <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
084 *   instances.</li>
085 *   <li>close is allowed to throw IOException.</li>
086 * </ul>
087 *
088 */
089public class ZipFile implements Closeable {
090    private static final int HASH_SIZE = 509;
091    static final int NIBLET_MASK = 0x0f;
092    static final int BYTE_SHIFT = 8;
093    private static final int POS_0 = 0;
094    private static final int POS_1 = 1;
095    private static final int POS_2 = 2;
096    private static final int POS_3 = 3;
097    private static final byte[] ONE_ZERO_BYTE = new byte[1];
098
099    /**
100     * List of entries in the order they appear inside the central
101     * directory.
102     */
103    private final List<ZipArchiveEntry> entries =
104        new LinkedList<>();
105
106    /**
107     * Maps String to list of ZipArchiveEntrys, name -> actual entries.
108     */
109    private final Map<String, LinkedList<ZipArchiveEntry>> nameMap =
110        new HashMap<>(HASH_SIZE);
111
112    /**
113     * The encoding to use for file names and the file comment.
114     *
115     * <p>For a list of possible values see <a
116     * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
117     * Defaults to UTF-8.</p>
118     */
119    private final String encoding;
120
121    /**
122     * The zip encoding to use for file names and the file comment.
123     */
124    private final ZipEncoding zipEncoding;
125
126    /**
127     * File name of actual source.
128     */
129    private final String archiveName;
130
131    /**
132     * The actual data source.
133     */
134    private final SeekableByteChannel archive;
135
136    /**
137     * Whether to look for and use Unicode extra fields.
138     */
139    private final boolean useUnicodeExtraFields;
140
141    /**
142     * Whether the file is closed.
143     */
144    private volatile boolean closed = true;
145
146    // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection)
147    private final byte[] dwordBuf = new byte[DWORD];
148    private final byte[] wordBuf = new byte[WORD];
149    private final byte[] cfhBuf = new byte[CFH_LEN];
150    private final byte[] shortBuf = new byte[SHORT];
151    private final ByteBuffer dwordBbuf = ByteBuffer.wrap(dwordBuf);
152    private final ByteBuffer wordBbuf = ByteBuffer.wrap(wordBuf);
153    private final ByteBuffer cfhBbuf = ByteBuffer.wrap(cfhBuf);
154
155    /**
156     * Opens the given file for reading, assuming "UTF8" for file names.
157     *
158     * @param f the archive.
159     *
160     * @throws IOException if an error occurs while reading the file.
161     */
162    public ZipFile(final File f) throws IOException {
163        this(f, ZipEncodingHelper.UTF8);
164    }
165
166    /**
167     * Opens the given file for reading, assuming "UTF8".
168     *
169     * @param name name of the archive.
170     *
171     * @throws IOException if an error occurs while reading the file.
172     */
173    public ZipFile(final String name) throws IOException {
174        this(new File(name), ZipEncodingHelper.UTF8);
175    }
176
177    /**
178     * Opens the given file for reading, assuming the specified
179     * encoding for file names, scanning unicode extra fields.
180     *
181     * @param name name of the archive.
182     * @param encoding the encoding to use for file names, use null
183     * for the platform's default encoding
184     *
185     * @throws IOException if an error occurs while reading the file.
186     */
187    public ZipFile(final String name, final String encoding) throws IOException {
188        this(new File(name), encoding, true);
189    }
190
191    /**
192     * Opens the given file for reading, assuming the specified
193     * encoding for file names and scanning for unicode extra fields.
194     *
195     * @param f the archive.
196     * @param encoding the encoding to use for file names, use null
197     * for the platform's default encoding
198     *
199     * @throws IOException if an error occurs while reading the file.
200     */
201    public ZipFile(final File f, final String encoding) throws IOException {
202        this(f, encoding, true);
203    }
204
205    /**
206     * Opens the given file for reading, assuming the specified
207     * encoding for file names.
208     *
209     * @param f the archive.
210     * @param encoding the encoding to use for file names, use null
211     * for the platform's default encoding
212     * @param useUnicodeExtraFields whether to use InfoZIP Unicode
213     * Extra Fields (if present) to set the file names.
214     *
215     * @throws IOException if an error occurs while reading the file.
216     */
217    public ZipFile(final File f, final String encoding, final boolean useUnicodeExtraFields)
218        throws IOException {
219        this(f, encoding, useUnicodeExtraFields, false);
220    }
221
222    /**
223     * Opens the given file for reading, assuming the specified
224     * encoding for file names.
225     *
226     *
227     * <p>By default the central directory record and all local file headers of the archive will be read immediately
228     * which may take a considerable amount of time when the archive is big. The {@code ignoreLocalFileHeader} parameter
229     * can be set to {@code true} which restricts parsing to the central directory. Unfortunately the local file header
230     * may contain information not present inside of the central directory which will not be available when the argument
231     * is set to {@code true}. This includes the content of the Unicode extra field, so setting {@code
232     * ignoreLocalFileHeader} to {@code true} means {@code useUnicodeExtraFields} will be ignored effectively. Also
233     * {@link #getRawInputStream} is always going to return {@code null} if {@code ignoreLocalFileHeader} is {@code
234     * true}.</p>
235     *
236     * @param f the archive.
237     * @param encoding the encoding to use for file names, use null
238     * for the platform's default encoding
239     * @param useUnicodeExtraFields whether to use InfoZIP Unicode
240     * Extra Fields (if present) to set the file names.
241     * @param ignoreLocalFileHeader whether to ignore information
242     * stored inside the local file header (see the notes in this method's javadoc)
243     *
244     * @throws IOException if an error occurs while reading the file.
245     * @since 1.19
246     */
247    public ZipFile(final File f, final String encoding, final boolean useUnicodeExtraFields,
248                   final boolean ignoreLocalFileHeader)
249        throws IOException {
250        this(Files.newByteChannel(f.toPath(), EnumSet.of(StandardOpenOption.READ)),
251             f.getAbsolutePath(), encoding, useUnicodeExtraFields, true, ignoreLocalFileHeader);
252    }
253
254    /**
255     * Opens the given channel for reading, assuming "UTF8" for file names.
256     *
257     * <p>{@link
258     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
259     * allows you to read from an in-memory archive.</p>
260     *
261     * @param channel the archive.
262     *
263     * @throws IOException if an error occurs while reading the file.
264     * @since 1.13
265     */
266    public ZipFile(final SeekableByteChannel channel)
267            throws IOException {
268        this(channel, "unknown archive", ZipEncodingHelper.UTF8, true);
269    }
270
271    /**
272     * Opens the given channel for reading, assuming the specified
273     * encoding for file names.
274     *
275     * <p>{@link
276     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
277     * allows you to read from an in-memory archive.</p>
278     *
279     * @param channel the archive.
280     * @param encoding the encoding to use for file names, use null
281     * for the platform's default encoding
282     *
283     * @throws IOException if an error occurs while reading the file.
284     * @since 1.13
285     */
286    public ZipFile(final SeekableByteChannel channel, final String encoding)
287        throws IOException {
288        this(channel, "unknown archive", encoding, true);
289    }
290
291    /**
292     * Opens the given channel for reading, assuming the specified
293     * encoding for file names.
294     *
295     * <p>{@link
296     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
297     * allows you to read from an in-memory archive.</p>
298     *
299     * @param channel the archive.
300     * @param archiveName name of the archive, used for error messages only.
301     * @param encoding the encoding to use for file names, use null
302     * for the platform's default encoding
303     * @param useUnicodeExtraFields whether to use InfoZIP Unicode
304     * Extra Fields (if present) to set the file names.
305     *
306     * @throws IOException if an error occurs while reading the file.
307     * @since 1.13
308     */
309    public ZipFile(final SeekableByteChannel channel, final String archiveName,
310                   final String encoding, final boolean useUnicodeExtraFields)
311        throws IOException {
312        this(channel, archiveName, encoding, useUnicodeExtraFields, false, false);
313    }
314
315    /**
316     * Opens the given channel for reading, assuming the specified
317     * encoding for file names.
318     *
319     * <p>{@link
320     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
321     * allows you to read from an in-memory archive.</p>
322     *
323     * <p>By default the central directory record and all local file headers of the archive will be read immediately
324     * which may take a considerable amount of time when the archive is big. The {@code ignoreLocalFileHeader} parameter
325     * can be set to {@code true} which restricts parsing to the central directory. Unfortunately the local file header
326     * may contain information not present inside of the central directory which will not be available when the argument
327     * is set to {@code true}. This includes the content of the Unicode extra field, so setting {@code
328     * ignoreLocalFileHeader} to {@code true} means {@code useUnicodeExtraFields} will be ignored effectively. Also
329     * {@link #getRawInputStream} is always going to return {@code null} if {@code ignoreLocalFileHeader} is {@code
330     * true}.</p>
331     *
332     * @param channel the archive.
333     * @param archiveName name of the archive, used for error messages only.
334     * @param encoding the encoding to use for file names, use null
335     * for the platform's default encoding
336     * @param useUnicodeExtraFields whether to use InfoZIP Unicode
337     * Extra Fields (if present) to set the file names.
338     * @param ignoreLocalFileHeader whether to ignore information
339     * stored inside the local file header (see the notes in this method's javadoc)
340     *
341     * @throws IOException if an error occurs while reading the file.
342     * @since 1.19
343     */
344    public ZipFile(final SeekableByteChannel channel, final String archiveName,
345                   final String encoding, final boolean useUnicodeExtraFields,
346                   final boolean ignoreLocalFileHeader)
347        throws IOException {
348        this(channel, archiveName, encoding, useUnicodeExtraFields, false, ignoreLocalFileHeader);
349    }
350
351    private ZipFile(final SeekableByteChannel channel, final String archiveName,
352                    final String encoding, final boolean useUnicodeExtraFields,
353                    final boolean closeOnError, final boolean ignoreLocalFileHeader)
354        throws IOException {
355        this.archiveName = archiveName;
356        this.encoding = encoding;
357        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
358        this.useUnicodeExtraFields = useUnicodeExtraFields;
359        archive = channel;
360        boolean success = false;
361        try {
362            final Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag =
363                populateFromCentralDirectory();
364            if (!ignoreLocalFileHeader) {
365                resolveLocalFileHeaderData(entriesWithoutUTF8Flag);
366            }
367            fillNameMap();
368            success = true;
369        } finally {
370            closed = !success;
371            if (!success && closeOnError) {
372                IOUtils.closeQuietly(archive);
373            }
374        }
375    }
376
377    /**
378     * The encoding to use for file names and the file comment.
379     *
380     * @return null if using the platform's default character encoding.
381     */
382    public String getEncoding() {
383        return encoding;
384    }
385
386    /**
387     * Closes the archive.
388     * @throws IOException if an error occurs closing the archive.
389     */
390    @Override
391    public void close() throws IOException {
392        // this flag is only written here and read in finalize() which
393        // can never be run in parallel.
394        // no synchronization needed.
395        closed = true;
396
397        archive.close();
398    }
399
400    /**
401     * close a zipfile quietly; throw no io fault, do nothing
402     * on a null parameter
403     * @param zipfile file to close, can be null
404     */
405    public static void closeQuietly(final ZipFile zipfile) {
406        IOUtils.closeQuietly(zipfile);
407    }
408
409    /**
410     * Returns all entries.
411     *
412     * <p>Entries will be returned in the same order they appear
413     * within the archive's central directory.</p>
414     *
415     * @return all entries as {@link ZipArchiveEntry} instances
416     */
417    public Enumeration<ZipArchiveEntry> getEntries() {
418        return Collections.enumeration(entries);
419    }
420
421    /**
422     * Returns all entries in physical order.
423     *
424     * <p>Entries will be returned in the same order their contents
425     * appear within the archive.</p>
426     *
427     * @return all entries as {@link ZipArchiveEntry} instances
428     *
429     * @since 1.1
430     */
431    public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() {
432        final ZipArchiveEntry[] allEntries = entries.toArray(new ZipArchiveEntry[entries.size()]);
433        Arrays.sort(allEntries, offsetComparator);
434        return Collections.enumeration(Arrays.asList(allEntries));
435    }
436
437    /**
438     * Returns a named entry - or {@code null} if no entry by
439     * that name exists.
440     *
441     * <p>If multiple entries with the same name exist the first entry
442     * in the archive's central directory by that name is
443     * returned.</p>
444     *
445     * @param name name of the entry.
446     * @return the ZipArchiveEntry corresponding to the given name - or
447     * {@code null} if not present.
448     */
449    public ZipArchiveEntry getEntry(final String name) {
450        final LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
451        return entriesOfThatName != null ? entriesOfThatName.getFirst() : null;
452    }
453
454    /**
455     * Returns all named entries in the same order they appear within
456     * the archive's central directory.
457     *
458     * @param name name of the entry.
459     * @return the Iterable&lt;ZipArchiveEntry&gt; corresponding to the
460     * given name
461     * @since 1.6
462     */
463    public Iterable<ZipArchiveEntry> getEntries(final String name) {
464        final List<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
465        return entriesOfThatName != null ? entriesOfThatName
466            : Collections.<ZipArchiveEntry>emptyList();
467    }
468
469    /**
470     * Returns all named entries in the same order their contents
471     * appear within the archive.
472     *
473     * @param name name of the entry.
474     * @return the Iterable&lt;ZipArchiveEntry&gt; corresponding to the
475     * given name
476     * @since 1.6
477     */
478    public Iterable<ZipArchiveEntry> getEntriesInPhysicalOrder(final String name) {
479        ZipArchiveEntry[] entriesOfThatName = new ZipArchiveEntry[0];
480        if (nameMap.containsKey(name)) {
481            entriesOfThatName = nameMap.get(name).toArray(entriesOfThatName);
482            Arrays.sort(entriesOfThatName, offsetComparator);
483        }
484        return Arrays.asList(entriesOfThatName);
485    }
486
487    /**
488     * Whether this class is able to read the given entry.
489     *
490     * <p>May return false if it is set up to use encryption or a
491     * compression method that hasn't been implemented yet.</p>
492     * @since 1.1
493     * @param ze the entry
494     * @return whether this class is able to read the given entry.
495     */
496    public boolean canReadEntryData(final ZipArchiveEntry ze) {
497        return ZipUtil.canHandleEntryData(ze);
498    }
499
500    /**
501     * Expose the raw stream of the archive entry (compressed form).
502     *
503     * <p>This method does not relate to how/if we understand the payload in the
504     * stream, since we really only intend to move it on to somewhere else.</p>
505     *
506     * @param ze The entry to get the stream for
507     * @return The raw input stream containing (possibly) compressed data.
508     * @since 1.11
509     */
510    public InputStream getRawInputStream(final ZipArchiveEntry ze) {
511        if (!(ze instanceof Entry)) {
512            return null;
513        }
514        final long start = ze.getDataOffset();
515        if (start == EntryStreamOffsets.OFFSET_UNKNOWN) {
516            return null;
517        }
518        return createBoundedInputStream(start, ze.getCompressedSize());
519    }
520
521
522    /**
523     * Transfer selected entries from this zipfile to a given #ZipArchiveOutputStream.
524     * Compression and all other attributes will be as in this file.
525     * <p>This method transfers entries based on the central directory of the zip file.</p>
526     *
527     * @param target The zipArchiveOutputStream to write the entries to
528     * @param predicate A predicate that selects which entries to write
529     * @throws IOException on error
530     */
531    public void copyRawEntries(final ZipArchiveOutputStream target, final ZipArchiveEntryPredicate predicate)
532            throws IOException {
533        final Enumeration<ZipArchiveEntry> src = getEntriesInPhysicalOrder();
534        while (src.hasMoreElements()) {
535            final ZipArchiveEntry entry = src.nextElement();
536            if (predicate.test( entry)) {
537                target.addRawArchiveEntry(entry, getRawInputStream(entry));
538            }
539        }
540    }
541
542    /**
543     * Returns an InputStream for reading the contents of the given entry.
544     *
545     * @param ze the entry to get the stream for.
546     * @return a stream to read the entry from. The returned stream
547     * implements {@link InputStreamStatistics}.
548     * @throws IOException if unable to create an input stream from the zipentry
549     */
550    public InputStream getInputStream(final ZipArchiveEntry ze)
551        throws IOException {
552        if (!(ze instanceof Entry)) {
553            return null;
554        }
555        // cast validity is checked just above
556        ZipUtil.checkRequestedFeatures(ze);
557        final long start = getDataOffset(ze);
558
559        // doesn't get closed if the method is not supported - which
560        // should never happen because of the checkRequestedFeatures
561        // call above
562        final InputStream is =
563            new BufferedInputStream(createBoundedInputStream(start, ze.getCompressedSize())); //NOSONAR
564        switch (ZipMethod.getMethodByCode(ze.getMethod())) {
565            case STORED:
566                return new StoredStatisticsStream(is);
567            case UNSHRINKING:
568                return new UnshrinkingInputStream(is);
569            case IMPLODING:
570                return new ExplodingInputStream(ze.getGeneralPurposeBit().getSlidingDictionarySize(),
571                        ze.getGeneralPurposeBit().getNumberOfShannonFanoTrees(), is);
572            case DEFLATED:
573                final Inflater inflater = new Inflater(true);
574                // Inflater with nowrap=true has this odd contract for a zero padding
575                // byte following the data stream; this used to be zlib's requirement
576                // and has been fixed a long time ago, but the contract persists so
577                // we comply.
578                // https://docs.oracle.com/javase/7/docs/api/java/util/zip/Inflater.html#Inflater(boolean)
579                return new InflaterInputStreamWithStatistics(new SequenceInputStream(is, new ByteArrayInputStream(ONE_ZERO_BYTE)),
580                    inflater) {
581                    @Override
582                    public void close() throws IOException {
583                        try {
584                            super.close();
585                        } finally {
586                            inflater.end();
587                        }
588                    }
589                };
590            case BZIP2:
591                return new BZip2CompressorInputStream(is);
592            case ENHANCED_DEFLATED:
593                return new Deflate64CompressorInputStream(is);
594            case AES_ENCRYPTED:
595            case EXPANDING_LEVEL_1:
596            case EXPANDING_LEVEL_2:
597            case EXPANDING_LEVEL_3:
598            case EXPANDING_LEVEL_4:
599            case JPEG:
600            case LZMA:
601            case PKWARE_IMPLODING:
602            case PPMD:
603            case TOKENIZATION:
604            case UNKNOWN:
605            case WAVPACK:
606            case XZ:
607            default:
608                throw new UnsupportedZipFeatureException(ZipMethod.getMethodByCode(ze.getMethod()), ze);
609        }
610    }
611
612    /**
613     * <p>
614     * Convenience method to return the entry's content as a String if isUnixSymlink()
615     * returns true for it, otherwise returns null.
616     * </p>
617     *
618     * <p>This method assumes the symbolic link's file name uses the
619     * same encoding that as been specified for this ZipFile.</p>
620     *
621     * @param entry ZipArchiveEntry object that represents the symbolic link
622     * @return entry's content as a String
623     * @throws IOException problem with content's input stream
624     * @since 1.5
625     */
626    public String getUnixSymlink(final ZipArchiveEntry entry) throws IOException {
627        if (entry != null && entry.isUnixSymlink()) {
628            try (InputStream in = getInputStream(entry)) {
629                return zipEncoding.decode(IOUtils.toByteArray(in));
630            }
631        }
632        return null;
633    }
634
635    /**
636     * Ensures that the close method of this zipfile is called when
637     * there are no more references to it.
638     * @see #close()
639     */
640    @Override
641    protected void finalize() throws Throwable {
642        try {
643            if (!closed) {
644                System.err.println("Cleaning up unclosed ZipFile for archive "
645                                   + archiveName);
646                close();
647            }
648        } finally {
649            super.finalize();
650        }
651    }
652
653    /**
654     * Length of a "central directory" entry structure without file
655     * name, extra fields or comment.
656     */
657    private static final int CFH_LEN =
658        /* version made by                 */ SHORT
659        /* version needed to extract       */ + SHORT
660        /* general purpose bit flag        */ + SHORT
661        /* compression method              */ + SHORT
662        /* last mod file time              */ + SHORT
663        /* last mod file date              */ + SHORT
664        /* crc-32                          */ + WORD
665        /* compressed size                 */ + WORD
666        /* uncompressed size               */ + WORD
667        /* file name length                 */ + SHORT
668        /* extra field length              */ + SHORT
669        /* file comment length             */ + SHORT
670        /* disk number start               */ + SHORT
671        /* internal file attributes        */ + SHORT
672        /* external file attributes        */ + WORD
673        /* relative offset of local header */ + WORD;
674
675    private static final long CFH_SIG =
676        ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG);
677
678    /**
679     * Reads the central directory of the given archive and populates
680     * the internal tables with ZipArchiveEntry instances.
681     *
682     * <p>The ZipArchiveEntrys will know all data that can be obtained from
683     * the central directory alone, but not the data that requires the
684     * local file header or additional data to be read.</p>
685     *
686     * @return a map of zipentries that didn't have the language
687     * encoding flag set when read.
688     */
689    private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory()
690        throws IOException {
691        final HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag =
692            new HashMap<>();
693
694        positionAtCentralDirectory();
695
696        wordBbuf.rewind();
697        IOUtils.readFully(archive, wordBbuf);
698        long sig = ZipLong.getValue(wordBuf);
699
700        if (sig != CFH_SIG && startsWithLocalFileHeader()) {
701            throw new IOException("Central directory is empty, can't expand"
702                                  + " corrupt archive.");
703        }
704
705        while (sig == CFH_SIG) {
706            readCentralDirectoryEntry(noUTF8Flag);
707            wordBbuf.rewind();
708            IOUtils.readFully(archive, wordBbuf);
709            sig = ZipLong.getValue(wordBuf);
710        }
711        return noUTF8Flag;
712    }
713
714    /**
715     * Reads an individual entry of the central directory, creats an
716     * ZipArchiveEntry from it and adds it to the global maps.
717     *
718     * @param noUTF8Flag map used to collect entries that don't have
719     * their UTF-8 flag set and whose name will be set by data read
720     * from the local file header later.  The current entry may be
721     * added to this map.
722     */
723    private void
724        readCentralDirectoryEntry(final Map<ZipArchiveEntry, NameAndComment> noUTF8Flag)
725        throws IOException {
726        cfhBbuf.rewind();
727        IOUtils.readFully(archive, cfhBbuf);
728        int off = 0;
729        final Entry ze = new Entry();
730
731        final int versionMadeBy = ZipShort.getValue(cfhBuf, off);
732        off += SHORT;
733        ze.setVersionMadeBy(versionMadeBy);
734        ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);
735
736        ze.setVersionRequired(ZipShort.getValue(cfhBuf, off));
737        off += SHORT; // version required
738
739        final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfhBuf, off);
740        final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
741        final ZipEncoding entryEncoding =
742            hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
743        if (hasUTF8Flag) {
744            ze.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG);
745        }
746        ze.setGeneralPurposeBit(gpFlag);
747        ze.setRawFlag(ZipShort.getValue(cfhBuf, off));
748
749        off += SHORT;
750
751        //noinspection MagicConstant
752        ze.setMethod(ZipShort.getValue(cfhBuf, off));
753        off += SHORT;
754
755        final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfhBuf, off));
756        ze.setTime(time);
757        off += WORD;
758
759        ze.setCrc(ZipLong.getValue(cfhBuf, off));
760        off += WORD;
761
762        ze.setCompressedSize(ZipLong.getValue(cfhBuf, off));
763        off += WORD;
764
765        ze.setSize(ZipLong.getValue(cfhBuf, off));
766        off += WORD;
767
768        final int fileNameLen = ZipShort.getValue(cfhBuf, off);
769        off += SHORT;
770
771        final int extraLen = ZipShort.getValue(cfhBuf, off);
772        off += SHORT;
773
774        final int commentLen = ZipShort.getValue(cfhBuf, off);
775        off += SHORT;
776
777        final int diskStart = ZipShort.getValue(cfhBuf, off);
778        off += SHORT;
779
780        ze.setInternalAttributes(ZipShort.getValue(cfhBuf, off));
781        off += SHORT;
782
783        ze.setExternalAttributes(ZipLong.getValue(cfhBuf, off));
784        off += WORD;
785
786        final byte[] fileName = new byte[fileNameLen];
787        IOUtils.readFully(archive, ByteBuffer.wrap(fileName));
788        ze.setName(entryEncoding.decode(fileName), fileName);
789
790        // LFH offset,
791        ze.setLocalHeaderOffset(ZipLong.getValue(cfhBuf, off));
792        // data offset will be filled later
793        entries.add(ze);
794
795        final byte[] cdExtraData = new byte[extraLen];
796        IOUtils.readFully(archive, ByteBuffer.wrap(cdExtraData));
797        ze.setCentralDirectoryExtra(cdExtraData);
798
799        setSizesAndOffsetFromZip64Extra(ze, diskStart);
800
801        final byte[] comment = new byte[commentLen];
802        IOUtils.readFully(archive, ByteBuffer.wrap(comment));
803        ze.setComment(entryEncoding.decode(comment));
804
805        if (!hasUTF8Flag && useUnicodeExtraFields) {
806            noUTF8Flag.put(ze, new NameAndComment(fileName, comment));
807        }
808
809        ze.setStreamContiguous(true);
810    }
811
812    /**
813     * If the entry holds a Zip64 extended information extra field,
814     * read sizes from there if the entry's sizes are set to
815     * 0xFFFFFFFFF, do the same for the offset of the local file
816     * header.
817     *
818     * <p>Ensures the Zip64 extra either knows both compressed and
819     * uncompressed size or neither of both as the internal logic in
820     * ExtraFieldUtils forces the field to create local header data
821     * even if they are never used - and here a field with only one
822     * size would be invalid.</p>
823     */
824    private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze,
825                                                 final int diskStart)
826        throws IOException {
827        final Zip64ExtendedInformationExtraField z64 =
828            (Zip64ExtendedInformationExtraField)
829            ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
830        if (z64 != null) {
831            final boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC;
832            final boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC;
833            final boolean hasRelativeHeaderOffset =
834                ze.getLocalHeaderOffset() == ZIP64_MAGIC;
835            z64.reparseCentralDirectoryData(hasUncompressedSize,
836                                            hasCompressedSize,
837                                            hasRelativeHeaderOffset,
838                                            diskStart == ZIP64_MAGIC_SHORT);
839
840            if (hasUncompressedSize) {
841                ze.setSize(z64.getSize().getLongValue());
842            } else if (hasCompressedSize) {
843                z64.setSize(new ZipEightByteInteger(ze.getSize()));
844            }
845
846            if (hasCompressedSize) {
847                ze.setCompressedSize(z64.getCompressedSize().getLongValue());
848            } else if (hasUncompressedSize) {
849                z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
850            }
851
852            if (hasRelativeHeaderOffset) {
853                ze.setLocalHeaderOffset(z64.getRelativeHeaderOffset().getLongValue());
854            }
855        }
856    }
857
858    /**
859     * Length of the "End of central directory record" - which is
860     * supposed to be the last structure of the archive - without file
861     * comment.
862     */
863    static final int MIN_EOCD_SIZE =
864        /* end of central dir signature    */ WORD
865        /* number of this disk             */ + SHORT
866        /* number of the disk with the     */
867        /* start of the central directory  */ + SHORT
868        /* total number of entries in      */
869        /* the central dir on this disk    */ + SHORT
870        /* total number of entries in      */
871        /* the central dir                 */ + SHORT
872        /* size of the central directory   */ + WORD
873        /* offset of start of central      */
874        /* directory with respect to       */
875        /* the starting disk number        */ + WORD
876        /* zipfile comment length          */ + SHORT;
877
878    /**
879     * Maximum length of the "End of central directory record" with a
880     * file comment.
881     */
882    private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE
883        /* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT;
884
885    /**
886     * Offset of the field that holds the location of the first
887     * central directory entry inside the "End of central directory
888     * record" relative to the start of the "End of central directory
889     * record".
890     */
891    private static final int CFD_LOCATOR_OFFSET =
892        /* end of central dir signature    */ WORD
893        /* number of this disk             */ + SHORT
894        /* number of the disk with the     */
895        /* start of the central directory  */ + SHORT
896        /* total number of entries in      */
897        /* the central dir on this disk    */ + SHORT
898        /* total number of entries in      */
899        /* the central dir                 */ + SHORT
900        /* size of the central directory   */ + WORD;
901
902    /**
903     * Length of the "Zip64 end of central directory locator" - which
904     * should be right in front of the "end of central directory
905     * record" if one is present at all.
906     */
907    private static final int ZIP64_EOCDL_LENGTH =
908        /* zip64 end of central dir locator sig */ WORD
909        /* number of the disk with the start    */
910        /* start of the zip64 end of            */
911        /* central directory                    */ + WORD
912        /* relative offset of the zip64         */
913        /* end of central directory record      */ + DWORD
914        /* total number of disks                */ + WORD;
915
916    /**
917     * Offset of the field that holds the location of the "Zip64 end
918     * of central directory record" inside the "Zip64 end of central
919     * directory locator" relative to the start of the "Zip64 end of
920     * central directory locator".
921     */
922    private static final int ZIP64_EOCDL_LOCATOR_OFFSET =
923        /* zip64 end of central dir locator sig */ WORD
924        /* number of the disk with the start    */
925        /* start of the zip64 end of            */
926        /* central directory                    */ + WORD;
927
928    /**
929     * Offset of the field that holds the location of the first
930     * central directory entry inside the "Zip64 end of central
931     * directory record" relative to the start of the "Zip64 end of
932     * central directory record".
933     */
934    private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET =
935        /* zip64 end of central dir        */
936        /* signature                       */ WORD
937        /* size of zip64 end of central    */
938        /* directory record                */ + DWORD
939        /* version made by                 */ + SHORT
940        /* version needed to extract       */ + SHORT
941        /* number of this disk             */ + WORD
942        /* number of the disk with the     */
943        /* start of the central directory  */ + WORD
944        /* total number of entries in the  */
945        /* central directory on this disk  */ + DWORD
946        /* total number of entries in the  */
947        /* central directory               */ + DWORD
948        /* size of the central directory   */ + DWORD;
949
950    /**
951     * Searches for either the &quot;Zip64 end of central directory
952     * locator&quot; or the &quot;End of central dir record&quot;, parses
953     * it and positions the stream at the first central directory
954     * record.
955     */
956    private void positionAtCentralDirectory()
957        throws IOException {
958        positionAtEndOfCentralDirectoryRecord();
959        boolean found = false;
960        final boolean searchedForZip64EOCD =
961            archive.position() > ZIP64_EOCDL_LENGTH;
962        if (searchedForZip64EOCD) {
963            archive.position(archive.position() - ZIP64_EOCDL_LENGTH);
964            wordBbuf.rewind();
965            IOUtils.readFully(archive, wordBbuf);
966            found = Arrays.equals(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG,
967                                  wordBuf);
968        }
969        if (!found) {
970            // not a ZIP64 archive
971            if (searchedForZip64EOCD) {
972                skipBytes(ZIP64_EOCDL_LENGTH - WORD);
973            }
974            positionAtCentralDirectory32();
975        } else {
976            positionAtCentralDirectory64();
977        }
978    }
979
980    /**
981     * Parses the &quot;Zip64 end of central directory locator&quot;,
982     * finds the &quot;Zip64 end of central directory record&quot; using the
983     * parsed information, parses that and positions the stream at the
984     * first central directory record.
985     *
986     * Expects stream to be positioned right behind the &quot;Zip64
987     * end of central directory locator&quot;'s signature.
988     */
989    private void positionAtCentralDirectory64()
990        throws IOException {
991        skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET
992                  - WORD /* signature has already been read */);
993        dwordBbuf.rewind();
994        IOUtils.readFully(archive, dwordBbuf);
995        archive.position(ZipEightByteInteger.getLongValue(dwordBuf));
996        wordBbuf.rewind();
997        IOUtils.readFully(archive, wordBbuf);
998        if (!Arrays.equals(wordBuf, ZipArchiveOutputStream.ZIP64_EOCD_SIG)) {
999            throw new ZipException("Archive's ZIP64 end of central "
1000                                   + "directory locator is corrupt.");
1001        }
1002        skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET
1003                  - WORD /* signature has already been read */);
1004        dwordBbuf.rewind();
1005        IOUtils.readFully(archive, dwordBbuf);
1006        archive.position(ZipEightByteInteger.getLongValue(dwordBuf));
1007    }
1008
1009    /**
1010     * Parses the &quot;End of central dir record&quot; and positions
1011     * the stream at the first central directory record.
1012     *
1013     * Expects stream to be positioned at the beginning of the
1014     * &quot;End of central dir record&quot;.
1015     */
1016    private void positionAtCentralDirectory32()
1017        throws IOException {
1018        skipBytes(CFD_LOCATOR_OFFSET);
1019        wordBbuf.rewind();
1020        IOUtils.readFully(archive, wordBbuf);
1021        archive.position(ZipLong.getValue(wordBuf));
1022    }
1023
1024    /**
1025     * Searches for the and positions the stream at the start of the
1026     * &quot;End of central dir record&quot;.
1027     */
1028    private void positionAtEndOfCentralDirectoryRecord()
1029        throws IOException {
1030        final boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE,
1031                                             ZipArchiveOutputStream.EOCD_SIG);
1032        if (!found) {
1033            throw new ZipException("Archive is not a ZIP archive");
1034        }
1035    }
1036
1037    /**
1038     * Searches the archive backwards from minDistance to maxDistance
1039     * for the given signature, positions the RandomaccessFile right
1040     * at the signature if it has been found.
1041     */
1042    private boolean tryToLocateSignature(final long minDistanceFromEnd,
1043                                         final long maxDistanceFromEnd,
1044                                         final byte[] sig) throws IOException {
1045        boolean found = false;
1046        long off = archive.size() - minDistanceFromEnd;
1047        final long stopSearching =
1048            Math.max(0L, archive.size() - maxDistanceFromEnd);
1049        if (off >= 0) {
1050            for (; off >= stopSearching; off--) {
1051                archive.position(off);
1052                try {
1053                    wordBbuf.rewind();
1054                    IOUtils.readFully(archive, wordBbuf);
1055                    wordBbuf.flip();
1056                } catch (EOFException ex) { // NOSONAR
1057                    break;
1058                }
1059                int curr = wordBbuf.get();
1060                if (curr == sig[POS_0]) {
1061                    curr = wordBbuf.get();
1062                    if (curr == sig[POS_1]) {
1063                        curr = wordBbuf.get();
1064                        if (curr == sig[POS_2]) {
1065                            curr = wordBbuf.get();
1066                            if (curr == sig[POS_3]) {
1067                                found = true;
1068                                break;
1069                            }
1070                        }
1071                    }
1072                }
1073            }
1074        }
1075        if (found) {
1076            archive.position(off);
1077        }
1078        return found;
1079    }
1080
1081    /**
1082     * Skips the given number of bytes or throws an EOFException if
1083     * skipping failed.
1084     */
1085    private void skipBytes(final int count) throws IOException {
1086        long currentPosition = archive.position();
1087        long newPosition = currentPosition + count;
1088        if (newPosition > archive.size()) {
1089            throw new EOFException();
1090        }
1091        archive.position(newPosition);
1092    }
1093
1094    /**
1095     * Number of bytes in local file header up to the &quot;length of
1096     * file name&quot; entry.
1097     */
1098    private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
1099        /* local file header signature     */ WORD
1100        /* version needed to extract       */ + SHORT
1101        /* general purpose bit flag        */ + SHORT
1102        /* compression method              */ + SHORT
1103        /* last mod file time              */ + SHORT
1104        /* last mod file date              */ + SHORT
1105        /* crc-32                          */ + WORD
1106        /* compressed size                 */ + WORD
1107        /* uncompressed size               */ + (long) WORD;
1108
1109    /**
1110     * Walks through all recorded entries and adds the data available
1111     * from the local file header.
1112     *
1113     * <p>Also records the offsets for the data to read from the
1114     * entries.</p>
1115     */
1116    private void resolveLocalFileHeaderData(final Map<ZipArchiveEntry, NameAndComment>
1117                                            entriesWithoutUTF8Flag)
1118        throws IOException {
1119        for (final ZipArchiveEntry zipArchiveEntry : entries) {
1120            // entries is filled in populateFromCentralDirectory and
1121            // never modified
1122            final Entry ze = (Entry) zipArchiveEntry;
1123            int[] lens = setDataOffset(ze);
1124            final int fileNameLen = lens[0];
1125            final int extraFieldLen = lens[1];
1126            skipBytes(fileNameLen);
1127            final byte[] localExtraData = new byte[extraFieldLen];
1128            IOUtils.readFully(archive, ByteBuffer.wrap(localExtraData));
1129            ze.setExtra(localExtraData);
1130
1131            if (entriesWithoutUTF8Flag.containsKey(ze)) {
1132                final NameAndComment nc = entriesWithoutUTF8Flag.get(ze);
1133                ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name,
1134                                                         nc.comment);
1135            }
1136        }
1137    }
1138
1139    private void fillNameMap() {
1140        for (final ZipArchiveEntry ze : entries) {
1141            // entries is filled in populateFromCentralDirectory and
1142            // never modified
1143            final String name = ze.getName();
1144            LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
1145            if (entriesOfThatName == null) {
1146                entriesOfThatName = new LinkedList<>();
1147                nameMap.put(name, entriesOfThatName);
1148            }
1149            entriesOfThatName.addLast(ze);
1150        }
1151    }
1152
1153    private int[] setDataOffset(ZipArchiveEntry ze) throws IOException {
1154        final long offset = ze.getLocalHeaderOffset();
1155        archive.position(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
1156        wordBbuf.rewind();
1157        IOUtils.readFully(archive, wordBbuf);
1158        wordBbuf.flip();
1159        wordBbuf.get(shortBuf);
1160        final int fileNameLen = ZipShort.getValue(shortBuf);
1161        wordBbuf.get(shortBuf);
1162        final int extraFieldLen = ZipShort.getValue(shortBuf);
1163        ze.setDataOffset(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
1164                         + SHORT + SHORT + fileNameLen + extraFieldLen);
1165        return new int[] { fileNameLen, extraFieldLen };
1166    }
1167
1168    private long getDataOffset(ZipArchiveEntry ze) throws IOException {
1169        long s = ze.getDataOffset();
1170        if (s == EntryStreamOffsets.OFFSET_UNKNOWN) {
1171            setDataOffset(ze);
1172            return ze.getDataOffset();
1173        }
1174        return s;
1175    }
1176
1177    /**
1178     * Checks whether the archive starts with a LFH.  If it doesn't,
1179     * it may be an empty archive.
1180     */
1181    private boolean startsWithLocalFileHeader() throws IOException {
1182        archive.position(0);
1183        wordBbuf.rewind();
1184        IOUtils.readFully(archive, wordBbuf);
1185        return Arrays.equals(wordBuf, ZipArchiveOutputStream.LFH_SIG);
1186    }
1187
1188    /**
1189     * Creates new BoundedInputStream, according to implementation of
1190     * underlying archive channel.
1191     */
1192    private BoundedInputStream createBoundedInputStream(long start, long remaining) {
1193        return archive instanceof FileChannel ?
1194            new BoundedFileChannelInputStream(start, remaining) :
1195            new BoundedInputStream(start, remaining);
1196    }
1197
1198    /**
1199     * InputStream that delegates requests to the underlying
1200     * SeekableByteChannel, making sure that only bytes from a certain
1201     * range can be read.
1202     */
1203    private class BoundedInputStream extends InputStream {
1204        private ByteBuffer singleByteBuffer;
1205        private final long end;
1206        private long loc;
1207
1208        BoundedInputStream(final long start, final long remaining) {
1209            this.end = start+remaining;
1210            if (this.end < start) {
1211                // check for potential vulnerability due to overflow
1212                throw new IllegalArgumentException("Invalid length of stream at offset="+start+", length="+remaining);
1213            }
1214            loc = start;
1215        }
1216
1217        @Override
1218        public synchronized int read() throws IOException {
1219            if (loc >= end) {
1220                return -1;
1221            }
1222            if (singleByteBuffer == null) {
1223                singleByteBuffer = ByteBuffer.allocate(1);
1224            }
1225            else {
1226                singleByteBuffer.rewind();
1227            }
1228            int read = read(loc, singleByteBuffer);
1229            if (read < 0) {
1230                return read;
1231            }
1232            loc++;
1233            return singleByteBuffer.get() & 0xff;
1234        }
1235
1236        @Override
1237        public synchronized int read(final byte[] b, final int off, int len) throws IOException {
1238            if (len <= 0) {
1239                return 0;
1240            }
1241
1242            if (len > end-loc) {
1243                if (loc >= end) {
1244                    return -1;
1245                }
1246                len = (int)(end-loc);
1247            }
1248
1249            ByteBuffer buf;
1250            buf = ByteBuffer.wrap(b, off, len);
1251            int ret = read(loc, buf);
1252            if (ret > 0) {
1253                loc += ret;
1254                return ret;
1255            }
1256            return ret;
1257        }
1258
1259        protected int read(long pos, ByteBuffer buf) throws IOException {
1260            int read;
1261            synchronized (archive) {
1262                archive.position(pos);
1263                read = archive.read(buf);
1264            }
1265            buf.flip();
1266            return read;
1267        }
1268    }
1269
1270    /**
1271     * Lock-free implementation of BoundedInputStream. The
1272     * implementation uses positioned reads on the underlying archive
1273     * file channel and therefore performs significantly faster in
1274     * concurrent environment.
1275     */
1276    private class BoundedFileChannelInputStream extends BoundedInputStream {
1277        private final FileChannel archive;
1278
1279        BoundedFileChannelInputStream(final long start, final long remaining) {
1280            super(start, remaining);
1281            archive = (FileChannel)ZipFile.this.archive;
1282        }
1283
1284        @Override
1285        protected int read(long pos, ByteBuffer buf) throws IOException {
1286            int read = archive.read(buf, pos);
1287            buf.flip();
1288            return read;
1289        }
1290    }
1291
1292    private static final class NameAndComment {
1293        private final byte[] name;
1294        private final byte[] comment;
1295        private NameAndComment(final byte[] name, final byte[] comment) {
1296            this.name = name;
1297            this.comment = comment;
1298        }
1299    }
1300
1301    /**
1302     * Compares two ZipArchiveEntries based on their offset within the archive.
1303     *
1304     * <p>Won't return any meaningful results if one of the entries
1305     * isn't part of the archive at all.</p>
1306     *
1307     * @since 1.1
1308     */
1309    private final Comparator<ZipArchiveEntry> offsetComparator =
1310        new Comparator<ZipArchiveEntry>() {
1311        @Override
1312        public int compare(final ZipArchiveEntry e1, final ZipArchiveEntry e2) {
1313            if (e1 == e2) {
1314                return 0;
1315            }
1316
1317            final Entry ent1 = e1 instanceof Entry ? (Entry) e1 : null;
1318            final Entry ent2 = e2 instanceof Entry ? (Entry) e2 : null;
1319            if (ent1 == null) {
1320                return 1;
1321            }
1322            if (ent2 == null) {
1323                return -1;
1324            }
1325            final long val = (ent1.getLocalHeaderOffset()
1326                        - ent2.getLocalHeaderOffset());
1327            return val == 0 ? 0 : val < 0 ? -1 : +1;
1328        }
1329    };
1330
1331    /**
1332     * Extends ZipArchiveEntry to store the offset within the archive.
1333     */
1334    private static class Entry extends ZipArchiveEntry {
1335
1336        Entry() {
1337        }
1338
1339        @Override
1340        public int hashCode() {
1341            return 3 * super.hashCode()
1342                + (int) getLocalHeaderOffset()+(int)(getLocalHeaderOffset()>>32);
1343        }
1344
1345        @Override
1346        public boolean equals(final Object other) {
1347            if (super.equals(other)) {
1348                // super.equals would return false if other were not an Entry
1349                final Entry otherEntry = (Entry) other;
1350                return getLocalHeaderOffset()
1351                        == otherEntry.getLocalHeaderOffset()
1352                    && super.getDataOffset()
1353                        == otherEntry.getDataOffset();
1354            }
1355            return false;
1356        }
1357    }
1358
1359    private static class StoredStatisticsStream extends CountingInputStream implements InputStreamStatistics {
1360        StoredStatisticsStream(InputStream in) {
1361            super(in);
1362        }
1363
1364        @Override
1365        public long getCompressedCount() {
1366            return super.getBytesRead();
1367        }
1368
1369        @Override
1370        public long getUncompressedCount() {
1371            return getCompressedCount();
1372        }
1373    }
1374}