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    }