    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     */
    018    package org.apache.commons.compress.archivers.zip;
    019
    020    import java.io.EOFException;
    021    import java.io.File;
    022    import java.io.IOException;
    023    import java.io.InputStream;
    024    import java.io.RandomAccessFile;
    025    import java.util.Arrays;
    026    import java.util.Collections;
    027    import java.util.Comparator;
    028    import java.util.Enumeration;
    029    import java.util.HashMap;
    030    import java.util.LinkedHashMap;
    031    import java.util.Map;
    032    import java.util.zip.Inflater;
    033    import java.util.zip.InflaterInputStream;
    034    import java.util.zip.ZipException;
    035
    036    import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
    037    import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT;
    038    import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
    039    import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC;
    040    import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT;
    041
    042    /**
    043     * Replacement for <code>java.util.ZipFile</code>.
    044     *
    045     * <p>This class adds support for file name encodings other than UTF-8
    046     * (which is required to work on ZIP files created by native zip tools
    047     * and is able to skip a preamble like the one found in self
    048     * extracting archives.  Furthermore it returns instances of
    049     * <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
    050     * instead of <code>java.util.zip.ZipEntry</code>.</p>
    051     *
    052     * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
    053     * have to reimplement all methods anyway.  Like
    054     * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
    055     * covers and supports compressed and uncompressed entries.  As of
    056     * Apache Commons Compress it also transparently supports Zip64
    057     * extensions and thus individual entries and archives larger than 4
    058     * GB or with more than 65536 entries.</p>
    059     *
    060     * <p>The method signatures mimic the ones of
    061     * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
    062     *
    063     * <ul>
    064     *   <li>There is no getName method.</li>
    065     *   <li>entries has been renamed to getEntries.</li>
    066     *   <li>getEntries and getEntry return
    067     *   <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
    068     *   instances.</li>
    069     *   <li>close is allowed to throw IOException.</li>
    070     * </ul>
    071     *
    072     */
    073    public class ZipFile {
    074        private static final int HASH_SIZE = 509;
    075        static final int NIBLET_MASK = 0x0f;
    076        static final int BYTE_SHIFT = 8;
    077        private static final int POS_0 = 0;
    078        private static final int POS_1 = 1;
    079        private static final int POS_2 = 2;
    080        private static final int POS_3 = 3;
    081
    082        /**
    083         * Maps ZipArchiveEntrys to two longs, recording the offsets of
    084         * the local file headers and the start of entry data.
    085         */
    086        private final Map<ZipArchiveEntry, OffsetEntry> entries =
    087            new LinkedHashMap<ZipArchiveEntry, OffsetEntry>(HASH_SIZE);
    088
    089        /**
    090         * Maps String to ZipArchiveEntrys, name -> actual entry.
    091         */
    092        private final Map<String, ZipArchiveEntry> nameMap =
    093            new HashMap<String, ZipArchiveEntry>(HASH_SIZE);
    094
    095        private static final class OffsetEntry {
    096            private long headerOffset = -1;
    097            private long dataOffset = -1;
    098        }
    099
    100        /**
    101         * The encoding to use for filenames and the file comment.
    102         *
    103         * <p>For a list of possible values see <a
    104         * 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>.
    105         * Defaults to UTF-8.</p>
    106         */
    107        private final String encoding;
    108
    109        /**
    110         * The zip encoding to use for filenames and the file comment.
    111         */
    112        private final ZipEncoding zipEncoding;
    113
    114        /**
    115         * File name of actual source.
    116         */
    117        private final String archiveName;
    118
    119        /**
    120         * The actual data source.
    121         */
    122        private final RandomAccessFile archive;
    123
    124        /**
    125         * Whether to look for and use Unicode extra fields.
    126         */
    127        private final boolean useUnicodeExtraFields;
    128
    129        /**
    130         * Whether the file is closed.
    131         */
    132        private boolean closed;
    133
    134        /**
    135         * Opens the given file for reading, assuming "UTF8" for file names.
    136         *
    137         * @param f the archive.
    138         *
    139         * @throws IOException if an error occurs while reading the file.
    140         */
    141        public ZipFile(File f) throws IOException {
    142            this(f, ZipEncodingHelper.UTF8);
    143        }
    144
    145        /**
    146         * Opens the given file for reading, assuming "UTF8".
    147         *
    148         * @param name name of the archive.
    149         *
    150         * @throws IOException if an error occurs while reading the file.
    151         */
    152        public ZipFile(String name) throws IOException {
    153            this(new File(name), ZipEncodingHelper.UTF8);
    154        }
    155
    156        /**
    157         * Opens the given file for reading, assuming the specified
    158         * encoding for file names, scanning unicode extra fields.
    159         *
    160         * @param name name of the archive.
    161         * @param encoding the encoding to use for file names, use null
    162         * for the platform's default encoding
    163         *
    164         * @throws IOException if an error occurs while reading the file.
    165         */
    166        public ZipFile(String name, String encoding) throws IOException {
    167            this(new File(name), encoding, true);
    168        }
    169
    170        /**
    171         * Opens the given file for reading, assuming the specified
    172         * encoding for file names and scanning for unicode extra fields.
    173         *
    174         * @param f the archive.
    175         * @param encoding the encoding to use for file names, use null
    176         * for the platform's default encoding
    177         *
    178         * @throws IOException if an error occurs while reading the file.
    179         */
    180        public ZipFile(File f, String encoding) throws IOException {
    181            this(f, encoding, true);
    182        }
    183
    184        /**
    185         * Opens the given file for reading, assuming the specified
    186         * encoding for file names.
    187         *
    188         * @param f the archive.
    189         * @param encoding the encoding to use for file names, use null
    190         * for the platform's default encoding
    191         * @param useUnicodeExtraFields whether to use InfoZIP Unicode
    192         * Extra Fields (if present) to set the file names.
    193         *
    194         * @throws IOException if an error occurs while reading the file.
    195         */
    196        public ZipFile(File f, String encoding, boolean useUnicodeExtraFields)
    197            throws IOException {
    198            this.archiveName = f.getAbsolutePath();
    199            this.encoding = encoding;
    200            this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
    201            this.useUnicodeExtraFields = useUnicodeExtraFields;
    202            archive = new RandomAccessFile(f, "r");
    203            boolean success = false;
    204            try {
    205                Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag =
    206                    populateFromCentralDirectory();
    207                resolveLocalFileHeaderData(entriesWithoutUTF8Flag);
    208                success = true;
    209            } finally {
    210                if (!success) {
    211                    try {
    212                        closed = true;
    213                        archive.close();
    214                    } catch (IOException e2) { // NOPMD
    215                        // swallow, throw the original exception instead
    216                    }
    217                }
    218            }
    219        }
    220
    221        /**
    222         * The encoding to use for filenames and the file comment.
    223         *
    224         * @return null if using the platform's default character encoding.
    225         */
    226        public String getEncoding() {
    227            return encoding;
    228        }
    229
    230        /**
    231         * Closes the archive.
    232         * @throws IOException if an error occurs closing the archive.
    233         */
    234        public void close() throws IOException {
    235            // this flag is only written here and read in finalize() which
    236            // can never be run in parallel.
    237            // no synchronization needed.
    238            closed = true;
    239
    240            archive.close();
    241        }
    242
    243        /**
    244         * close a zipfile quietly; throw no io fault, do nothing
    245         * on a null parameter
    246         * @param zipfile file to close, can be null
    247         */
    248        public static void closeQuietly(ZipFile zipfile) {
    249            if (zipfile != null) {
    250                try {
    251                    zipfile.close();
    252                } catch (IOException e) { // NOPMD
    253                    //ignore, that's why the method is called "quietly"
    254                }
    255            }
    256        }
    257
    258        /**
    259         * Returns all entries.
    260         *
    261         * <p>Entries will be returned in the same order they appear
    262         * within the archive's central directory.</p>
    263         *
    264         * @return all entries as {@link ZipArchiveEntry} instances
    265         */
    266        public Enumeration<ZipArchiveEntry> getEntries() {
    267            return Collections.enumeration(entries.keySet());
    268        }
    269
    270        /**
    271         * Returns all entries in physical order.
    272         *
    273         * <p>Entries will be returned in the same order their contents
    274         * appear within the archive.</p>
    275         *
    276         * @return all entries as {@link ZipArchiveEntry} instances
    277         *
    278         * @since Commons Compress 1.1
    279         */
    280        public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() {
    281            ZipArchiveEntry[] allEntries =
    282                entries.keySet().toArray(new ZipArchiveEntry[0]);
    283            Arrays.sort(allEntries, OFFSET_COMPARATOR);
    284            return Collections.enumeration(Arrays.asList(allEntries));
    285        }
    286
    287        /**
    288         * Returns a named entry - or <code>null</code> if no entry by
    289         * that name exists.
    290         * @param name name of the entry.
    291         * @return the ZipArchiveEntry corresponding to the given name - or
    292         * <code>null</code> if not present.
    293         */
    294        public ZipArchiveEntry getEntry(String name) {
    295            return nameMap.get(name);
    296        }
    297
    298        /**
    299         * Whether this class is able to read the given entry.
    300         *
    301         * <p>May return false if it is set up to use encryption or a
    302         * compression method that hasn't been implemented yet.</p>
    303         * @since Apache Commons Compress 1.1
    304         */
    305        public boolean canReadEntryData(ZipArchiveEntry ze) {
    306            return ZipUtil.canHandleEntryData(ze);
    307        }
    308
    309        /**
    310         * Returns an InputStream for reading the contents of the given entry.
    311         *
    312         * @param ze the entry to get the stream for.
    313         * @return a stream to read the entry from.
    314         * @throws IOException if unable to create an input stream from the zipenty
    315         * @throws ZipException if the zipentry uses an unsupported feature
    316         */
    317        public InputStream getInputStream(ZipArchiveEntry ze)
    318            throws IOException, ZipException {
    319            OffsetEntry offsetEntry = entries.get(ze);
    320            if (offsetEntry == null) {
    321                return null;
    322            }
    323            ZipUtil.checkRequestedFeatures(ze);
    324            long start = offsetEntry.dataOffset;
    325            BoundedInputStream bis =
    326                new BoundedInputStream(start, ze.getCompressedSize());
    327            switch (ze.getMethod()) {
    328                case ZipArchiveEntry.STORED:
    329                    return bis;
    330                case ZipArchiveEntry.DEFLATED:
    331                    bis.addDummy();
    332                    final Inflater inflater = new Inflater(true);
    333                    return new InflaterInputStream(bis, inflater) {
    334                        @Override
    335                        public void close() throws IOException {
    336                            super.close();
    337                            inflater.end();
    338                        }
    339                    };
    340                default:
    341                    throw new ZipException("Found unsupported compression method "
    342                                           + ze.getMethod());
    343            }
    344        }
    345
    346        /**
    347         * Ensures that the close method of this zipfile is called when
    348         * there are no more references to it.
    349         * @see #close()
    350         */
    351        @Override
    352        protected void finalize() throws Throwable {
    353            try {
    354                if (!closed) {
    355                    System.err.println("Cleaning up unclosed ZipFile for archive "
    356                                       + archiveName);
    357                    close();
    358                }
    359            } finally {
    360                super.finalize();
    361            }
    362        }
    363
    364        /**
    365         * Length of a "central directory" entry structure without file
    366         * name, extra fields or comment.
    367         */
    368        private static final int CFH_LEN =
    369            /* version made by                 */ SHORT
    370            /* version needed to extract       */ + SHORT
    371            /* general purpose bit flag        */ + SHORT
    372            /* compression method              */ + SHORT
    373            /* last mod file time              */ + SHORT
    374            /* last mod file date              */ + SHORT
    375            /* crc-32                          */ + WORD
    376            /* compressed size                 */ + WORD
    377            /* uncompressed size               */ + WORD
    378            /* filename length                 */ + SHORT
    379            /* extra field length              */ + SHORT
    380            /* file comment length             */ + SHORT
    381            /* disk number start               */ + SHORT
    382            /* internal file attributes        */ + SHORT
    383            /* external file attributes        */ + WORD
    384            /* relative offset of local header */ + WORD;
    385
    386        private static final long CFH_SIG =
    387            ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG);
    388
    389        /**
    390         * Reads the central directory of the given archive and populates
    391         * the internal tables with ZipArchiveEntry instances.
    392         *
    393         * <p>The ZipArchiveEntrys will know all data that can be obtained from
    394         * the central directory alone, but not the data that requires the
    395         * local file header or additional data to be read.</p>
    396         *
    397         * @return a map of zipentries that didn't have the language
    398         * encoding flag set when read.
    399         */
    400        private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory()
    401            throws IOException {
    402            HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag =
    403                new HashMap<ZipArchiveEntry, NameAndComment>();
    404
    405            positionAtCentralDirectory();
    406
    407            byte[] signatureBytes = new byte[WORD];
    408            archive.readFully(signatureBytes);
    409            long sig = ZipLong.getValue(signatureBytes);
    410
    411            if (sig != CFH_SIG && startsWithLocalFileHeader()) {
    412                throw new IOException("central directory is empty, can't expand"
    413                                      + " corrupt archive.");
    414            }
    415
    416            while (sig == CFH_SIG) {
    417                readCentralDirectoryEntry(noUTF8Flag);
    418                archive.readFully(signatureBytes);
    419                sig = ZipLong.getValue(signatureBytes);
    420            }
    421            return noUTF8Flag;
    422        }
    423
    424        /**
    425         * Reads an individual entry of the central directory, creats an
    426         * ZipArchiveEntry from it and adds it to the global maps.
    427         *
    428         * @param noUTF8Flag map used to collect entries that don't have
    429         * their UTF-8 flag set and whose name will be set by data read
    430         * from the local file header later.  The current entry may be
    431         * added to this map.
    432         */
    433        private void
    434            readCentralDirectoryEntry(Map<ZipArchiveEntry, NameAndComment> noUTF8Flag)
    435            throws IOException {
    436            byte[] cfh = new byte[CFH_LEN];
    437
    438            archive.readFully(cfh);
    439            int off = 0;
    440            ZipArchiveEntry ze = new ZipArchiveEntry();
    441
    442            int versionMadeBy = ZipShort.getValue(cfh, off);
    443            off += SHORT;
    444            ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);
    445
    446            off += SHORT; // skip version info
    447
    448            final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfh, off);
    449            final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
    450            final ZipEncoding entryEncoding =
    451                hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
    452            ze.setGeneralPurposeBit(gpFlag);
    453
    454            off += SHORT;
    455
    456            ze.setMethod(ZipShort.getValue(cfh, off));
    457            off += SHORT;
    458
    459            long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfh, off));
    460            ze.setTime(time);
    461            off += WORD;
    462
    463            ze.setCrc(ZipLong.getValue(cfh, off));
    464            off += WORD;
    465
    466            ze.setCompressedSize(ZipLong.getValue(cfh, off));
    467            off += WORD;
    468
    469            ze.setSize(ZipLong.getValue(cfh, off));
    470            off += WORD;
    471
    472            int fileNameLen = ZipShort.getValue(cfh, off);
    473            off += SHORT;
    474
    475            int extraLen = ZipShort.getValue(cfh, off);
    476            off += SHORT;
    477
    478            int commentLen = ZipShort.getValue(cfh, off);
    479            off += SHORT;
    480
    481            int diskStart = ZipShort.getValue(cfh, off);
    482            off += SHORT;
    483
    484            ze.setInternalAttributes(ZipShort.getValue(cfh, off));
    485            off += SHORT;
    486
    487            ze.setExternalAttributes(ZipLong.getValue(cfh, off));
    488            off += WORD;
    489
    490            byte[] fileName = new byte[fileNameLen];
    491            archive.readFully(fileName);
    492            ze.setName(entryEncoding.decode(fileName), fileName);
    493
    494            // LFH offset,
    495            OffsetEntry offset = new OffsetEntry();
    496            offset.headerOffset = ZipLong.getValue(cfh, off);
    497            // data offset will be filled later
    498            entries.put(ze, offset);
    499
    500            nameMap.put(ze.getName(), ze);
    501
    502            byte[] cdExtraData = new byte[extraLen];
    503            archive.readFully(cdExtraData);
    504            ze.setCentralDirectoryExtra(cdExtraData);
    505
    506            setSizesAndOffsetFromZip64Extra(ze, offset, diskStart);
    507
    508            byte[] comment = new byte[commentLen];
    509            archive.readFully(comment);
    510            ze.setComment(entryEncoding.decode(comment));
    511
    512            if (!hasUTF8Flag && useUnicodeExtraFields) {
    513                noUTF8Flag.put(ze, new NameAndComment(fileName, comment));
    514            }
    515        }
    516
    517        /**
    518         * If the entry holds a Zip64 extended information extra field,
    519         * read sizes from there if the entry's sizes are set to
    520         * 0xFFFFFFFFF, do the same for the offset of the local file
    521         * header.
    522         *
    523         * <p>Ensures the Zip64 extra either knows both compressed and
    524         * uncompressed size or neither of both as the internal logic in
    525         * ExtraFieldUtils forces the field to create local header data
    526         * even if they are never used - and here a field with only one
    527         * size would be invalid.</p>
    528         */
    529        private void setSizesAndOffsetFromZip64Extra(ZipArchiveEntry ze,
    530                                                     OffsetEntry offset,
    531                                                     int diskStart)
    532            throws IOException {
    533            Zip64ExtendedInformationExtraField z64 =
    534                (Zip64ExtendedInformationExtraField)
    535                ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
    536            if (z64 != null) {
    537                boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC;
    538                boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC;
    539                boolean hasRelativeHeaderOffset =
    540                    offset.headerOffset == ZIP64_MAGIC;
    541                z64.reparseCentralDirectoryData(hasUncompressedSize,
    542                                                hasCompressedSize,
    543                                                hasRelativeHeaderOffset,
    544                                                diskStart == ZIP64_MAGIC_SHORT);
    545
    546                if (hasUncompressedSize) {
    547                    ze.setSize(z64.getSize().getLongValue());
    548                } else if (hasCompressedSize) {
    549                    z64.setSize(new ZipEightByteInteger(ze.getSize()));
    550                }
    551
    552                if (hasCompressedSize) {
    553                    ze.setCompressedSize(z64.getCompressedSize().getLongValue());
    554                } else if (hasUncompressedSize) {
    555                    z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
    556                }
    557
    558                if (hasRelativeHeaderOffset) {
    559                    offset.headerOffset =
    560                        z64.getRelativeHeaderOffset().getLongValue();
    561                }
    562            }
    563        }
    564
    565        /**
    566         * Length of the "End of central directory record" - which is
    567         * supposed to be the last structure of the archive - without file
    568         * comment.
    569         */
    570        private static final int MIN_EOCD_SIZE =
    571            /* end of central dir signature    */ WORD
    572            /* number of this disk             */ + SHORT
    573            /* number of the disk with the     */
    574            /* start of the central directory  */ + SHORT
    575            /* total number of entries in      */
    576            /* the central dir on this disk    */ + SHORT
    577            /* total number of entries in      */
    578            /* the central dir                 */ + SHORT
    579            /* size of the central directory   */ + WORD
    580            /* offset of start of central      */
    581            /* directory with respect to       */
    582            /* the starting disk number        */ + WORD
    583            /* zipfile comment length          */ + SHORT;
    584
    585        /**
    586         * Maximum length of the "End of central directory record" with a
    587         * file comment.
    588         */
    589        private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE
    590            /* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT;
    591
    592        /**
    593         * Offset of the field that holds the location of the first
    594         * central directory entry inside the "End of central directory
    595         * record" relative to the start of the "End of central directory
    596         * record".
    597         */
    598        private static final int CFD_LOCATOR_OFFSET =
    599            /* end of central dir signature    */ WORD
    600            /* number of this disk             */ + SHORT
    601            /* number of the disk with the     */
    602            /* start of the central directory  */ + SHORT
    603            /* total number of entries in      */
    604            /* the central dir on this disk    */ + SHORT
    605            /* total number of entries in      */
    606            /* the central dir                 */ + SHORT
    607            /* size of the central directory   */ + WORD;
    608
    609        /**
    610         * Length of the "Zip64 end of central directory locator" - which
    611         * should be right in front of the "end of central directory
    612         * record" if one is present at all.
    613         */
    614        private static final int ZIP64_EOCDL_LENGTH =
    615            /* zip64 end of central dir locator sig */ WORD
    616            /* number of the disk with the start    */
    617            /* start of the zip64 end of            */
    618            /* central directory                    */ + WORD
    619            /* relative offset of the zip64         */
    620            /* end of central directory record      */ + DWORD
    621            /* total number of disks                */ + WORD;
    622
    623        /**
    624         * Offset of the field that holds the location of the "Zip64 end
    625         * of central directory record" inside the "Zip64 end of central
    626         * directory locator" relative to the start of the "Zip64 end of
    627         * central directory locator".
    628         */
    629        private static final int ZIP64_EOCDL_LOCATOR_OFFSET =
    630            /* zip64 end of central dir locator sig */ WORD
    631            /* number of the disk with the start    */
    632            /* start of the zip64 end of            */
    633            /* central directory                    */ + WORD;
    634
    635        /**
    636         * Offset of the field that holds the location of the first
    637         * central directory entry inside the "Zip64 end of central
    638         * directory record" relative to the start of the "Zip64 end of
    639         * central directory record".
    640         */
    641        private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET =
    642            /* zip64 end of central dir        */
    643            /* signature                       */ WORD
    644            /* size of zip64 end of central    */
    645            /* directory record                */ + DWORD
    646            /* version made by                 */ + SHORT
    647            /* version needed to extract       */ + SHORT
    648            /* number of this disk             */ + WORD
    649            /* number of the disk with the     */
    650            /* start of the central directory  */ + WORD
    651            /* total number of entries in the  */
    652            /* central directory on this disk  */ + DWORD
    653            /* total number of entries in the  */
    654            /* central directory               */ + DWORD
    655            /* size of the central directory   */ + DWORD;
    656
    657        /**
    658         * Searches for either the &quot;Zip64 end of central directory
    659         * locator&quot; or the &quot;End of central dir record&quot;, parses
    660         * it and positions the stream at the first central directory
    661         * record.
    662         */
    663        private void positionAtCentralDirectory()
    664            throws IOException {
    665            boolean found = tryToLocateSignature(MIN_EOCD_SIZE + ZIP64_EOCDL_LENGTH,
    666                                                 MAX_EOCD_SIZE + ZIP64_EOCDL_LENGTH,
    667                                                 ZipArchiveOutputStream
    668                                                 .ZIP64_EOCD_LOC_SIG);
    669            if (!found) {
    670                // not a ZIP64 archive
    671                positionAtCentralDirectory32();
    672            } else {
    673                positionAtCentralDirectory64();
    674            }
    675        }
    676
    677        /**
    678         * Parses the &quot;Zip64 end of central directory locator&quot;,
    679         * finds the &quot;Zip64 end of central directory record&quot; using the
    680         * parsed information, parses that and positions the stream at the
    681         * first central directory record.
    682         */
    683        private void positionAtCentralDirectory64()
    684            throws IOException {
    685            skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET);
    686            byte[] zip64EocdOffset = new byte[DWORD];
    687            archive.readFully(zip64EocdOffset);
    688            archive.seek(ZipEightByteInteger.getLongValue(zip64EocdOffset));
    689            byte[] sig = new byte[WORD];
    690            archive.readFully(sig);
    691            if (sig[POS_0] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_0]
    692                || sig[POS_1] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_1]
    693                || sig[POS_2] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_2]
    694                || sig[POS_3] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_3]
    695                ) {
    696                throw new ZipException("archive's ZIP64 end of central "
    697                                       + "directory locator is corrupt.");
    698            }
    699            skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET
    700                      - WORD /* signature has already been read */);
    701            byte[] cfdOffset = new byte[DWORD];
    702            archive.readFully(cfdOffset);
    703            archive.seek(ZipEightByteInteger.getLongValue(cfdOffset));
    704        }
    705
    706        /**
    707         * Searches for the &quot;End of central dir record&quot;, parses
    708         * it and positions the stream at the first central directory
    709         * record.
    710         */
    711        private void positionAtCentralDirectory32()
    712            throws IOException {
    713            boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE,
    714                                                 ZipArchiveOutputStream.EOCD_SIG);
    715            if (!found) {
    716                throw new ZipException("archive is not a ZIP archive");
    717            }
    718            skipBytes(CFD_LOCATOR_OFFSET);
    719            byte[] cfdOffset = new byte[WORD];
    720            archive.readFully(cfdOffset);
    721            archive.seek(ZipLong.getValue(cfdOffset));
    722        }
    723
    724        /**
    725         * Searches the archive backwards from minDistance to maxDistance
    726         * for the given signature, positions the RandomaccessFile right
    727         * at the signature if it has been found.
    728         */
    729        private boolean tryToLocateSignature(long minDistanceFromEnd,
    730                                             long maxDistanceFromEnd,
    731                                             byte[] sig) throws IOException {
    732            boolean found = false;
    733            long off = archive.length() - minDistanceFromEnd;
    734            final long stopSearching =
    735                Math.max(0L, archive.length() - maxDistanceFromEnd);
    736            if (off >= 0) {
    737                for (; off >= stopSearching; off--) {
    738                    archive.seek(off);
    739                    int curr = archive.read();
    740                    if (curr == -1) {
    741                        break;
    742                    }
    743                    if (curr == sig[POS_0]) {
    744                        curr = archive.read();
    745                        if (curr == sig[POS_1]) {
    746                            curr = archive.read();
    747                            if (curr == sig[POS_2]) {
    748                                curr = archive.read();
    749                                if (curr == sig[POS_3]) {
    750                                    found = true;
    751                                    break;
    752                                }
    753                            }
    754                        }
    755                    }
    756                }
    757            }
    758            if (found) {
    759                archive.seek(off);
    760            }
    761            return found;
    762        }
    763
    764        /**
    765         * Skips the given number of bytes or throws an EOFException if
    766         * skipping failed.
    767         */
    768        private void skipBytes(final int count) throws IOException {
    769            int totalSkipped = 0;
    770            while (totalSkipped < count) {
    771                int skippedNow = archive.skipBytes(count - totalSkipped);
    772                if (skippedNow <= 0) {
    773                    throw new EOFException();
    774                }
    775                totalSkipped += skippedNow;
    776            }
    777        }
    778
    779        /**
    780         * Number of bytes in local file header up to the &quot;length of
    781         * filename&quot; entry.
    782         */
    783        private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
    784            /* local file header signature     */ WORD
    785            /* version needed to extract       */ + SHORT
    786            /* general purpose bit flag        */ + SHORT
    787            /* compression method              */ + SHORT
    788            /* last mod file time              */ + SHORT
    789            /* last mod file date              */ + SHORT
    790            /* crc-32                          */ + WORD
    791            /* compressed size                 */ + WORD
    792            /* uncompressed size               */ + WORD;
    793
    794        /**
    795         * Walks through all recorded entries and adds the data available
    796         * from the local file header.
    797         *
    798         * <p>Also records the offsets for the data to read from the
    799         * entries.</p>
    800         */
    801        private void resolveLocalFileHeaderData(Map<ZipArchiveEntry, NameAndComment>
    802                                                entriesWithoutUTF8Flag)
    803            throws IOException {
    804            for (ZipArchiveEntry ze : entries.keySet()) {
    805                OffsetEntry offsetEntry = entries.get(ze);
    806                long offset = offsetEntry.headerOffset;
    807                archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
    808                byte[] b = new byte[SHORT];
    809                archive.readFully(b);
    810                int fileNameLen = ZipShort.getValue(b);
    811                archive.readFully(b);
    812                int extraFieldLen = ZipShort.getValue(b);
    813                int lenToSkip = fileNameLen;
    814                while (lenToSkip > 0) {
    815                    int skipped = archive.skipBytes(lenToSkip);
    816                    if (skipped <= 0) {
    817                        throw new RuntimeException("failed to skip file name in"
    818                                                   + " local file header");
    819                    }
    820                    lenToSkip -= skipped;
    821                }
    822                byte[] localExtraData = new byte[extraFieldLen];
    823                archive.readFully(localExtraData);
    824                ze.setExtra(localExtraData);
    825                offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
    826                    + SHORT + SHORT + fileNameLen + extraFieldLen;
    827
    828                if (entriesWithoutUTF8Flag.containsKey(ze)) {
    829                    String orig = ze.getName();
    830                    NameAndComment nc = entriesWithoutUTF8Flag.get(ze);
    831                    ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name,
    832                                                             nc.comment);
    833                    if (!orig.equals(ze.getName())) {
    834                        nameMap.remove(orig);
    835                        nameMap.put(ze.getName(), ze);
    836                    }
    837                }
    838            }
    839        }
    840
    841        /**
    842         * Checks whether the archive starts with a LFH.  If it doesn't,
    843         * it may be an empty archive.
    844         */
    845        private boolean startsWithLocalFileHeader() throws IOException {
    846            archive.seek(0);
    847            final byte[] start = new byte[WORD];
    848            archive.readFully(start);
    849            for (int i = 0; i < start.length; i++) {
    850                if (start[i] != ZipArchiveOutputStream.LFH_SIG[i]) {
    851                    return false;
    852                }
    853            }
    854            return true;
    855        }
    856
    857        /**
    858         * InputStream that delegates requests to the underlying
    859         * RandomAccessFile, making sure that only bytes from a certain
    860         * range can be read.
    861         */
    862        private class BoundedInputStream extends InputStream {
    863            private long remaining;
    864            private long loc;
    865            private boolean addDummyByte = false;
    866
    867            BoundedInputStream(long start, long remaining) {
    868                this.remaining = remaining;
    869                loc = start;
    870            }
    871
    872            @Override
    873            public int read() throws IOException {
    874                if (remaining-- <= 0) {
    875                    if (addDummyByte) {
    876                        addDummyByte = false;
    877                        return 0;
    878                    }
    879                    return -1;
    880                }
    881                synchronized (archive) {
    882                    archive.seek(loc++);
    883                    return archive.read();
    884                }
    885            }
    886
    887            @Override
    888            public int read(byte[] b, int off, int len) throws IOException {
    889                if (remaining <= 0) {
    890                    if (addDummyByte) {
    891                        addDummyByte = false;
    892                        b[off] = 0;
    893                        return 1;
    894                    }
    895                    return -1;
    896                }
    897
    898                if (len <= 0) {
    899                    return 0;
    900                }
    901
    902                if (len > remaining) {
    903                    len = (int) remaining;
    904                }
    905                int ret = -1;
    906                synchronized (archive) {
    907                    archive.seek(loc);
    908                    ret = archive.read(b, off, len);
    909                }
    910                if (ret > 0) {
    911                    loc += ret;
    912                    remaining -= ret;
    913                }
    914                return ret;
    915            }
    916
    917            /**
    918             * Inflater needs an extra dummy byte for nowrap - see
    919             * Inflater's javadocs.
    920             */
    921            void addDummy() {
    922                addDummyByte = true;
    923            }
    924        }
    925
    926        private static final class NameAndComment {
    927            private final byte[] name;
    928            private final byte[] comment;
    929            private NameAndComment(byte[] name, byte[] comment) {
    930                this.name = name;
    931                this.comment = comment;
    932            }
    933        }
    934
    935        /**
    936         * Compares two ZipArchiveEntries based on their offset within the archive.
    937         *
    938         * <p>Won't return any meaningful results if one of the entries
    939         * isn't part of the archive at all.</p>
    940         *
    941         * @since Commons Compress 1.1
    942         */
    943        private final Comparator<ZipArchiveEntry> OFFSET_COMPARATOR =
    944            new Comparator<ZipArchiveEntry>() {
    945            public int compare(ZipArchiveEntry e1, ZipArchiveEntry e2) {
    946                if (e1 == e2)
    947                    return 0;
    948
    949                OffsetEntry off1 = entries.get(e1);
    950                OffsetEntry off2 = entries.get(e2);
    951                if (off1 == null) {
    952                    return 1;
    953                }
    954                if (off2 == null) {
    955                    return -1;
    956                }
    957                long val = (off1.headerOffset - off2.headerOffset);
    958                return val == 0 ? 0 : val < 0 ? -1 : +1;
    959            }
    960        };
    961    }