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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 package org.apache.tools.zip;
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;
35 * Replacement for <code>java.util.ZipFile</code>.
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>.
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
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.
58 * The method signatures mimic the ones of <code>java.util.zip.ZipFile</code>,
59 * with a couple of exceptions:
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>
66 * <li>close is allowed to throw IOException.</li>
70 public class ZipFile {
73 * Maps ZipEntrys to Longs, recording the offsets of the local file headers.
75 private Hashtable entries = new Hashtable(509);
78 * Maps String to ZipEntrys, name -> actual entry.
80 private Hashtable nameMap = new Hashtable(509);
82 private static final class OffsetEntry {
83 private long headerOffset = -1;
85 private long dataOffset = -1;
89 * The encoding to use for filenames and the file comment.
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.
98 private String encoding = null;
101 * The actual data source.
103 private RandomAccessFile archive;
106 * Opens the given file for reading, assuming the platform's native encoding
112 * @throws IOException
113 * if an error occurs while reading the file.
115 public ZipFile(File f) throws IOException {
120 * Opens the given file for reading, assuming the platform's native encoding
124 * name of the archive.
126 * @throws IOException
127 * if an error occurs while reading the file.
129 public ZipFile(String name) throws IOException {
130 this(new File(name), null);
134 * Opens the given file for reading, assuming the specified encoding for file
138 * name of the archive.
140 * the encoding to use for file names
142 * @throws IOException
143 * if an error occurs while reading the file.
145 public ZipFile(String name, String encoding) throws IOException {
146 this(new File(name), encoding);
150 * Opens the given file for reading, assuming the specified encoding for file
156 * the encoding to use for file names
158 * @throws IOException
159 * if an error occurs while reading the file.
161 public ZipFile(File f, String encoding) throws IOException {
162 this(new RandomAccessFile(f, "r"), encoding);
166 * Read an archive from the given random access file
169 * the archive as a readable random access file
170 * @throws IOException
172 public ZipFile(RandomAccessFile rafile) throws IOException {
177 * Read an archive from the given random access file, assuming the specified
178 * encoding for file names.
180 * @param readablearchive
181 * the archive opened as a readable random access file
183 * the encoding to use for file names
185 * @throws IOException
186 * if an error occurs while reading the file.
188 public ZipFile(RandomAccessFile readablearchive, String encoding)
190 this.encoding = encoding;
191 archive = readablearchive;
193 populateFromCentralDirectory();
194 resolveLocalFileHeaderData();
195 } catch (IOException e) {
198 } catch (IOException e2) {
199 // swallow, throw the original exception instead
206 * The encoding to use for filenames and the file comment.
208 * @return null if using the platform's default character encoding.
210 public String getEncoding() {
215 * Closes the archive.
217 * @throws IOException
218 * if an error occurs closing the archive.
220 public void close() throws IOException {
225 * close a zipfile quietly; throw no io fault, do nothing on a null parameter
228 * file to close, can be null
230 public static void closeQuietly(ZipFile zipfile) {
231 if (zipfile != null) {
234 } catch (IOException e) {
241 * Returns all entries.
243 * @return all entries as {@link ZipEntry} instances
245 public Enumeration getEntries() {
246 return entries.keys();
250 * Returns a named entry - or <code>null</code> if no entry by that name
255 * @return the ZipEntry corresponding to the given name - or <code>null</code>
258 public ZipEntry getEntry(String name) {
259 return (ZipEntry) nameMap.get(name);
263 * Returns an InputStream for reading the contents of the given entry.
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
273 public InputStream getInputStream(ZipEntry ze) throws IOException,
275 OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
276 if (offsetEntry == null) {
279 long start = offsetEntry.dataOffset;
280 BoundedInputStream bis = new BoundedInputStream(start, ze
281 .getCompressedSize());
282 switch (ze.getMethod()) {
283 case ZipEntry.STORED:
285 case ZipEntry.DEFLATED:
287 return new InflaterInputStream(bis, new Inflater(true));
289 throw new ZipException("Found unsupported compression method "
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
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;
313 * Reads the central directory of the given archive and populates the internal
314 * tables with ZipEntry instances.
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.
322 private void populateFromCentralDirectory() throws IOException {
323 positionAtCentralDirectory();
325 byte[] cfh = new byte[CFH_LEN];
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);
334 ZipEntry ze = new ZipEntry();
336 int versionMadeBy = ZipShort.getValue(cfh, off);
338 ze.setPlatform((versionMadeBy >> 8) & 0x0F);
340 off += 4; // skip version info and general purpose byte
342 ze.setMethod(ZipShort.getValue(cfh, off));
345 // FIXME this is actually not very cpu cycles friendly as we are
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));
353 ze.setCrc(ZipLong.getValue(cfh, off));
356 ze.setCompressedSize(ZipLong.getValue(cfh, off));
359 ze.setSize(ZipLong.getValue(cfh, off));
362 int fileNameLen = ZipShort.getValue(cfh, off);
365 int extraLen = ZipShort.getValue(cfh, off);
368 int commentLen = ZipShort.getValue(cfh, off);
371 off += 2; // disk number
373 ze.setInternalAttributes(ZipShort.getValue(cfh, off));
376 ze.setExternalAttributes(ZipLong.getValue(cfh, off));
379 byte[] fileName = new byte[fileNameLen];
380 archive.readFully(fileName);
381 ze.setName(getString(fileName));
384 OffsetEntry offset = new OffsetEntry();
385 offset.headerOffset = ZipLong.getValue(cfh, off);
386 // data offset will be filled later
387 entries.put(ze, offset);
389 nameMap.put(ze.getName(), ze);
391 archive.skipBytes(extraLen);
393 byte[] comment = new byte[commentLen];
394 archive.readFully(comment);
395 ze.setComment(getString(comment));
397 archive.readFully(signatureBytes);
398 sig = ZipLong.getValue(signatureBytes);
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;
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;
429 * Searches for the "End of central dir record", parses it and
430 * positions the stream at the first central directory record.
432 private void positionAtCentralDirectory() throws IOException {
433 boolean found = false;
434 long off = archive.length() - MIN_EOCD_SIZE;
437 byte[] sig = ZipOutputStream.EOCD_SIG;
438 int curr = archive.read();
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]) {
454 curr = archive.read();
458 throw new ZipException("archive is not a ZIP archive");
460 archive.seek(off + CFD_LOCATOR_OFFSET);
461 byte[] cfdOffset = new byte[4];
462 archive.readFully(cfdOffset);
463 archive.seek(ZipLong.getValue(cfdOffset));
467 * Number of bytes in local file header up to the "length of
468 * filename" entry.
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
478 /* compressed size */+ 4
479 /* uncompressed size */+ 4;
482 * Walks through all recorded entries and adds the data available from the
486 * Also records the offsets for the data to read from the entries.
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);
506 * dataOffsets.put(ze, new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH +
507 * 2 + 2 + fileNameLen + extraFieldLen));
509 offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH + 2 + 2
510 + fileNameLen + extraFieldLen;
515 * Convert a DOS date/time field to a Date object.
518 * contains the stored DOS time.
519 * @return a Date instance corresponding to the given time.
521 protected static Date fromDosTime(ZipLong zipDosTime) {
522 long dosTime = zipDosTime.getValue();
523 return new Date(dosToJavaTime(dosTime));
527 * Converts DOS time to Java time (number of milliseconds since epoch).
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();
541 * Retrieve a String from the given bytes using the encoding set for this
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.
550 protected String getString(byte[] bytes) throws ZipException {
551 if (encoding == null) {
552 return new String(bytes);
555 return new String(bytes, encoding);
556 } catch (UnsupportedEncodingException uee) {
557 throw new ZipException(uee.getMessage());
563 * InputStream that delegates requests to the underlying RandomAccessFile,
564 * making sure that only bytes from a certain range can be read.
566 private class BoundedInputStream extends InputStream {
567 private long remaining;
571 private boolean addDummyByte = false;
573 BoundedInputStream(long start, long remaining) {
574 this.remaining = remaining;
578 public int read() throws IOException {
579 if (remaining-- <= 0) {
581 addDummyByte = false;
586 synchronized (archive) {
588 return archive.read();
592 public int read(byte[] b, int off, int len) throws IOException {
593 if (remaining <= 0) {
595 addDummyByte = false;
606 if (len > remaining) {
607 len = (int) remaining;
610 synchronized (archive) {
612 ret = archive.read(b, off, len);
622 * Inflater needs an extra dummy byte for nowrap - see Inflater's javadocs.