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