2de51aa4bab3db9611dc2e28d0063e16f7166f63
[vamsas.git] / src / org / apache / tools / zip / ZipFile.java
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
19 package org.apache.tools.zip;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.RandomAccessFile;
25 import java.io.UnsupportedEncodingException;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.Enumeration;
29 import java.util.Hashtable;
30 import java.util.zip.Inflater;
31 import java.util.zip.InflaterInputStream;
32 import java.util.zip.ZipException;
33
34 /**
35  * Replacement for <code>java.util.ZipFile</code>.
36  *
37  * <p>This class adds support for file name encodings other than UTF-8
38  * (which is required to work on ZIP files created by native zip tools
39  * and is able to skip a preamble like the one found in self
40  * extracting archives.  Furthermore it returns instances of
41  * <code>org.apache.tools.zip.ZipEntry</code> instead of
42  * <code>java.util.zip.ZipEntry</code>.</p>
43  *
44  * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
45  * have to reimplement all methods anyway.  Like
46  * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
47  * covers and supports compressed and uncompressed entries.</p>
48  * <p>For the VAMSAS library, a new constructor was added to pass in
49  * existing RandomAccessFile directly. This allows the ZIPped data input
50  * from a file locked under the <code>java.nio.FileChannel.Lock</code>
51  * mechanism.</p>
52  * <p>The method signatures mimic the ones of
53  * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
54  *
55  * <ul>
56  *   <li>There is no getName method.</li>
57  *   <li>entries has been renamed to getEntries.</li>
58  *   <li>getEntries and getEntry return
59  *   <code>org.apache.tools.zip.ZipEntry</code> instances.</li>
60  *   <li>close is allowed to throw IOException.</li>
61  * </ul>
62  *
63  */
64 public class ZipFile {
65
66     /**
67      * Maps ZipEntrys to Longs, recording the offsets of the local
68      * file headers.
69      */
70     private Hashtable entries = new Hashtable(509);
71
72     /**
73      * Maps String to ZipEntrys, name -> actual entry.
74      */
75     private Hashtable nameMap = new Hashtable(509);
76
77     private static final class OffsetEntry {
78         private long headerOffset = -1;
79         private long dataOffset = -1;
80     }
81
82     /**
83      * The encoding to use for filenames and the file comment.
84      *
85      * <p>For a list of possible values see <a
86      * 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>.
87      * Defaults to the platform's default character encoding.</p>
88      */
89     private String encoding = null;
90
91     /**
92      * The actual data source.
93      */
94     private RandomAccessFile archive;
95
96     /**
97      * Opens the given file for reading, assuming the platform's
98      * native encoding for file names.
99      *
100      * @param f the archive.
101      *
102      * @throws IOException if an error occurs while reading the file.
103      */
104     public ZipFile(File f) throws IOException {
105         this(f, null);
106     }
107
108     /**
109      * Opens the given file for reading, assuming the platform's
110      * native encoding for file names.
111      *
112      * @param name name of the archive.
113      *
114      * @throws IOException if an error occurs while reading the file.
115      */
116     public ZipFile(String name) throws IOException {
117         this(new File(name), null);
118     }
119
120     /**
121      * Opens the given file for reading, assuming the specified
122      * encoding for file names.
123      *
124      * @param name name of the archive.
125      * @param encoding the encoding to use for file names
126      *
127      * @throws IOException if an error occurs while reading the file.
128      */
129     public ZipFile(String name, String encoding) throws IOException {
130         this(new File(name), encoding);
131     }
132
133     /**
134      * Opens the given file for reading, assuming the specified
135      * encoding for file names.
136      *
137      * @param f the archive.
138      * @param encoding the encoding to use for file names
139      *
140      * @throws IOException if an error occurs while reading the file.
141      */
142     public ZipFile(File f, String encoding) throws IOException {
143       this(new RandomAccessFile(f, "r"), encoding);
144     }
145     /**
146      * Read an archive from the given random access file
147      * 
148      * @param rafile the archive as a readable random access file
149      * @throws IOException
150      */
151     public ZipFile(RandomAccessFile rafile) throws IOException
152     {
153       this (rafile, null);
154     }
155     /**
156      * Read an archive from the given random access file, assuming the specified
157      * encoding for file names.
158      *
159      * @param readablearchive the archive opened as a readable random access file
160      * @param encoding the encoding to use for file names
161      * 
162      * @throws IOException if an error occurs while reading the file.
163      */
164     public ZipFile(RandomAccessFile readablearchive, String encoding) throws IOException
165     {
166       this.encoding = encoding;
167       archive = readablearchive;
168         try {
169             populateFromCentralDirectory();
170             resolveLocalFileHeaderData();
171         } catch (IOException e) {
172             try {
173                 archive.close();
174             } catch (IOException e2) {
175                 // swallow, throw the original exception instead
176             }
177             throw e;
178         }
179     }
180     /**
181      * The encoding to use for filenames and the file comment.
182      *
183      * @return null if using the platform's default character encoding.
184      */
185     public String getEncoding() {
186         return encoding;
187     }
188
189     /**
190      * Closes the archive.
191      * @throws IOException if an error occurs closing the archive.
192      */
193     public void close() throws IOException {
194         archive.close();
195     }
196
197     /**
198      * close a zipfile quietly; throw no io fault, do nothing
199      * on a null parameter
200      * @param zipfile file to close, can be null
201      */
202     public static void closeQuietly(ZipFile zipfile) {
203         if (zipfile != null) {
204             try {
205                 zipfile.close();
206             } catch (IOException e) {
207                 //ignore
208             }
209         }
210     }
211
212     /**
213      * Returns all entries.
214      * @return all entries as {@link ZipEntry} instances
215      */
216     public Enumeration getEntries() {
217         return entries.keys();
218     }
219
220     /**
221      * Returns a named entry - or <code>null</code> if no entry by
222      * that name exists.
223      * @param name name of the entry.
224      * @return the ZipEntry corresponding to the given name - or
225      * <code>null</code> if not present.
226      */
227     public ZipEntry getEntry(String name) {
228         return (ZipEntry) nameMap.get(name);
229     }
230
231     /**
232      * Returns an InputStream for reading the contents of the given entry.
233      * @param ze the entry to get the stream for.
234      * @return a stream to read the entry from.
235      * @throws IOException if unable to create an input stream from the zipenty
236      * @throws ZipException if the zipentry has an unsupported compression method
237      */
238     public InputStream getInputStream(ZipEntry ze)
239         throws IOException, ZipException {
240         OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
241         if (offsetEntry == null) {
242             return null;
243         }
244         long start = offsetEntry.dataOffset;
245         BoundedInputStream bis =
246             new BoundedInputStream(start, ze.getCompressedSize());
247         switch (ze.getMethod()) {
248             case ZipEntry.STORED:
249                 return bis;
250             case ZipEntry.DEFLATED:
251                 bis.addDummy();
252                 return new InflaterInputStream(bis, new Inflater(true));
253             default:
254                 throw new ZipException("Found unsupported compression method "
255                                        + ze.getMethod());
256         }
257     }
258
259     private static final int CFH_LEN =
260         /* version made by                 */ 2
261         /* version needed to extract       */ + 2
262         /* general purpose bit flag        */ + 2
263         /* compression method              */ + 2
264         /* last mod file time              */ + 2
265         /* last mod file date              */ + 2
266         /* crc-32                          */ + 4
267         /* compressed size                 */ + 4
268         /* uncompressed size               */ + 4
269         /* filename length                 */ + 2
270         /* extra field length              */ + 2
271         /* file comment length             */ + 2
272         /* disk number start               */ + 2
273         /* internal file attributes        */ + 2
274         /* external file attributes        */ + 4
275         /* relative offset of local header */ + 4;
276
277     /**
278      * Reads the central directory of the given archive and populates
279      * the internal tables with ZipEntry instances.
280      *
281      * <p>The ZipEntrys will know all data that can be obtained from
282      * the central directory alone, but not the data that requires the
283      * local file header or additional data to be read.</p>
284      */
285     private void populateFromCentralDirectory()
286         throws IOException {
287         positionAtCentralDirectory();
288
289         byte[] cfh = new byte[CFH_LEN];
290
291         byte[] signatureBytes = new byte[4];
292         archive.readFully(signatureBytes);
293         long sig = ZipLong.getValue(signatureBytes);
294         final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);
295         while (sig == cfhSig) {
296             archive.readFully(cfh);
297             int off = 0;
298             ZipEntry ze = new ZipEntry();
299
300             int versionMadeBy = ZipShort.getValue(cfh, off);
301             off += 2;
302             ze.setPlatform((versionMadeBy >> 8) & 0x0F);
303
304             off += 4; // skip version info and general purpose byte
305
306             ze.setMethod(ZipShort.getValue(cfh, off));
307             off += 2;
308
309             // FIXME this is actually not very cpu cycles friendly as we are converting from
310             // dos to java while the underlying Sun implementation will convert
311             // from java to dos time for internal storage...
312             long time = dosToJavaTime(ZipLong.getValue(cfh, off));
313             ze.setTime(time);
314             off += 4;
315
316             ze.setCrc(ZipLong.getValue(cfh, off));
317             off += 4;
318
319             ze.setCompressedSize(ZipLong.getValue(cfh, off));
320             off += 4;
321
322             ze.setSize(ZipLong.getValue(cfh, off));
323             off += 4;
324
325             int fileNameLen = ZipShort.getValue(cfh, off);
326             off += 2;
327
328             int extraLen = ZipShort.getValue(cfh, off);
329             off += 2;
330
331             int commentLen = ZipShort.getValue(cfh, off);
332             off += 2;
333
334             off += 2; // disk number
335
336             ze.setInternalAttributes(ZipShort.getValue(cfh, off));
337             off += 2;
338
339             ze.setExternalAttributes(ZipLong.getValue(cfh, off));
340             off += 4;
341
342             byte[] fileName = new byte[fileNameLen];
343             archive.readFully(fileName);
344             ze.setName(getString(fileName));
345
346
347             // LFH offset,
348             OffsetEntry offset = new OffsetEntry();
349             offset.headerOffset = ZipLong.getValue(cfh, off);
350             // data offset will be filled later
351             entries.put(ze, offset);
352
353             nameMap.put(ze.getName(), ze);
354
355             archive.skipBytes(extraLen);
356
357             byte[] comment = new byte[commentLen];
358             archive.readFully(comment);
359             ze.setComment(getString(comment));
360
361             archive.readFully(signatureBytes);
362             sig = ZipLong.getValue(signatureBytes);
363         }
364     }
365
366     private static final int MIN_EOCD_SIZE =
367         /* end of central dir signature    */ 4
368         /* number of this disk             */ + 2
369         /* number of the disk with the     */
370         /* start of the central directory  */ + 2
371         /* total number of entries in      */
372         /* the central dir on this disk    */ + 2
373         /* total number of entries in      */
374         /* the central dir                 */ + 2
375         /* size of the central directory   */ + 4
376         /* offset of start of central      */
377         /* directory with respect to       */
378         /* the starting disk number        */ + 4
379         /* zipfile comment length          */ + 2;
380
381     private static final int CFD_LOCATOR_OFFSET =
382         /* end of central dir signature    */ 4
383         /* number of this disk             */ + 2
384         /* number of the disk with the     */
385         /* start of the central directory  */ + 2
386         /* total number of entries in      */
387         /* the central dir on this disk    */ + 2
388         /* total number of entries in      */
389         /* the central dir                 */ + 2
390         /* size of the central directory   */ + 4;
391
392     /**
393      * Searches for the &quot;End of central dir record&quot;, parses
394      * it and positions the stream at the first central directory
395      * record.
396      */
397     private void positionAtCentralDirectory()
398         throws IOException {
399         boolean found = false;
400         long off = archive.length() - MIN_EOCD_SIZE;
401         if (off >= 0) {
402             archive.seek(off);
403             byte[] sig = ZipOutputStream.EOCD_SIG;
404             int curr = archive.read();
405             while (curr != -1) {
406                 if (curr == sig[0]) {
407                     curr = archive.read();
408                     if (curr == sig[1]) {
409                         curr = archive.read();
410                         if (curr == sig[2]) {
411                             curr = archive.read();
412                             if (curr == sig[3]) {
413                                 found = true;
414                                 break;
415                             }
416                         }
417                     }
418                 }
419                 archive.seek(--off);
420                 curr = archive.read();
421             }
422         }
423         if (!found) {
424             throw new ZipException("archive is not a ZIP archive");
425         }
426         archive.seek(off + CFD_LOCATOR_OFFSET);
427         byte[] cfdOffset = new byte[4];
428         archive.readFully(cfdOffset);
429         archive.seek(ZipLong.getValue(cfdOffset));
430     }
431
432     /**
433      * Number of bytes in local file header up to the &quot;length of
434      * filename&quot; entry.
435      */
436     private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
437         /* local file header signature     */ 4
438         /* version needed to extract       */ + 2
439         /* general purpose bit flag        */ + 2
440         /* compression method              */ + 2
441         /* last mod file time              */ + 2
442         /* last mod file date              */ + 2
443         /* crc-32                          */ + 4
444         /* compressed size                 */ + 4
445         /* uncompressed size               */ + 4;
446
447     /**
448      * Walks through all recorded entries and adds the data available
449      * from the local file header.
450      *
451      * <p>Also records the offsets for the data to read from the
452      * entries.</p>
453      */
454     private void resolveLocalFileHeaderData()
455         throws IOException {
456         Enumeration e = getEntries();
457         while (e.hasMoreElements()) {
458             ZipEntry ze = (ZipEntry) e.nextElement();
459             OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
460             long offset = offsetEntry.headerOffset;
461             archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
462             byte[] b = new byte[2];
463             archive.readFully(b);
464             int fileNameLen = ZipShort.getValue(b);
465             archive.readFully(b);
466             int extraFieldLen = ZipShort.getValue(b);
467             archive.skipBytes(fileNameLen);
468             byte[] localExtraData = new byte[extraFieldLen];
469             archive.readFully(localExtraData);
470             ze.setExtra(localExtraData);
471             /*dataOffsets.put(ze,
472                             new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
473                                      + 2 + 2 + fileNameLen + extraFieldLen));
474             */
475             offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
476                                      + 2 + 2 + fileNameLen + extraFieldLen;
477         }
478     }
479
480     /**
481      * Convert a DOS date/time field to a Date object.
482      *
483      * @param zipDosTime contains the stored DOS time.
484      * @return a Date instance corresponding to the given time.
485      */
486     protected static Date fromDosTime(ZipLong zipDosTime) {
487         long dosTime = zipDosTime.getValue();
488         return new Date(dosToJavaTime(dosTime));
489     }
490
491     /*
492      * Converts DOS time to Java time (number of milliseconds since epoch).
493      */
494     private static long dosToJavaTime(long dosTime) {
495         Calendar cal = Calendar.getInstance();
496         cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
497         cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
498         cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
499         cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
500         cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
501         cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
502         return cal.getTime().getTime();
503     }
504
505
506     /**
507      * Retrieve a String from the given bytes using the encoding set
508      * for this ZipFile.
509      *
510      * @param bytes the byte array to transform
511      * @return String obtained by using the given encoding
512      * @throws ZipException if the encoding cannot be recognized.
513      */
514     protected String getString(byte[] bytes) throws ZipException {
515         if (encoding == null) {
516             return new String(bytes);
517         } else {
518             try {
519                 return new String(bytes, encoding);
520             } catch (UnsupportedEncodingException uee) {
521                 throw new ZipException(uee.getMessage());
522             }
523         }
524     }
525
526     /**
527      * InputStream that delegates requests to the underlying
528      * RandomAccessFile, making sure that only bytes from a certain
529      * range can be read.
530      */
531     private class BoundedInputStream extends InputStream {
532         private long remaining;
533         private long loc;
534         private boolean addDummyByte = false;
535
536         BoundedInputStream(long start, long remaining) {
537             this.remaining = remaining;
538             loc = start;
539         }
540
541         public int read() throws IOException {
542             if (remaining-- <= 0) {
543                 if (addDummyByte) {
544                     addDummyByte = false;
545                     return 0;
546                 }
547                 return -1;
548             }
549             synchronized (archive) {
550                 archive.seek(loc++);
551                 return archive.read();
552             }
553         }
554
555         public int read(byte[] b, int off, int len) throws IOException {
556             if (remaining <= 0) {
557                 if (addDummyByte) {
558                     addDummyByte = false;
559                     b[off] = 0;
560                     return 1;
561                 }
562                 return -1;
563             }
564
565             if (len <= 0) {
566                 return 0;
567             }
568
569             if (len > remaining) {
570                 len = (int) remaining;
571             }
572             int ret = -1;
573             synchronized (archive) {
574                 archive.seek(loc);
575                 ret = archive.read(b, off, len);
576             }
577             if (ret > 0) {
578                 loc += ret;
579                 remaining -= ret;
580             }
581             return ret;
582         }
583
584         /**
585          * Inflater needs an extra dummy byte for nowrap - see
586          * Inflater's javadocs.
587          */
588         void addDummy() {
589             addDummyByte = true;
590         }
591     }
592
593 }