View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   *
17   */
18  package org.apache.commons.compress.archivers.zip;
19  
20  import java.io.BufferedInputStream;
21  import java.io.Closeable;
22  import java.io.EOFException;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.RandomAccessFile;
27  import java.util.Arrays;
28  import java.util.Collections;
29  import java.util.Comparator;
30  import java.util.Enumeration;
31  import java.util.HashMap;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.zip.Inflater;
36  import java.util.zip.InflaterInputStream;
37  import java.util.zip.ZipException;
38  
39  import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
40  import org.apache.commons.compress.utils.IOUtils;
41  
42  import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
43  import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT;
44  import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
45  import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC;
46  import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT;
47  
48  /**
49   * Replacement for <code>java.util.ZipFile</code>.
50   *
51   * <p>This class adds support for file name encodings other than UTF-8
52   * (which is required to work on ZIP files created by native zip tools
53   * and is able to skip a preamble like the one found in self
54   * extracting archives.  Furthermore it returns instances of
55   * <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
56   * instead of <code>java.util.zip.ZipEntry</code>.</p>
57   *
58   * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
59   * have to reimplement all methods anyway.  Like
60   * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
61   * covers and supports compressed and uncompressed entries.  As of
62   * Apache Commons Compress 1.3 it also transparently supports Zip64
63   * extensions and thus individual entries and archives larger than 4
64   * GB or with more than 65536 entries.</p>
65   *
66   * <p>The method signatures mimic the ones of
67   * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
68   *
69   * <ul>
70   *   <li>There is no getName method.</li>
71   *   <li>entries has been renamed to getEntries.</li>
72   *   <li>getEntries and getEntry return
73   *   <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
74   *   instances.</li>
75   *   <li>close is allowed to throw IOException.</li>
76   * </ul>
77   *
78   */
79  public class ZipFile implements Closeable {
80      private static final int HASH_SIZE = 509;
81      static final int NIBLET_MASK = 0x0f;
82      static final int BYTE_SHIFT = 8;
83      private static final int POS_0 = 0;
84      private static final int POS_1 = 1;
85      private static final int POS_2 = 2;
86      private static final int POS_3 = 3;
87  
88      /**
89       * List of entries in the order they appear inside the central
90       * directory.
91       */
92      private final List<ZipArchiveEntry> entries =
93          new LinkedList<ZipArchiveEntry>();
94  
95      /**
96       * Maps String to list of ZipArchiveEntrys, name -> actual entries.
97       */
98      private final Map<String, LinkedList<ZipArchiveEntry>> nameMap =
99          new HashMap<String, LinkedList<ZipArchiveEntry>>(HASH_SIZE);
100 
101     private static final class OffsetEntry {
102         private long headerOffset = -1;
103         private long dataOffset = -1;
104     }
105 
106     /**
107      * The encoding to use for filenames and the file comment.
108      *
109      * <p>For a list of possible values see <a
110      * 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>.
111      * Defaults to UTF-8.</p>
112      */
113     private final String encoding;
114 
115     /**
116      * The zip encoding to use for filenames and the file comment.
117      */
118     private final ZipEncoding zipEncoding;
119 
120     /**
121      * File name of actual source.
122      */
123     private final String archiveName;
124 
125     /**
126      * The actual data source.
127      */
128     private final RandomAccessFile archive;
129 
130     /**
131      * Whether to look for and use Unicode extra fields.
132      */
133     private final boolean useUnicodeExtraFields;
134 
135     /**
136      * Whether the file is closed.
137      */
138     private volatile boolean closed = true;
139 
140     // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection)
141     private final byte[] DWORD_BUF = new byte[DWORD];
142     private final byte[] WORD_BUF = new byte[WORD];
143     private final byte[] CFH_BUF = new byte[CFH_LEN];
144     private final byte[] SHORT_BUF = new byte[SHORT];
145 
146     /**
147      * Opens the given file for reading, assuming "UTF8" for file names.
148      *
149      * @param f the archive.
150      *
151      * @throws IOException if an error occurs while reading the file.
152      */
153     public ZipFile(final File f) throws IOException {
154         this(f, ZipEncodingHelper.UTF8);
155     }
156 
157     /**
158      * Opens the given file for reading, assuming "UTF8".
159      *
160      * @param name name of the archive.
161      *
162      * @throws IOException if an error occurs while reading the file.
163      */
164     public ZipFile(final String name) throws IOException {
165         this(new File(name), ZipEncodingHelper.UTF8);
166     }
167 
168     /**
169      * Opens the given file for reading, assuming the specified
170      * encoding for file names, scanning unicode extra fields.
171      *
172      * @param name name of the archive.
173      * @param encoding the encoding to use for file names, use null
174      * for the platform's default encoding
175      *
176      * @throws IOException if an error occurs while reading the file.
177      */
178     public ZipFile(final String name, final String encoding) throws IOException {
179         this(new File(name), encoding, true);
180     }
181 
182     /**
183      * Opens the given file for reading, assuming the specified
184      * encoding for file names and scanning for unicode extra fields.
185      *
186      * @param f the archive.
187      * @param encoding the encoding to use for file names, use null
188      * for the platform's default encoding
189      *
190      * @throws IOException if an error occurs while reading the file.
191      */
192     public ZipFile(final File f, final String encoding) throws IOException {
193         this(f, encoding, true);
194     }
195 
196     /**
197      * Opens the given file for reading, assuming the specified
198      * encoding for file names.
199      *
200      * @param f the archive.
201      * @param encoding the encoding to use for file names, use null
202      * for the platform's default encoding
203      * @param useUnicodeExtraFields whether to use InfoZIP Unicode
204      * Extra Fields (if present) to set the file names.
205      *
206      * @throws IOException if an error occurs while reading the file.
207      */
208     public ZipFile(final File f, final String encoding, final boolean useUnicodeExtraFields)
209         throws IOException {
210         this.archiveName = f.getAbsolutePath();
211         this.encoding = encoding;
212         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
213         this.useUnicodeExtraFields = useUnicodeExtraFields;
214         archive = new RandomAccessFile(f, "r");
215         boolean success = false;
216         try {
217             final Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag =
218                 populateFromCentralDirectory();
219             resolveLocalFileHeaderData(entriesWithoutUTF8Flag);
220             success = true;
221         } finally {
222             closed = !success;
223             if (!success) {
224                 IOUtils.closeQuietly(archive);
225             }
226         }
227     }
228 
229     /**
230      * The encoding to use for filenames and the file comment.
231      *
232      * @return null if using the platform's default character encoding.
233      */
234     public String getEncoding() {
235         return encoding;
236     }
237 
238     /**
239      * Closes the archive.
240      * @throws IOException if an error occurs closing the archive.
241      */
242     @Override
243     public void close() throws IOException {
244         // this flag is only written here and read in finalize() which
245         // can never be run in parallel.
246         // no synchronization needed.
247         closed = true;
248 
249         archive.close();
250     }
251 
252     /**
253      * close a zipfile quietly; throw no io fault, do nothing
254      * on a null parameter
255      * @param zipfile file to close, can be null
256      */
257     public static void closeQuietly(final ZipFile zipfile) {
258         IOUtils.closeQuietly(zipfile);
259     }
260 
261     /**
262      * Returns all entries.
263      *
264      * <p>Entries will be returned in the same order they appear
265      * within the archive's central directory.</p>
266      *
267      * @return all entries as {@link ZipArchiveEntry} instances
268      */
269     public Enumeration<ZipArchiveEntry> getEntries() {
270         return Collections.enumeration(entries);
271     }
272 
273     /**
274      * Returns all entries in physical order.
275      *
276      * <p>Entries will be returned in the same order their contents
277      * appear within the archive.</p>
278      *
279      * @return all entries as {@link ZipArchiveEntry} instances
280      *
281      * @since 1.1
282      */
283     public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() {
284         final ZipArchiveEntry[] allEntries = entries.toArray(new ZipArchiveEntry[entries.size()]);
285         Arrays.sort(allEntries, OFFSET_COMPARATOR);
286         return Collections.enumeration(Arrays.asList(allEntries));
287     }
288 
289     /**
290      * Returns a named entry - or {@code null} if no entry by
291      * that name exists.
292      *
293      * <p>If multiple entries with the same name exist the first entry
294      * in the archive's central directory by that name is
295      * returned.</p>
296      *
297      * @param name name of the entry.
298      * @return the ZipArchiveEntry corresponding to the given name - or
299      * {@code null} if not present.
300      */
301     public ZipArchiveEntry getEntry(final String name) {
302         final LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
303         return entriesOfThatName != null ? entriesOfThatName.getFirst() : null;
304     }
305 
306     /**
307      * Returns all named entries in the same order they appear within
308      * the archive's central directory.
309      *
310      * @param name name of the entry.
311      * @return the Iterable&lt;ZipArchiveEntry&gt; corresponding to the
312      * given name
313      * @since 1.6
314      */
315     public Iterable<ZipArchiveEntry> getEntries(final String name) {
316         final List<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
317         return entriesOfThatName != null ? entriesOfThatName
318             : Collections.<ZipArchiveEntry>emptyList();
319     }
320 
321     /**
322      * Returns all named entries in the same order their contents
323      * appear within the archive.
324      *
325      * @param name name of the entry.
326      * @return the Iterable&lt;ZipArchiveEntry&gt; corresponding to the
327      * given name
328      * @since 1.6
329      */
330     public Iterable<ZipArchiveEntry> getEntriesInPhysicalOrder(final String name) {
331         ZipArchiveEntry[] entriesOfThatName = new ZipArchiveEntry[0];
332         if (nameMap.containsKey(name)) {
333             entriesOfThatName = nameMap.get(name).toArray(entriesOfThatName);
334             Arrays.sort(entriesOfThatName, OFFSET_COMPARATOR);
335         }
336         return Arrays.asList(entriesOfThatName);
337     }
338 
339     /**
340      * Whether this class is able to read the given entry.
341      *
342      * <p>May return false if it is set up to use encryption or a
343      * compression method that hasn't been implemented yet.</p>
344      * @since 1.1
345      * @param ze the entry
346      * @return whether this class is able to read the given entry.
347      */
348     public boolean canReadEntryData(final ZipArchiveEntry ze) {
349         return ZipUtil.canHandleEntryData(ze);
350     }
351 
352     /**
353      * Expose the raw stream of the archive entry (compressed form).
354      *
355      * <p>This method does not relate to how/if we understand the payload in the
356      * stream, since we really only intend to move it on to somewhere else.</p>
357      *
358      * @param ze The entry to get the stream for
359      * @return The raw input stream containing (possibly) compressed data.
360      * @since 1.11
361      */
362     public InputStream getRawInputStream(final ZipArchiveEntry ze) {
363         if (!(ze instanceof Entry)) {
364             return null;
365         }
366         final OffsetEntry offsetEntry = ((Entry) ze).getOffsetEntry();
367         final long start = offsetEntry.dataOffset;
368         return new BoundedInputStream(start, ze.getCompressedSize());
369     }
370 
371 
372     /**
373      * Transfer selected entries from this zipfile to a given #ZipArchiveOutputStream.
374      * Compression and all other attributes will be as in this file.
375      * <p>This method transfers entries based on the central directory of the zip file.</p>
376      *
377      * @param target The zipArchiveOutputStream to write the entries to
378      * @param predicate A predicate that selects which entries to write
379      * @throws IOException on error
380      */
381     public void copyRawEntries(final ZipArchiveOutputStream target, final ZipArchiveEntryPredicate predicate)
382             throws IOException {
383         final Enumeration<ZipArchiveEntry> src = getEntriesInPhysicalOrder();
384         while (src.hasMoreElements()) {
385             final ZipArchiveEntry entry = src.nextElement();
386             if (predicate.test( entry)) {
387                 target.addRawArchiveEntry(entry, getRawInputStream(entry));
388             }
389         }
390     }
391 
392     /**
393      * Returns an InputStream for reading the contents of the given entry.
394      *
395      * @param ze the entry to get the stream for.
396      * @return a stream to read the entry from.
397      * @throws IOException if unable to create an input stream from the zipentry
398      * @throws ZipException if the zipentry uses an unsupported feature
399      */
400     public InputStream getInputStream(final ZipArchiveEntry ze)
401         throws IOException, ZipException {
402         if (!(ze instanceof Entry)) {
403             return null;
404         }
405         // cast valididty is checked just above
406         final OffsetEntry offsetEntry = ((Entry) ze).getOffsetEntry();
407         ZipUtil.checkRequestedFeatures(ze);
408         final long start = offsetEntry.dataOffset;
409         final BoundedInputStream bis =
410             new BoundedInputStream(start, ze.getCompressedSize());
411         switch (ZipMethod.getMethodByCode(ze.getMethod())) {
412             case STORED:
413                 return bis;
414             case UNSHRINKING:
415                 return new UnshrinkingInputStream(bis);
416             case IMPLODING:
417                 return new ExplodingInputStream(ze.getGeneralPurposeBit().getSlidingDictionarySize(),
418                         ze.getGeneralPurposeBit().getNumberOfShannonFanoTrees(), new BufferedInputStream(bis));
419             case DEFLATED:
420                 bis.addDummy();
421                 final Inflater inflater = new Inflater(true);
422                 return new InflaterInputStream(bis, inflater) {
423                     @Override
424                     public void close() throws IOException {
425                         try {
426                             super.close();
427                         } finally {
428                             inflater.end();
429                         }
430                     }
431                 };
432             case BZIP2:
433                 return new BZip2CompressorInputStream(bis);
434             case AES_ENCRYPTED:
435             case ENHANCED_DEFLATED:
436             case EXPANDING_LEVEL_1:
437             case EXPANDING_LEVEL_2:
438             case EXPANDING_LEVEL_3:
439             case EXPANDING_LEVEL_4:
440             case JPEG:
441             case LZMA:
442             case PKWARE_IMPLODING:
443             case PPMD:
444             case TOKENIZATION:
445             case UNKNOWN:
446             case WAVPACK:
447             default:
448                 throw new ZipException("Found unsupported compression method "
449                                        + ze.getMethod());
450         }
451     }
452 
453     /**
454      * <p>
455      * Convenience method to return the entry's content as a String if isUnixSymlink()
456      * returns true for it, otherwise returns null.
457      * </p>
458      *
459      * <p>This method assumes the symbolic link's file name uses the
460      * same encoding that as been specified for this ZipFile.</p>
461      *
462      * @param entry ZipArchiveEntry object that represents the symbolic link
463      * @return entry's content as a String
464      * @throws IOException problem with content's input stream
465      * @since 1.5
466      */
467     public String getUnixSymlink(final ZipArchiveEntry entry) throws IOException {
468         if (entry != null && entry.isUnixSymlink()) {
469             InputStream in = null;
470             try {
471                 in = getInputStream(entry);
472                 final byte[] symlinkBytes = IOUtils.toByteArray(in);
473                 return zipEncoding.decode(symlinkBytes);
474             } finally {
475                 if (in != null) {
476                     in.close();
477                 }
478             }
479         }
480         return null;
481     }
482 
483     /**
484      * Ensures that the close method of this zipfile is called when
485      * there are no more references to it.
486      * @see #close()
487      */
488     @Override
489     protected void finalize() throws Throwable {
490         try {
491             if (!closed) {
492                 System.err.println("Cleaning up unclosed ZipFile for archive "
493                                    + archiveName);
494                 close();
495             }
496         } finally {
497             super.finalize();
498         }
499     }
500 
501     /**
502      * Length of a "central directory" entry structure without file
503      * name, extra fields or comment.
504      */
505     private static final int CFH_LEN =
506         /* version made by                 */ SHORT
507         /* version needed to extract       */ + SHORT
508         /* general purpose bit flag        */ + SHORT
509         /* compression method              */ + SHORT
510         /* last mod file time              */ + SHORT
511         /* last mod file date              */ + SHORT
512         /* crc-32                          */ + WORD
513         /* compressed size                 */ + WORD
514         /* uncompressed size               */ + WORD
515         /* filename length                 */ + SHORT
516         /* extra field length              */ + SHORT
517         /* file comment length             */ + SHORT
518         /* disk number start               */ + SHORT
519         /* internal file attributes        */ + SHORT
520         /* external file attributes        */ + WORD
521         /* relative offset of local header */ + WORD;
522 
523     private static final long CFH_SIG =
524         ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG);
525 
526     /**
527      * Reads the central directory of the given archive and populates
528      * the internal tables with ZipArchiveEntry instances.
529      *
530      * <p>The ZipArchiveEntrys will know all data that can be obtained from
531      * the central directory alone, but not the data that requires the
532      * local file header or additional data to be read.</p>
533      *
534      * @return a map of zipentries that didn't have the language
535      * encoding flag set when read.
536      */
537     private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory()
538         throws IOException {
539         final HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag =
540             new HashMap<ZipArchiveEntry, NameAndComment>();
541 
542         positionAtCentralDirectory();
543 
544         archive.readFully(WORD_BUF);
545         long sig = ZipLong.getValue(WORD_BUF);
546 
547         if (sig != CFH_SIG && startsWithLocalFileHeader()) {
548             throw new IOException("central directory is empty, can't expand"
549                                   + " corrupt archive.");
550         }
551 
552         while (sig == CFH_SIG) {
553             readCentralDirectoryEntry(noUTF8Flag);
554             archive.readFully(WORD_BUF);
555             sig = ZipLong.getValue(WORD_BUF);
556         }
557         return noUTF8Flag;
558     }
559 
560     /**
561      * Reads an individual entry of the central directory, creats an
562      * ZipArchiveEntry from it and adds it to the global maps.
563      *
564      * @param noUTF8Flag map used to collect entries that don't have
565      * their UTF-8 flag set and whose name will be set by data read
566      * from the local file header later.  The current entry may be
567      * added to this map.
568      */
569     private void
570         readCentralDirectoryEntry(final Map<ZipArchiveEntry, NameAndComment> noUTF8Flag)
571         throws IOException {
572         archive.readFully(CFH_BUF);
573         int off = 0;
574         final OffsetEntry offset = new OffsetEntry();
575         final Entry ze = new Entry(offset);
576 
577         final int versionMadeBy = ZipShort.getValue(CFH_BUF, off);
578         off += SHORT;
579         ze.setVersionMadeBy(versionMadeBy);
580         ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);
581 
582         ze.setVersionRequired(ZipShort.getValue(CFH_BUF, off));
583         off += SHORT; // version required
584 
585         final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(CFH_BUF, off);
586         final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
587         final ZipEncoding entryEncoding =
588             hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
589         ze.setGeneralPurposeBit(gpFlag);
590         ze.setRawFlag(ZipShort.getValue(CFH_BUF, off));
591 
592         off += SHORT;
593 
594         //noinspection MagicConstant
595         ze.setMethod(ZipShort.getValue(CFH_BUF, off));
596         off += SHORT;
597 
598         final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(CFH_BUF, off));
599         ze.setTime(time);
600         off += WORD;
601 
602         ze.setCrc(ZipLong.getValue(CFH_BUF, off));
603         off += WORD;
604 
605         ze.setCompressedSize(ZipLong.getValue(CFH_BUF, off));
606         off += WORD;
607 
608         ze.setSize(ZipLong.getValue(CFH_BUF, off));
609         off += WORD;
610 
611         final int fileNameLen = ZipShort.getValue(CFH_BUF, off);
612         off += SHORT;
613 
614         final int extraLen = ZipShort.getValue(CFH_BUF, off);
615         off += SHORT;
616 
617         final int commentLen = ZipShort.getValue(CFH_BUF, off);
618         off += SHORT;
619 
620         final int diskStart = ZipShort.getValue(CFH_BUF, off);
621         off += SHORT;
622 
623         ze.setInternalAttributes(ZipShort.getValue(CFH_BUF, off));
624         off += SHORT;
625 
626         ze.setExternalAttributes(ZipLong.getValue(CFH_BUF, off));
627         off += WORD;
628 
629         final byte[] fileName = new byte[fileNameLen];
630         archive.readFully(fileName);
631         ze.setName(entryEncoding.decode(fileName), fileName);
632 
633         // LFH offset,
634         offset.headerOffset = ZipLong.getValue(CFH_BUF, off);
635         // data offset will be filled later
636         entries.add(ze);
637 
638         final byte[] cdExtraData = new byte[extraLen];
639         archive.readFully(cdExtraData);
640         ze.setCentralDirectoryExtra(cdExtraData);
641 
642         setSizesAndOffsetFromZip64Extra(ze, offset, diskStart);
643 
644         final byte[] comment = new byte[commentLen];
645         archive.readFully(comment);
646         ze.setComment(entryEncoding.decode(comment));
647 
648         if (!hasUTF8Flag && useUnicodeExtraFields) {
649             noUTF8Flag.put(ze, new NameAndComment(fileName, comment));
650         }
651     }
652 
653     /**
654      * If the entry holds a Zip64 extended information extra field,
655      * read sizes from there if the entry's sizes are set to
656      * 0xFFFFFFFFF, do the same for the offset of the local file
657      * header.
658      *
659      * <p>Ensures the Zip64 extra either knows both compressed and
660      * uncompressed size or neither of both as the internal logic in
661      * ExtraFieldUtils forces the field to create local header data
662      * even if they are never used - and here a field with only one
663      * size would be invalid.</p>
664      */
665     private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze,
666                                                  final OffsetEntry offset,
667                                                  final int diskStart)
668         throws IOException {
669         final Zip64ExtendedInformationExtraField z64 =
670             (Zip64ExtendedInformationExtraField)
671             ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
672         if (z64 != null) {
673             final boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC;
674             final boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC;
675             final boolean hasRelativeHeaderOffset =
676                 offset.headerOffset == ZIP64_MAGIC;
677             z64.reparseCentralDirectoryData(hasUncompressedSize,
678                                             hasCompressedSize,
679                                             hasRelativeHeaderOffset,
680                                             diskStart == ZIP64_MAGIC_SHORT);
681 
682             if (hasUncompressedSize) {
683                 ze.setSize(z64.getSize().getLongValue());
684             } else if (hasCompressedSize) {
685                 z64.setSize(new ZipEightByteInteger(ze.getSize()));
686             }
687 
688             if (hasCompressedSize) {
689                 ze.setCompressedSize(z64.getCompressedSize().getLongValue());
690             } else if (hasUncompressedSize) {
691                 z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
692             }
693 
694             if (hasRelativeHeaderOffset) {
695                 offset.headerOffset =
696                     z64.getRelativeHeaderOffset().getLongValue();
697             }
698         }
699     }
700 
701     /**
702      * Length of the "End of central directory record" - which is
703      * supposed to be the last structure of the archive - without file
704      * comment.
705      */
706     static final int MIN_EOCD_SIZE =
707         /* end of central dir signature    */ WORD
708         /* number of this disk             */ + SHORT
709         /* number of the disk with the     */
710         /* start of the central directory  */ + SHORT
711         /* total number of entries in      */
712         /* the central dir on this disk    */ + SHORT
713         /* total number of entries in      */
714         /* the central dir                 */ + SHORT
715         /* size of the central directory   */ + WORD
716         /* offset of start of central      */
717         /* directory with respect to       */
718         /* the starting disk number        */ + WORD
719         /* zipfile comment length          */ + SHORT;
720 
721     /**
722      * Maximum length of the "End of central directory record" with a
723      * file comment.
724      */
725     private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE
726         /* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT;
727 
728     /**
729      * Offset of the field that holds the location of the first
730      * central directory entry inside the "End of central directory
731      * record" relative to the start of the "End of central directory
732      * record".
733      */
734     private static final int CFD_LOCATOR_OFFSET =
735         /* end of central dir signature    */ WORD
736         /* number of this disk             */ + SHORT
737         /* number of the disk with the     */
738         /* start of the central directory  */ + SHORT
739         /* total number of entries in      */
740         /* the central dir on this disk    */ + SHORT
741         /* total number of entries in      */
742         /* the central dir                 */ + SHORT
743         /* size of the central directory   */ + WORD;
744 
745     /**
746      * Length of the "Zip64 end of central directory locator" - which
747      * should be right in front of the "end of central directory
748      * record" if one is present at all.
749      */
750     private static final int ZIP64_EOCDL_LENGTH =
751         /* zip64 end of central dir locator sig */ WORD
752         /* number of the disk with the start    */
753         /* start of the zip64 end of            */
754         /* central directory                    */ + WORD
755         /* relative offset of the zip64         */
756         /* end of central directory record      */ + DWORD
757         /* total number of disks                */ + WORD;
758 
759     /**
760      * Offset of the field that holds the location of the "Zip64 end
761      * of central directory record" inside the "Zip64 end of central
762      * directory locator" relative to the start of the "Zip64 end of
763      * central directory locator".
764      */
765     private static final int ZIP64_EOCDL_LOCATOR_OFFSET =
766         /* zip64 end of central dir locator sig */ WORD
767         /* number of the disk with the start    */
768         /* start of the zip64 end of            */
769         /* central directory                    */ + WORD;
770 
771     /**
772      * Offset of the field that holds the location of the first
773      * central directory entry inside the "Zip64 end of central
774      * directory record" relative to the start of the "Zip64 end of
775      * central directory record".
776      */
777     private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET =
778         /* zip64 end of central dir        */
779         /* signature                       */ WORD
780         /* size of zip64 end of central    */
781         /* directory record                */ + DWORD
782         /* version made by                 */ + SHORT
783         /* version needed to extract       */ + SHORT
784         /* number of this disk             */ + WORD
785         /* number of the disk with the     */
786         /* start of the central directory  */ + WORD
787         /* total number of entries in the  */
788         /* central directory on this disk  */ + DWORD
789         /* total number of entries in the  */
790         /* central directory               */ + DWORD
791         /* size of the central directory   */ + DWORD;
792 
793     /**
794      * Searches for either the &quot;Zip64 end of central directory
795      * locator&quot; or the &quot;End of central dir record&quot;, parses
796      * it and positions the stream at the first central directory
797      * record.
798      */
799     private void positionAtCentralDirectory()
800         throws IOException {
801         positionAtEndOfCentralDirectoryRecord();
802         boolean found = false;
803         final boolean searchedForZip64EOCD =
804             archive.getFilePointer() > ZIP64_EOCDL_LENGTH;
805         if (searchedForZip64EOCD) {
806             archive.seek(archive.getFilePointer() - ZIP64_EOCDL_LENGTH);
807             archive.readFully(WORD_BUF);
808             found = Arrays.equals(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG,
809                                   WORD_BUF);
810         }
811         if (!found) {
812             // not a ZIP64 archive
813             if (searchedForZip64EOCD) {
814                 skipBytes(ZIP64_EOCDL_LENGTH - WORD);
815             }
816             positionAtCentralDirectory32();
817         } else {
818             positionAtCentralDirectory64();
819         }
820     }
821 
822     /**
823      * Parses the &quot;Zip64 end of central directory locator&quot;,
824      * finds the &quot;Zip64 end of central directory record&quot; using the
825      * parsed information, parses that and positions the stream at the
826      * first central directory record.
827      *
828      * Expects stream to be positioned right behind the &quot;Zip64
829      * end of central directory locator&quot;'s signature.
830      */
831     private void positionAtCentralDirectory64()
832         throws IOException {
833         skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET
834                   - WORD /* signature has already been read */);
835         archive.readFully(DWORD_BUF);
836         archive.seek(ZipEightByteInteger.getLongValue(DWORD_BUF));
837         archive.readFully(WORD_BUF);
838         if (!Arrays.equals(WORD_BUF, ZipArchiveOutputStream.ZIP64_EOCD_SIG)) {
839             throw new ZipException("archive's ZIP64 end of central "
840                                    + "directory locator is corrupt.");
841         }
842         skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET
843                   - WORD /* signature has already been read */);
844         archive.readFully(DWORD_BUF);
845         archive.seek(ZipEightByteInteger.getLongValue(DWORD_BUF));
846     }
847 
848     /**
849      * Parses the &quot;End of central dir record&quot; and positions
850      * the stream at the first central directory record.
851      *
852      * Expects stream to be positioned at the beginning of the
853      * &quot;End of central dir record&quot;.
854      */
855     private void positionAtCentralDirectory32()
856         throws IOException {
857         skipBytes(CFD_LOCATOR_OFFSET);
858         archive.readFully(WORD_BUF);
859         archive.seek(ZipLong.getValue(WORD_BUF));
860     }
861 
862     /**
863      * Searches for the and positions the stream at the start of the
864      * &quot;End of central dir record&quot;.
865      */
866     private void positionAtEndOfCentralDirectoryRecord()
867         throws IOException {
868         final boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE,
869                                              ZipArchiveOutputStream.EOCD_SIG);
870         if (!found) {
871             throw new ZipException("archive is not a ZIP archive");
872         }
873     }
874 
875     /**
876      * Searches the archive backwards from minDistance to maxDistance
877      * for the given signature, positions the RandomaccessFile right
878      * at the signature if it has been found.
879      */
880     private boolean tryToLocateSignature(final long minDistanceFromEnd,
881                                          final long maxDistanceFromEnd,
882                                          final byte[] sig) throws IOException {
883         boolean found = false;
884         long off = archive.length() - minDistanceFromEnd;
885         final long stopSearching =
886             Math.max(0L, archive.length() - maxDistanceFromEnd);
887         if (off >= 0) {
888             for (; off >= stopSearching; off--) {
889                 archive.seek(off);
890                 int curr = archive.read();
891                 if (curr == -1) {
892                     break;
893                 }
894                 if (curr == sig[POS_0]) {
895                     curr = archive.read();
896                     if (curr == sig[POS_1]) {
897                         curr = archive.read();
898                         if (curr == sig[POS_2]) {
899                             curr = archive.read();
900                             if (curr == sig[POS_3]) {
901                                 found = true;
902                                 break;
903                             }
904                         }
905                     }
906                 }
907             }
908         }
909         if (found) {
910             archive.seek(off);
911         }
912         return found;
913     }
914 
915     /**
916      * Skips the given number of bytes or throws an EOFException if
917      * skipping failed.
918      */ 
919     private void skipBytes(final int count) throws IOException {
920         int totalSkipped = 0;
921         while (totalSkipped < count) {
922             final int skippedNow = archive.skipBytes(count - totalSkipped);
923             if (skippedNow <= 0) {
924                 throw new EOFException();
925             }
926             totalSkipped += skippedNow;
927         }
928     }
929 
930     /**
931      * Number of bytes in local file header up to the &quot;length of
932      * filename&quot; entry.
933      */
934     private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
935         /* local file header signature     */ WORD
936         /* version needed to extract       */ + SHORT
937         /* general purpose bit flag        */ + SHORT
938         /* compression method              */ + SHORT
939         /* last mod file time              */ + SHORT
940         /* last mod file date              */ + SHORT
941         /* crc-32                          */ + WORD
942         /* compressed size                 */ + WORD
943         /* uncompressed size               */ + WORD;
944 
945     /**
946      * Walks through all recorded entries and adds the data available
947      * from the local file header.
948      *
949      * <p>Also records the offsets for the data to read from the
950      * entries.</p>
951      */
952     private void resolveLocalFileHeaderData(final Map<ZipArchiveEntry, NameAndComment>
953                                             entriesWithoutUTF8Flag)
954         throws IOException {
955         for (final ZipArchiveEntry zipArchiveEntry : entries) {
956             // entries is filled in populateFromCentralDirectory and
957             // never modified
958             final Entry ze = (Entry) zipArchiveEntry;
959             final OffsetEntry offsetEntry = ze.getOffsetEntry();
960             final long offset = offsetEntry.headerOffset;
961             archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
962             archive.readFully(SHORT_BUF);
963             final int fileNameLen = ZipShort.getValue(SHORT_BUF);
964             archive.readFully(SHORT_BUF);
965             final int extraFieldLen = ZipShort.getValue(SHORT_BUF);
966             int lenToSkip = fileNameLen;
967             while (lenToSkip > 0) {
968                 final int skipped = archive.skipBytes(lenToSkip);
969                 if (skipped <= 0) {
970                     throw new IOException("failed to skip file name in"
971                                           + " local file header");
972                 }
973                 lenToSkip -= skipped;
974             }
975             final byte[] localExtraData = new byte[extraFieldLen];
976             archive.readFully(localExtraData);
977             ze.setExtra(localExtraData);
978             offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
979                 + SHORT + SHORT + fileNameLen + extraFieldLen;
980 
981             if (entriesWithoutUTF8Flag.containsKey(ze)) {
982                 final NameAndComment nc = entriesWithoutUTF8Flag.get(ze);
983                 ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name,
984                                                          nc.comment);
985             }
986 
987             final String name = ze.getName();
988             LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name);
989             if (entriesOfThatName == null) {
990                 entriesOfThatName = new LinkedList<ZipArchiveEntry>();
991                 nameMap.put(name, entriesOfThatName);
992             }
993             entriesOfThatName.addLast(ze);
994         }
995     }
996 
997     /**
998      * Checks whether the archive starts with a LFH.  If it doesn't,
999      * it may be an empty archive.
1000      */
1001     private boolean startsWithLocalFileHeader() throws IOException {
1002         archive.seek(0);
1003         archive.readFully(WORD_BUF);
1004         return Arrays.equals(WORD_BUF, ZipArchiveOutputStream.LFH_SIG);
1005     }
1006 
1007     /**
1008      * InputStream that delegates requests to the underlying
1009      * RandomAccessFile, making sure that only bytes from a certain
1010      * range can be read.
1011      */
1012     private class BoundedInputStream extends InputStream {
1013         private long remaining;
1014         private long loc;
1015         private boolean addDummyByte = false;
1016 
1017         BoundedInputStream(final long start, final long remaining) {
1018             this.remaining = remaining;
1019             loc = start;
1020         }
1021 
1022         @Override
1023         public int read() throws IOException {
1024             if (remaining-- <= 0) {
1025                 if (addDummyByte) {
1026                     addDummyByte = false;
1027                     return 0;
1028                 }
1029                 return -1;
1030             }
1031             synchronized (archive) {
1032                 archive.seek(loc++);
1033                 return archive.read();
1034             }
1035         }
1036 
1037         @Override
1038         public int read(final byte[] b, final int off, int len) throws IOException {
1039             if (remaining <= 0) {
1040                 if (addDummyByte) {
1041                     addDummyByte = false;
1042                     b[off] = 0;
1043                     return 1;
1044                 }
1045                 return -1;
1046             }
1047 
1048             if (len <= 0) {
1049                 return 0;
1050             }
1051 
1052             if (len > remaining) {
1053                 len = (int) remaining;
1054             }
1055             int ret = -1;
1056             synchronized (archive) {
1057                 archive.seek(loc);
1058                 ret = archive.read(b, off, len);
1059             }
1060             if (ret > 0) {
1061                 loc += ret;
1062                 remaining -= ret;
1063             }
1064             return ret;
1065         }
1066 
1067         /**
1068          * Inflater needs an extra dummy byte for nowrap - see
1069          * Inflater's javadocs.
1070          */
1071         void addDummy() {
1072             addDummyByte = true;
1073         }
1074     }
1075 
1076     private static final class NameAndComment {
1077         private final byte[] name;
1078         private final byte[] comment;
1079         private NameAndComment(final byte[] name, final byte[] comment) {
1080             this.name = name;
1081             this.comment = comment;
1082         }
1083     }
1084 
1085     /**
1086      * Compares two ZipArchiveEntries based on their offset within the archive.
1087      *
1088      * <p>Won't return any meaningful results if one of the entries
1089      * isn't part of the archive at all.</p>
1090      *
1091      * @since 1.1
1092      */
1093     private final Comparator<ZipArchiveEntry> OFFSET_COMPARATOR =
1094         new Comparator<ZipArchiveEntry>() {
1095         @Override
1096         public int compare(final ZipArchiveEntry e1, final ZipArchiveEntry e2) {
1097             if (e1 == e2) {
1098                 return 0;
1099             }
1100 
1101             final Entry ent1 = e1 instanceof Entry ? (Entry) e1 : null;
1102             final Entry ent2 = e2 instanceof Entry ? (Entry) e2 : null;
1103             if (ent1 == null) {
1104                 return 1;
1105             }
1106             if (ent2 == null) {
1107                 return -1;
1108             }
1109             final long val = (ent1.getOffsetEntry().headerOffset
1110                         - ent2.getOffsetEntry().headerOffset);
1111             return val == 0 ? 0 : val < 0 ? -1 : +1;
1112         }
1113     };
1114 
1115     /**
1116      * Extends ZipArchiveEntry to store the offset within the archive.
1117      */
1118     private static class Entry extends ZipArchiveEntry {
1119 
1120         private final OffsetEntry offsetEntry;
1121 
1122         Entry(final OffsetEntry offset) {
1123             this.offsetEntry = offset;
1124         }
1125 
1126         OffsetEntry getOffsetEntry() {
1127             return offsetEntry;
1128         }
1129 
1130         @Override
1131         public int hashCode() {
1132             return 3 * super.hashCode()
1133                 + (int) (offsetEntry.headerOffset % Integer.MAX_VALUE);
1134         }
1135 
1136         @Override
1137         public boolean equals(final Object other) {
1138             if (super.equals(other)) {
1139                 // super.equals would return false if other were not an Entry
1140                 final Entry otherEntry = (Entry) other;
1141                 return offsetEntry.headerOffset
1142                         == otherEntry.offsetEntry.headerOffset
1143                     && offsetEntry.dataOffset
1144                         == otherEntry.offsetEntry.dataOffset;
1145             }
1146             return false;
1147         }
1148     }
1149 }