KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > tools > zip > ZipFile


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 JavaDoc;
22 import java.io.IOException JavaDoc;
23 import java.io.InputStream JavaDoc;
24 import java.io.RandomAccessFile JavaDoc;
25 import java.io.UnsupportedEncodingException JavaDoc;
26 import java.util.Calendar JavaDoc;
27 import java.util.Date JavaDoc;
28 import java.util.Enumeration JavaDoc;
29 import java.util.Hashtable JavaDoc;
30 import java.util.zip.Inflater JavaDoc;
31 import java.util.zip.InflaterInputStream JavaDoc;
32 import java.util.zip.ZipException JavaDoc;
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  *
49  * <p>The method signatures mimic the ones of
50  * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
51  *
52  * <ul>
53  * <li>There is no getName method.</li>
54  * <li>entries has been renamed to getEntries.</li>
55  * <li>getEntries and getEntry return
56  * <code>org.apache.tools.zip.ZipEntry</code> instances.</li>
57  * <li>close is allowed to throw IOException.</li>
58  * </ul>
59  *
60  */

61 public class ZipFile {
62
63     /**
64      * Maps ZipEntrys to Longs, recording the offsets of the local
65      * file headers.
66      */

67     private Hashtable JavaDoc entries = new Hashtable JavaDoc(509);
68
69     /**
70      * Maps String to ZipEntrys, name -> actual entry.
71      */

72     private Hashtable JavaDoc nameMap = new Hashtable JavaDoc(509);
73
74     private static final class OffsetEntry {
75         private long headerOffset = -1;
76         private long dataOffset = -1;
77     }
78
79     /**
80      * The encoding to use for filenames and the file comment.
81      *
82      * <p>For a list of possible values see <a
83      * 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>.
84      * Defaults to the platform's default character encoding.</p>
85      */

86     private String JavaDoc encoding = null;
87
88     /**
89      * The actual data source.
90      */

91     private RandomAccessFile JavaDoc archive;
92
93     /**
94      * Opens the given file for reading, assuming the platform's
95      * native encoding for file names.
96      *
97      * @param f the archive.
98      *
99      * @throws IOException if an error occurs while reading the file.
100      */

101     public ZipFile(File JavaDoc f) throws IOException JavaDoc {
102         this(f, null);
103     }
104
105     /**
106      * Opens the given file for reading, assuming the platform's
107      * native encoding for file names.
108      *
109      * @param name name of the archive.
110      *
111      * @throws IOException if an error occurs while reading the file.
112      */

113     public ZipFile(String JavaDoc name) throws IOException JavaDoc {
114         this(new File JavaDoc(name), null);
115     }
116
117     /**
118      * Opens the given file for reading, assuming the specified
119      * encoding for file names.
120      *
121      * @param name name of the archive.
122      * @param encoding the encoding to use for file names
123      *
124      * @throws IOException if an error occurs while reading the file.
125      */

126     public ZipFile(String JavaDoc name, String JavaDoc encoding) throws IOException JavaDoc {
127         this(new File JavaDoc(name), encoding);
128     }
129
130     /**
131      * Opens the given file for reading, assuming the specified
132      * encoding for file names.
133      *
134      * @param f the archive.
135      * @param encoding the encoding to use for file names
136      *
137      * @throws IOException if an error occurs while reading the file.
138      */

139     public ZipFile(File JavaDoc f, String JavaDoc encoding) throws IOException JavaDoc {
140         this.encoding = encoding;
141         archive = new RandomAccessFile JavaDoc(f, "r");
142         try {
143             populateFromCentralDirectory();
144             resolveLocalFileHeaderData();
145         } catch (IOException JavaDoc e) {
146             try {
147                 archive.close();
148             } catch (IOException JavaDoc e2) {
149                 // swallow, throw the original exception instead
150
}
151             throw e;
152         }
153     }
154
155     /**
156      * The encoding to use for filenames and the file comment.
157      *
158      * @return null if using the platform's default character encoding.
159      */

160     public String JavaDoc getEncoding() {
161         return encoding;
162     }
163
164     /**
165      * Closes the archive.
166      * @throws IOException if an error occurs closing the archive.
167      */

168     public void close() throws IOException JavaDoc {
169         archive.close();
170     }
171
172     /**
173      * close a zipfile quietly; throw no io fault, do nothing
174      * on a null parameter
175      * @param zipfile file to close, can be null
176      */

177     public static void closeQuietly(ZipFile zipfile) {
178         if (zipfile != null) {
179             try {
180                 zipfile.close();
181             } catch (IOException JavaDoc e) {
182                 //ignore
183
}
184         }
185     }
186
187     /**
188      * Returns all entries.
189      * @return all entries as {@link ZipEntry} instances
190      */

191     public Enumeration JavaDoc getEntries() {
192         return entries.keys();
193     }
194
195     /**
196      * Returns a named entry - or <code>null</code> if no entry by
197      * that name exists.
198      * @param name name of the entry.
199      * @return the ZipEntry corresponding to the given name - or
200      * <code>null</code> if not present.
201      */

202     public ZipEntry getEntry(String JavaDoc name) {
203         return (ZipEntry) nameMap.get(name);
204     }
205
206     /**
207      * Returns an InputStream for reading the contents of the given entry.
208      * @param ze the entry to get the stream for.
209      * @return a stream to read the entry from.
210      * @throws IOException if unable to create an input stream from the zipenty
211      * @throws ZipException if the zipentry has an unsupported compression method
212      */

213     public InputStream JavaDoc getInputStream(ZipEntry ze)
214         throws IOException JavaDoc, ZipException JavaDoc {
215         OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
216         if (offsetEntry == null) {
217             return null;
218         }
219         long start = offsetEntry.dataOffset;
220         BoundedInputStream bis =
221             new BoundedInputStream(start, ze.getCompressedSize());
222         switch (ze.getMethod()) {
223             case ZipEntry.STORED:
224                 return bis;
225             case ZipEntry.DEFLATED:
226                 bis.addDummy();
227                 return new InflaterInputStream JavaDoc(bis, new Inflater JavaDoc(true));
228             default:
229                 throw new ZipException JavaDoc("Found unsupported compression method "
230                                        + ze.getMethod());
231         }
232     }
233
234     private static final int CFH_LEN =
235         /* version made by */ 2
236         /* version needed to extract */ + 2
237         /* general purpose bit flag */ + 2
238         /* compression method */ + 2
239         /* last mod file time */ + 2
240         /* last mod file date */ + 2
241         /* crc-32 */ + 4
242         /* compressed size */ + 4
243         /* uncompressed size */ + 4
244         /* filename length */ + 2
245         /* extra field length */ + 2
246         /* file comment length */ + 2
247         /* disk number start */ + 2
248         /* internal file attributes */ + 2
249         /* external file attributes */ + 4
250         /* relative offset of local header */ + 4;
251
252     /**
253      * Reads the central directory of the given archive and populates
254      * the internal tables with ZipEntry instances.
255      *
256      * <p>The ZipEntrys will know all data that can be obtained from
257      * the central directory alone, but not the data that requires the
258      * local file header or additional data to be read.</p>
259      */

260     private void populateFromCentralDirectory()
261         throws IOException JavaDoc {
262         positionAtCentralDirectory();
263
264         byte[] cfh = new byte[CFH_LEN];
265
266         byte[] signatureBytes = new byte[4];
267         archive.readFully(signatureBytes);
268         long sig = ZipLong.getValue(signatureBytes);
269         final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);
270         while (sig == cfhSig) {
271             archive.readFully(cfh);
272             int off = 0;
273             ZipEntry ze = new ZipEntry();
274
275             int versionMadeBy = ZipShort.getValue(cfh, off);
276             off += 2;
277             ze.setPlatform((versionMadeBy >> 8) & 0x0F);
278
279             off += 4; // skip version info and general purpose byte
280

281             ze.setMethod(ZipShort.getValue(cfh, off));
282             off += 2;
283
284             // FIXME this is actually not very cpu cycles friendly as we are converting from
285
// dos to java while the underlying Sun implementation will convert
286
// from java to dos time for internal storage...
287
long time = dosToJavaTime(ZipLong.getValue(cfh, off));
288             ze.setTime(time);
289             off += 4;
290
291             ze.setCrc(ZipLong.getValue(cfh, off));
292             off += 4;
293
294             ze.setCompressedSize(ZipLong.getValue(cfh, off));
295             off += 4;
296
297             ze.setSize(ZipLong.getValue(cfh, off));
298             off += 4;
299
300             int fileNameLen = ZipShort.getValue(cfh, off);
301             off += 2;
302
303             int extraLen = ZipShort.getValue(cfh, off);
304             off += 2;
305
306             int commentLen = ZipShort.getValue(cfh, off);
307             off += 2;
308
309             off += 2; // disk number
310

311             ze.setInternalAttributes(ZipShort.getValue(cfh, off));
312             off += 2;
313
314             ze.setExternalAttributes(ZipLong.getValue(cfh, off));
315             off += 4;
316
317             byte[] fileName = new byte[fileNameLen];
318             archive.readFully(fileName);
319             ze.setName(getString(fileName));
320
321
322             // LFH offset,
323
OffsetEntry offset = new OffsetEntry();
324             offset.headerOffset = ZipLong.getValue(cfh, off);
325             // data offset will be filled later
326
entries.put(ze, offset);
327
328             nameMap.put(ze.getName(), ze);
329
330             archive.skipBytes(extraLen);
331
332             byte[] comment = new byte[commentLen];
333             archive.readFully(comment);
334             ze.setComment(getString(comment));
335
336             archive.readFully(signatureBytes);
337             sig = ZipLong.getValue(signatureBytes);
338         }
339     }
340
341     private static final int MIN_EOCD_SIZE =
342         /* end of central dir signature */ 4
343         /* number of this disk */ + 2
344         /* number of the disk with the */
345         /* start of the central directory */ + 2
346         /* total number of entries in */
347         /* the central dir on this disk */ + 2
348         /* total number of entries in */
349         /* the central dir */ + 2
350         /* size of the central directory */ + 4
351         /* offset of start of central */
352         /* directory with respect to */
353         /* the starting disk number */ + 4
354         /* zipfile comment length */ + 2;
355
356     private static final int CFD_LOCATOR_OFFSET =
357         /* end of central dir signature */ 4
358         /* number of this disk */ + 2
359         /* number of the disk with the */
360         /* start of the central directory */ + 2
361         /* total number of entries in */
362         /* the central dir on this disk */ + 2
363         /* total number of entries in */
364         /* the central dir */ + 2
365         /* size of the central directory */ + 4;
366
367     /**
368      * Searches for the &quot;End of central dir record&quot;, parses
369      * it and positions the stream at the first central directory
370      * record.
371      */

372     private void positionAtCentralDirectory()
373         throws IOException JavaDoc {
374         boolean found = false;
375         long off = archive.length() - MIN_EOCD_SIZE;
376         if (off >= 0) {
377             archive.seek(off);
378             byte[] sig = ZipOutputStream.EOCD_SIG;
379             int curr = archive.read();
380             while (curr != -1) {
381                 if (curr == sig[0]) {
382                     curr = archive.read();
383                     if (curr == sig[1]) {
384                         curr = archive.read();
385                         if (curr == sig[2]) {
386                             curr = archive.read();
387                             if (curr == sig[3]) {
388                                 found = true;
389                                 break;
390                             }
391                         }
392                     }
393                 }
394                 archive.seek(--off);
395                 curr = archive.read();
396             }
397         }
398         if (!found) {
399             throw new ZipException JavaDoc("archive is not a ZIP archive");
400         }
401         archive.seek(off + CFD_LOCATOR_OFFSET);
402         byte[] cfdOffset = new byte[4];
403         archive.readFully(cfdOffset);
404         archive.seek(ZipLong.getValue(cfdOffset));
405     }
406
407     /**
408      * Number of bytes in local file header up to the &quot;length of
409      * filename&quot; entry.
410      */

411     private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
412         /* local file header signature */ 4
413         /* version needed to extract */ + 2
414         /* general purpose bit flag */ + 2
415         /* compression method */ + 2
416         /* last mod file time */ + 2
417         /* last mod file date */ + 2
418         /* crc-32 */ + 4
419         /* compressed size */ + 4
420         /* uncompressed size */ + 4;
421
422     /**
423      * Walks through all recorded entries and adds the data available
424      * from the local file header.
425      *
426      * <p>Also records the offsets for the data to read from the
427      * entries.</p>
428      */

429     private void resolveLocalFileHeaderData()
430         throws IOException JavaDoc {
431         Enumeration JavaDoc e = getEntries();
432         while (e.hasMoreElements()) {
433             ZipEntry ze = (ZipEntry) e.nextElement();
434             OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
435             long offset = offsetEntry.headerOffset;
436             archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
437             byte[] b = new byte[2];
438             archive.readFully(b);
439             int fileNameLen = ZipShort.getValue(b);
440             archive.readFully(b);
441             int extraFieldLen = ZipShort.getValue(b);
442             archive.skipBytes(fileNameLen);
443             byte[] localExtraData = new byte[extraFieldLen];
444             archive.readFully(localExtraData);
445             ze.setExtra(localExtraData);
446             /*dataOffsets.put(ze,
447                             new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
448                                      + 2 + 2 + fileNameLen + extraFieldLen));
449             */

450             offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
451                                      + 2 + 2 + fileNameLen + extraFieldLen;
452         }
453     }
454
455     /**
456      * Convert a DOS date/time field to a Date object.
457      *
458      * @param zipDosTime contains the stored DOS time.
459      * @return a Date instance corresponding to the given time.
460      */

461     protected static Date JavaDoc fromDosTime(ZipLong zipDosTime) {
462         long dosTime = zipDosTime.getValue();
463         return new Date JavaDoc(dosToJavaTime(dosTime));
464     }
465
466     /*
467      * Converts DOS time to Java time (number of milliseconds since epoch).
468      */

469     private static long dosToJavaTime(long dosTime) {
470         Calendar JavaDoc cal = Calendar.getInstance();
471         cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
472         cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
473         cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
474         cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
475         cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
476         cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
477         return cal.getTime().getTime();
478     }
479
480
481     /**
482      * Retrieve a String from the given bytes using the encoding set
483      * for this ZipFile.
484      *
485      * @param bytes the byte array to transform
486      * @return String obtained by using the given encoding
487      * @throws ZipException if the encoding cannot be recognized.
488      */

489     protected String JavaDoc getString(byte[] bytes) throws ZipException JavaDoc {
490         if (encoding == null) {
491             return new String JavaDoc(bytes);
492         } else {
493             try {
494                 return new String JavaDoc(bytes, encoding);
495             } catch (UnsupportedEncodingException JavaDoc uee) {
496                 throw new ZipException JavaDoc(uee.getMessage());
497             }
498         }
499     }
500
501     /**
502      * InputStream that delegates requests to the underlying
503      * RandomAccessFile, making sure that only bytes from a certain
504      * range can be read.
505      */

506     private class BoundedInputStream extends InputStream JavaDoc {
507         private long remaining;
508         private long loc;
509         private boolean addDummyByte = false;
510
511         BoundedInputStream(long start, long remaining) {
512             this.remaining = remaining;
513             loc = start;
514         }
515
516         public int read() throws IOException JavaDoc {
517             if (remaining-- <= 0) {
518                 if (addDummyByte) {
519                     addDummyByte = false;
520                     return 0;
521                 }
522                 return -1;
523             }
524             synchronized (archive) {
525                 archive.seek(loc++);
526                 return archive.read();
527             }
528         }
529
530         public int read(byte[] b, int off, int len) throws IOException JavaDoc {
531             if (remaining <= 0) {
532                 if (addDummyByte) {
533                     addDummyByte = false;
534                     b[off] = 0;
535                     return 1;
536                 }
537                 return -1;
538             }
539
540             if (len <= 0) {
541                 return 0;
542             }
543
544             if (len > remaining) {
545                 len = (int) remaining;
546             }
547             int ret = -1;
548             synchronized (archive) {
549                 archive.seek(loc);
550                 ret = archive.read(b, off, len);
551             }
552             if (ret > 0) {
553                 loc += ret;
554                 remaining -= ret;
555             }
556             return ret;
557         }
558
559         /**
560          * Inflater needs an extra dummy byte for nowrap - see
561          * Inflater's javadocs.
562          */

563         void addDummy() {
564             addDummyByte = true;
565         }
566     }
567
568 }
569
Popular Tags