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