1 /* java.util.zip.ZipFile
2    Copyright (C) 2001, 2002, 2003 Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
19 02111-1307 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 package java.util.zip;
39 
40 import java.io.BufferedInputStream;
41 import java.io.DataInput;
42 import java.io.File;
43 import java.io.InputStream;
44 import java.io.IOException;
45 import java.io.EOFException;
46 import java.io.RandomAccessFile;
47 import java.util.Enumeration;
48 import java.util.HashMap;
49 import java.util.Iterator;
50 import java.util.NoSuchElementException;
51 
52 /**
53  * This class represents a Zip archive.  You can ask for the contained
54  * entries, or get an input stream for a file entry.  The entry is
55  * automatically decompressed.
56  *
57  * This class is thread safe:  You can open input streams for arbitrary
58  * entries in different threads.
59  *
60  * @author Jochen Hoenicke
61  * @author Artur Biesiadowski
62  */
63 public class ZipFile implements ZipConstants
64 {
65 
66   /**
67    * Mode flag to open a zip file for reading.
68    */
69   public static final int OPEN_READ = 0x1;
70 
71   /**
72    * Mode flag to delete a zip file after reading.
73    */
74   public static final int OPEN_DELETE = 0x4;
75 
76   // Name of this zip file.
77   private final String name;
78 
79   // File from which zip entries are read.
80   private final RandomAccessFile raf;
81 
82   // The entries of this zip file when initialized and not yet closed.
83   private HashMap entries;
84 
85   private boolean closed = false;
86 
87   /**
88    * Opens a Zip file with the given name for reading.
89    * @exception IOException if a i/o error occured.
90    * @exception ZipException if the file doesn't contain a valid zip
91    * archive.
92    */
ZipFile(String name)93   public ZipFile(String name) throws ZipException, IOException
94   {
95     this.raf = new RandomAccessFile(name, "r");
96     this.name = name;
97   }
98 
99   /**
100    * Opens a Zip file reading the given File.
101    * @exception IOException if a i/o error occured.
102    * @exception ZipException if the file doesn't contain a valid zip
103    * archive.
104    */
ZipFile(File file)105   public ZipFile(File file) throws ZipException, IOException
106   {
107     this.raf = new RandomAccessFile(file, "r");
108     this.name = file.getPath();
109   }
110 
111   /**
112    * Opens a Zip file reading the given File in the given mode.
113    *
114    * If the OPEN_DELETE mode is specified, the zip file will be deleted at
115    * some time moment after it is opened. It will be deleted before the zip
116    * file is closed or the Virtual Machine exits.
117    *
118    * The contents of the zip file will be accessible until it is closed.
119    *
120    * The OPEN_DELETE mode is currently unimplemented in this library
121    *
122    * @since JDK1.3
123    * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
124    *
125    * @exception IOException if a i/o error occured.
126    * @exception ZipException if the file doesn't contain a valid zip
127    * archive.
128    */
ZipFile(File file, int mode)129   public ZipFile(File file, int mode) throws ZipException, IOException
130   {
131     if ((mode & OPEN_DELETE) != 0)
132       {
133 	throw new IllegalArgumentException
134 	  ("OPEN_DELETE mode not supported yet in java.util.zip.ZipFile");
135       }
136     this.raf = new RandomAccessFile(file, "r");
137     this.name = file.getPath();
138   }
139 
140   /**
141    * Read an unsigned short in little endian byte order from the given
142    * DataInput stream using the given byte buffer.
143    *
144    * @param di DataInput stream to read from.
145    * @param b the byte buffer to read in (must be at least 2 bytes long).
146    * @return The value read.
147    *
148    * @exception IOException if a i/o error occured.
149    * @exception EOFException if the file ends prematurely
150    */
readLeShort(DataInput di, byte[] b)151   private final int readLeShort(DataInput di, byte[] b) throws IOException
152   {
153     di.readFully(b, 0, 2);
154     return (b[0] & 0xff) | (b[1] & 0xff) << 8;
155   }
156 
157   /**
158    * Read an int in little endian byte order from the given
159    * DataInput stream using the given byte buffer.
160    *
161    * @param di DataInput stream to read from.
162    * @param b the byte buffer to read in (must be at least 4 bytes long).
163    * @return The value read.
164    *
165    * @exception IOException if a i/o error occured.
166    * @exception EOFException if the file ends prematurely
167    */
readLeInt(DataInput di, byte[] b)168   private final int readLeInt(DataInput di, byte[] b) throws IOException
169   {
170     di.readFully(b, 0, 4);
171     return ((b[0] & 0xff) | (b[1] & 0xff) << 8)
172 	    | ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16;
173   }
174 
175 
176   /**
177    * Read an unsigned short in little endian byte order from the given
178    * byte buffer at the given offset.
179    *
180    * @param b the byte array to read from.
181    * @param off the offset to read from.
182    * @return The value read.
183    */
readLeShort(byte[] b, int off)184   private final int readLeShort(byte[] b, int off)
185   {
186     return (b[off] & 0xff) | (b[off+1] & 0xff) << 8;
187   }
188 
189   /**
190    * Read an int in little endian byte order from the given
191    * byte buffer at the given offset.
192    *
193    * @param b the byte array to read from.
194    * @param off the offset to read from.
195    * @return The value read.
196    */
readLeInt(byte[] b, int off)197   private final int readLeInt(byte[] b, int off)
198   {
199     return ((b[off] & 0xff) | (b[off+1] & 0xff) << 8)
200 	    | ((b[off+2] & 0xff) | (b[off+3] & 0xff) << 8) << 16;
201   }
202 
203 
204   /**
205    * Read the central directory of a zip file and fill the entries
206    * array.  This is called exactly once when first needed. It is called
207    * while holding the lock on <code>raf</code>.
208    *
209    * @exception IOException if a i/o error occured.
210    * @exception ZipException if the central directory is malformed
211    */
readEntries()212   private void readEntries() throws ZipException, IOException
213   {
214     /* Search for the End Of Central Directory.  When a zip comment is
215      * present the directory may start earlier.
216      * FIXME: This searches the whole file in a very slow manner if the
217      * file isn't a zip file.
218      */
219     long pos = raf.length() - ENDHDR;
220     byte[] ebs  = new byte[CENHDR];
221 
222     do
223       {
224 	if (pos < 0)
225 	  throw new ZipException
226 	    ("central directory not found, probably not a zip file: " + name);
227 	raf.seek(pos--);
228       }
229     while (readLeInt(raf, ebs) != ENDSIG);
230 
231     if (raf.skipBytes(ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
232       throw new EOFException(name);
233     int count = readLeShort(raf, ebs);
234     if (raf.skipBytes(ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
235       throw new EOFException(name);
236     int centralOffset = readLeInt(raf, ebs);
237 
238     entries = new HashMap(count+count/2);
239     raf.seek(centralOffset);
240 
241     byte[] buffer = new byte[16];
242     for (int i = 0; i < count; i++)
243       {
244       	raf.readFully(ebs);
245 	if (readLeInt(ebs, 0) != CENSIG)
246 	  throw new ZipException("Wrong Central Directory signature: " + name);
247 
248 	int method = readLeShort(ebs, CENHOW);
249 	int dostime = readLeInt(ebs, CENTIM);
250 	int crc = readLeInt(ebs, CENCRC);
251 	int csize = readLeInt(ebs, CENSIZ);
252 	int size = readLeInt(ebs, CENLEN);
253 	int nameLen = readLeShort(ebs, CENNAM);
254 	int extraLen = readLeShort(ebs, CENEXT);
255 	int commentLen = readLeShort(ebs, CENCOM);
256 
257 	int offset = readLeInt(ebs, CENOFF);
258 
259 	int needBuffer = Math.max(nameLen, commentLen);
260 	if (buffer.length < needBuffer)
261 	  buffer = new byte[needBuffer];
262 
263 	raf.readFully(buffer, 0, nameLen);
264 	String name = new String(buffer, 0, 0, nameLen);
265 
266 	ZipEntry entry = new ZipEntry(name);
267 	entry.setMethod(method);
268 	entry.setCrc(crc & 0xffffffffL);
269 	entry.setSize(size & 0xffffffffL);
270 	entry.setCompressedSize(csize & 0xffffffffL);
271 	entry.setDOSTime(dostime);
272 	if (extraLen > 0)
273 	  {
274 	    byte[] extra = new byte[extraLen];
275 	    raf.readFully(extra);
276 	    entry.setExtra(extra);
277 	  }
278 	if (commentLen > 0)
279 	  {
280 	    raf.readFully(buffer, 0, commentLen);
281 	    entry.setComment(new String(buffer, 0, commentLen));
282 	  }
283 	entry.offset = offset;
284 	entries.put(name, entry);
285       }
286   }
287 
288   /**
289    * Closes the ZipFile.  This also closes all input streams given by
290    * this class.  After this is called, no further method should be
291    * called.
292    *
293    * @exception IOException if a i/o error occured.
294    */
close()295   public void close() throws IOException
296   {
297     synchronized (raf)
298       {
299 	closed = true;
300 	entries = null;
301 	raf.close();
302       }
303   }
304 
305   /**
306    * Calls the <code>close()</code> method when this ZipFile has not yet
307    * been explicitly closed.
308    */
finalize()309   protected void finalize() throws IOException
310   {
311     if (!closed && raf != null) close();
312   }
313 
314   /**
315    * Returns an enumeration of all Zip entries in this Zip file.
316    */
entries()317   public Enumeration entries()
318   {
319     try
320       {
321 	return new ZipEntryEnumeration(getEntries().values().iterator());
322       }
323     catch (IOException ioe)
324       {
325 	return null;
326       }
327   }
328 
329   /**
330    * Checks that the ZipFile is still open and reads entries when necessary.
331    *
332    * @exception IllegalStateException when the ZipFile has already been closed.
333    * @exception IOEexception when the entries could not be read.
334    */
getEntries()335   private HashMap getEntries() throws IOException
336   {
337     synchronized(raf)
338       {
339 	if (closed)
340 	  throw new IllegalStateException("ZipFile has closed: " + name);
341 
342 	if (entries == null)
343 	  readEntries();
344 
345 	return entries;
346       }
347   }
348 
349   /**
350    * Searches for a zip entry in this archive with the given name.
351    *
352    * @param the name. May contain directory components separated by
353    * slashes ('/').
354    * @return the zip entry, or null if no entry with that name exists.
355    */
getEntry(String name)356   public ZipEntry getEntry(String name)
357   {
358     try
359       {
360 	HashMap entries = getEntries();
361 	ZipEntry entry = (ZipEntry) entries.get(name);
362 	return entry != null ? (ZipEntry) entry.clone() : null;
363       }
364     catch (IOException ioe)
365       {
366 	return null;
367       }
368   }
369 
370 
371   //access should be protected by synchronized(raf)
372   private byte[] locBuf = new byte[LOCHDR];
373 
374   /**
375    * Checks, if the local header of the entry at index i matches the
376    * central directory, and returns the offset to the data.
377    *
378    * @param entry to check.
379    * @return the start offset of the (compressed) data.
380    *
381    * @exception IOException if a i/o error occured.
382    * @exception ZipException if the local header doesn't match the
383    * central directory header
384    */
checkLocalHeader(ZipEntry entry)385   private long checkLocalHeader(ZipEntry entry) throws IOException
386   {
387     synchronized (raf)
388       {
389 	raf.seek(entry.offset);
390 	raf.readFully(locBuf);
391 
392 	if (readLeInt(locBuf, 0) != LOCSIG)
393 	  throw new ZipException("Wrong Local header signature: " + name);
394 
395 	if (entry.getMethod() != readLeShort(locBuf, LOCHOW))
396 	  throw new ZipException("Compression method mismatch: " + name);
397 
398 	if (entry.getName().length() != readLeShort(locBuf, LOCNAM))
399 	  throw new ZipException("file name length mismatch: " + name);
400 
401 	int extraLen = entry.getName().length() + readLeShort(locBuf, LOCEXT);
402 	return entry.offset + LOCHDR + extraLen;
403       }
404   }
405 
406   /**
407    * Creates an input stream reading the given zip entry as
408    * uncompressed data.  Normally zip entry should be an entry
409    * returned by getEntry() or entries().
410    *
411    * @param entry the entry to create an InputStream for.
412    * @return the input stream.
413    *
414    * @exception IOException if a i/o error occured.
415    * @exception ZipException if the Zip archive is malformed.
416    */
getInputStream(ZipEntry entry)417   public InputStream getInputStream(ZipEntry entry) throws IOException
418   {
419     HashMap entries = getEntries();
420     String name = entry.getName();
421     ZipEntry zipEntry = (ZipEntry) entries.get(name);
422     if (zipEntry == null)
423       throw new NoSuchElementException(name);
424 
425     long start = checkLocalHeader(zipEntry);
426     int method = zipEntry.getMethod();
427     InputStream is = new BufferedInputStream(new PartialInputStream
428       (raf, start, zipEntry.getCompressedSize()));
429     switch (method)
430       {
431       case ZipOutputStream.STORED:
432 	return is;
433       case ZipOutputStream.DEFLATED:
434 	return new InflaterInputStream(is, new Inflater(true));
435       default:
436 	throw new ZipException("Unknown compression method " + method);
437       }
438   }
439 
440   /**
441    * Returns the (path) name of this zip file.
442    */
getName()443   public String getName()
444   {
445     return name;
446   }
447 
448   /**
449    * Returns the number of entries in this zip file.
450    */
size()451   public int size()
452   {
453     try
454       {
455 	return getEntries().size();
456       }
457     catch (IOException ioe)
458       {
459 	return 0;
460       }
461   }
462 
463   private static class ZipEntryEnumeration implements Enumeration
464   {
465     private final Iterator elements;
466 
ZipEntryEnumeration(Iterator elements)467     public ZipEntryEnumeration(Iterator elements)
468     {
469       this.elements = elements;
470     }
471 
hasMoreElements()472     public boolean hasMoreElements()
473     {
474       return elements.hasNext();
475     }
476 
nextElement()477     public Object nextElement()
478     {
479       /* We return a clone, just to be safe that the user doesn't
480        * change the entry.
481        */
482       return ((ZipEntry)elements.next()).clone();
483     }
484   }
485 
486   private static class PartialInputStream extends InputStream
487   {
488     private final RandomAccessFile raf;
489     long filepos, end;
490 
PartialInputStream(RandomAccessFile raf, long start, long len)491     public PartialInputStream(RandomAccessFile raf, long start, long len)
492     {
493       this.raf = raf;
494       filepos = start;
495       end = start + len;
496     }
497 
available()498     public int available()
499     {
500       long amount = end - filepos;
501       if (amount > Integer.MAX_VALUE)
502 	return Integer.MAX_VALUE;
503       return (int) amount;
504     }
505 
read()506     public int read() throws IOException
507     {
508       if (filepos == end)
509 	return -1;
510       synchronized (raf)
511 	{
512 	  raf.seek(filepos++);
513 	  return raf.read();
514 	}
515     }
516 
read(byte[] b, int off, int len)517     public int read(byte[] b, int off, int len) throws IOException
518     {
519       if (len > end - filepos)
520 	{
521 	  len = (int) (end - filepos);
522 	  if (len == 0)
523 	    return -1;
524 	}
525       synchronized (raf)
526 	{
527 	  raf.seek(filepos);
528 	  int count = raf.read(b, off, len);
529 	  if (count > 0)
530 	    filepos += len;
531 	  return count;
532 	}
533     }
534 
skip(long amount)535     public long skip(long amount)
536     {
537       if (amount < 0)
538 	throw new IllegalArgumentException();
539       if (amount > end - filepos)
540 	amount = end - filepos;
541       filepos += amount;
542       return amount;
543     }
544   }
545 }
546