001/* ZipFile.java --
002   Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006
003   Free Software Foundation, Inc.
004
005This file is part of GNU Classpath.
006
007GNU Classpath is free software; you can redistribute it and/or modify
008it under the terms of the GNU General Public License as published by
009the Free Software Foundation; either version 2, or (at your option)
010any later version.
011
012GNU Classpath is distributed in the hope that it will be useful, but
013WITHOUT ANY WARRANTY; without even the implied warranty of
014MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015General Public License for more details.
016
017You should have received a copy of the GNU General Public License
018along with GNU Classpath; see the file COPYING.  If not, write to the
019Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02002110-1301 USA.
021
022Linking this library statically or dynamically with other modules is
023making a combined work based on this library.  Thus, the terms and
024conditions of the GNU General Public License cover the whole
025combination.
026
027As a special exception, the copyright holders of this library give you
028permission to link this library with independent modules to produce an
029executable, regardless of the license terms of these independent
030modules, and to copy and distribute the resulting executable under
031terms of your choice, provided that you also meet, for each linked
032independent module, the terms and conditions of the license of that
033module.  An independent module is a module which is not derived from
034or based on this library.  If you modify this library, you may extend
035this exception to your version of the library, but you are not
036obligated to do so.  If you do not wish to do so, delete this
037exception statement from your version. */
038
039
040package java.util.zip;
041
042import gnu.java.util.EmptyEnumeration;
043
044import java.io.EOFException;
045import java.io.File;
046import java.io.FileNotFoundException;
047import java.io.IOException;
048import java.io.InputStream;
049import java.io.RandomAccessFile;
050import java.io.UnsupportedEncodingException;
051import java.nio.ByteBuffer;
052import java.nio.charset.Charset;
053import java.nio.charset.CharsetDecoder;
054import java.util.Enumeration;
055import java.util.Iterator;
056import java.util.LinkedHashMap;
057
058/**
059 * This class represents a Zip archive.  You can ask for the contained
060 * entries, or get an input stream for a file entry.  The entry is
061 * automatically decompressed.
062 *
063 * This class is thread safe:  You can open input streams for arbitrary
064 * entries in different threads.
065 *
066 * @author Jochen Hoenicke
067 * @author Artur Biesiadowski
068 */
069public class ZipFile implements ZipConstants
070{
071
072  /**
073   * Mode flag to open a zip file for reading.
074   */
075  public static final int OPEN_READ = 0x1;
076
077  /**
078   * Mode flag to delete a zip file after reading.
079   */
080  public static final int OPEN_DELETE = 0x4;
081
082  /**
083   * This field isn't defined in the JDK's ZipConstants, but should be.
084   */
085  static final int ENDNRD =  4;
086
087  // Name of this zip file.
088  private final String name;
089
090  // File from which zip entries are read.
091  private final RandomAccessFile raf;
092
093  // The entries of this zip file when initialized and not yet closed.
094  private LinkedHashMap<String, ZipEntry> entries;
095
096  private boolean closed = false;
097
098
099  /**
100   * Helper function to open RandomAccessFile and throw the proper
101   * ZipException in case opening the file fails.
102   *
103   * @param name the file name, or null if file is provided
104   *
105   * @param file the file, or null if name is provided
106   *
107   * @return the newly open RandomAccessFile, never null
108   */
109  private RandomAccessFile openFile(String name,
110                                    File file)
111    throws ZipException, IOException
112  {
113    try
114      {
115        return
116          (name != null)
117          ? new RandomAccessFile(name, "r")
118          : new RandomAccessFile(file, "r");
119      }
120    catch (FileNotFoundException f)
121      {
122        ZipException ze = new ZipException(f.getMessage());
123        ze.initCause(f);
124        throw ze;
125      }
126  }
127
128
129  /**
130   * Opens a Zip file with the given name for reading.
131   * @exception IOException if a i/o error occured.
132   * @exception ZipException if the file doesn't contain a valid zip
133   * archive.
134   */
135  public ZipFile(String name) throws ZipException, IOException
136  {
137    this.raf = openFile(name,null);
138    this.name = name;
139    checkZipFile();
140  }
141
142  /**
143   * Opens a Zip file reading the given File.
144   * @exception IOException if a i/o error occured.
145   * @exception ZipException if the file doesn't contain a valid zip
146   * archive.
147   */
148  public ZipFile(File file) throws ZipException, IOException
149  {
150    this.raf = openFile(null,file);
151    this.name = file.getPath();
152    checkZipFile();
153  }
154
155  /**
156   * Opens a Zip file reading the given File in the given mode.
157   *
158   * If the OPEN_DELETE mode is specified, the zip file will be deleted at
159   * some time moment after it is opened. It will be deleted before the zip
160   * file is closed or the Virtual Machine exits.
161   *
162   * The contents of the zip file will be accessible until it is closed.
163   *
164   * @since JDK1.3
165   * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
166   *
167   * @exception IOException if a i/o error occured.
168   * @exception ZipException if the file doesn't contain a valid zip
169   * archive.
170   */
171  public ZipFile(File file, int mode) throws ZipException, IOException
172  {
173    if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE))
174      throw new IllegalArgumentException("invalid mode");
175    if ((mode & OPEN_DELETE) != 0)
176      file.deleteOnExit();
177    this.raf = openFile(null,file);
178    this.name = file.getPath();
179    checkZipFile();
180  }
181
182  private void checkZipFile() throws ZipException
183  {
184    boolean valid = false;
185
186    try
187      {
188        byte[] buf = new byte[4];
189        raf.readFully(buf);
190        int sig = buf[0] & 0xFF
191                | ((buf[1] & 0xFF) << 8)
192                | ((buf[2] & 0xFF) << 16)
193                | ((buf[3] & 0xFF) << 24);
194        valid = sig == LOCSIG;
195      }
196    catch (IOException _)
197      {
198      }
199
200    if (!valid)
201      {
202        try
203          {
204            raf.close();
205          }
206        catch (IOException _)
207          {
208          }
209        throw new ZipException("Not a valid zip file");
210      }
211  }
212
213  /**
214   * Checks if file is closed and throws an exception.
215   */
216  private void checkClosed()
217  {
218    if (closed)
219      throw new IllegalStateException("ZipFile has closed: " + name);
220  }
221
222  /**
223   * Read the central directory of a zip file and fill the entries
224   * array.  This is called exactly once when first needed. It is called
225   * while holding the lock on <code>raf</code>.
226   *
227   * @exception IOException if a i/o error occured.
228   * @exception ZipException if the central directory is malformed
229   */
230  private void readEntries() throws ZipException, IOException
231  {
232    /* Search for the End Of Central Directory.  When a zip comment is
233     * present the directory may start earlier.
234     * Note that a comment has a maximum length of 64K, so that is the
235     * maximum we search backwards.
236     */
237    PartialInputStream inp = new PartialInputStream(raf, 4096);
238    long pos = raf.length() - ENDHDR;
239    long top = Math.max(0, pos - 65536);
240    do
241      {
242        if (pos < top)
243          throw new ZipException
244            ("central directory not found, probably not a zip file: " + name);
245        inp.seek(pos--);
246      }
247    while (inp.readLeInt() != ENDSIG);
248
249    if (inp.skip(ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
250      throw new EOFException(name);
251    int count = inp.readLeShort();
252    if (inp.skip(ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
253      throw new EOFException(name);
254    int centralOffset = inp.readLeInt();
255
256    entries = new LinkedHashMap<String, ZipEntry> (count+count/2);
257    inp.seek(centralOffset);
258
259    for (int i = 0; i < count; i++)
260      {
261        if (inp.readLeInt() != CENSIG)
262          throw new ZipException("Wrong Central Directory signature: " + name);
263
264        inp.skip(6);
265        int method = inp.readLeShort();
266        int dostime = inp.readLeInt();
267        int crc = inp.readLeInt();
268        int csize = inp.readLeInt();
269        int size = inp.readLeInt();
270        int nameLen = inp.readLeShort();
271        int extraLen = inp.readLeShort();
272        int commentLen = inp.readLeShort();
273        inp.skip(8);
274        int offset = inp.readLeInt();
275        String name = inp.readString(nameLen);
276
277        ZipEntry entry = new ZipEntry(name);
278        entry.setMethod(method);
279        entry.setCrc(crc & 0xffffffffL);
280        entry.setSize(size & 0xffffffffL);
281        entry.setCompressedSize(csize & 0xffffffffL);
282        entry.setDOSTime(dostime);
283        if (extraLen > 0)
284          {
285            byte[] extra = new byte[extraLen];
286            inp.readFully(extra);
287            entry.setExtra(extra);
288          }
289        if (commentLen > 0)
290          {
291            entry.setComment(inp.readString(commentLen));
292          }
293        entry.offset = offset;
294        entries.put(name, entry);
295      }
296  }
297
298  /**
299   * Closes the ZipFile.  This also closes all input streams given by
300   * this class.  After this is called, no further method should be
301   * called.
302   *
303   * @exception IOException if a i/o error occured.
304   */
305  public void close() throws IOException
306  {
307    RandomAccessFile raf = this.raf;
308    if (raf == null)
309      return;
310
311    synchronized (raf)
312      {
313        closed = true;
314        entries = null;
315        raf.close();
316      }
317  }
318
319  /**
320   * Calls the <code>close()</code> method when this ZipFile has not yet
321   * been explicitly closed.
322   */
323  protected void finalize() throws IOException
324  {
325    if (!closed && raf != null) close();
326  }
327
328  /**
329   * Returns an enumeration of all Zip entries in this Zip file.
330   *
331   * @exception IllegalStateException when the ZipFile has already been closed
332   */
333  public Enumeration<? extends ZipEntry> entries()
334  {
335    checkClosed();
336
337    try
338      {
339        return new ZipEntryEnumeration(getEntries().values().iterator());
340      }
341    catch (IOException ioe)
342      {
343        return new EmptyEnumeration<ZipEntry>();
344      }
345  }
346
347  /**
348   * Checks that the ZipFile is still open and reads entries when necessary.
349   *
350   * @exception IllegalStateException when the ZipFile has already been closed.
351   * @exception IOException when the entries could not be read.
352   */
353  private LinkedHashMap<String, ZipEntry> getEntries() throws IOException
354  {
355    synchronized(raf)
356      {
357        checkClosed();
358
359        if (entries == null)
360          readEntries();
361
362        return entries;
363      }
364  }
365
366  /**
367   * Searches for a zip entry in this archive with the given name.
368   *
369   * @param name the name. May contain directory components separated by
370   * slashes ('/').
371   * @return the zip entry, or null if no entry with that name exists.
372   *
373   * @exception IllegalStateException when the ZipFile has already been closed
374   */
375  public ZipEntry getEntry(String name)
376  {
377    checkClosed();
378
379    try
380      {
381        LinkedHashMap<String, ZipEntry> entries = getEntries();
382        ZipEntry entry = entries.get(name);
383        // If we didn't find it, maybe it's a directory.
384        if (entry == null && !name.endsWith("/"))
385          entry = entries.get(name + '/');
386        return entry != null ? new ZipEntry(entry, name) : null;
387      }
388    catch (IOException ioe)
389      {
390        return null;
391      }
392  }
393
394  /**
395   * Creates an input stream reading the given zip entry as
396   * uncompressed data.  Normally zip entry should be an entry
397   * returned by getEntry() or entries().
398   *
399   * This implementation returns null if the requested entry does not
400   * exist.  This decision is not obviously correct, however, it does
401   * appear to mirror Sun's implementation, and it is consistant with
402   * their javadoc.  On the other hand, the old JCL book, 2nd Edition,
403   * claims that this should return a "non-null ZIP entry".  We have
404   * chosen for now ignore the old book, as modern versions of Ant (an
405   * important application) depend on this behaviour.  See discussion
406   * in this thread:
407   * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html
408   *
409   * @param entry the entry to create an InputStream for.
410   * @return the input stream, or null if the requested entry does not exist.
411   *
412   * @exception IllegalStateException when the ZipFile has already been closed
413   * @exception IOException if a i/o error occured.
414   * @exception ZipException if the Zip archive is malformed.
415   */
416  public InputStream getInputStream(ZipEntry entry) throws IOException
417  {
418    checkClosed();
419
420    LinkedHashMap<String, ZipEntry> entries = getEntries();
421    String name = entry.getName();
422    ZipEntry zipEntry = entries.get(name);
423    if (zipEntry == null)
424      return null;
425
426    PartialInputStream inp = new PartialInputStream(raf, 1024);
427    inp.seek(zipEntry.offset);
428
429    if (inp.readLeInt() != LOCSIG)
430      throw new ZipException("Wrong Local header signature: " + name);
431
432    inp.skip(4);
433
434    if (zipEntry.getMethod() != inp.readLeShort())
435      throw new ZipException("Compression method mismatch: " + name);
436
437    inp.skip(16);
438
439    int nameLen = inp.readLeShort();
440    int extraLen = inp.readLeShort();
441    inp.skip(nameLen + extraLen);
442
443    inp.setLength(zipEntry.getCompressedSize());
444
445    int method = zipEntry.getMethod();
446    switch (method)
447      {
448      case ZipOutputStream.STORED:
449        return inp;
450      case ZipOutputStream.DEFLATED:
451        inp.addDummyByte();
452        final Inflater inf = new Inflater(true);
453        final int sz = (int) entry.getSize();
454        return new InflaterInputStream(inp, inf)
455        {
456          public int available() throws IOException
457          {
458            if (sz == -1)
459              return super.available();
460            if (super.available() != 0)
461              return sz - inf.getTotalOut();
462            return 0;
463          }
464        };
465      default:
466        throw new ZipException("Unknown compression method " + method);
467      }
468  }
469
470  /**
471   * Returns the (path) name of this zip file.
472   */
473  public String getName()
474  {
475    return name;
476  }
477
478  /**
479   * Returns the number of entries in this zip file.
480   *
481   * @exception IllegalStateException when the ZipFile has already been closed
482   */
483  public int size()
484  {
485    checkClosed();
486
487    try
488      {
489        return getEntries().size();
490      }
491    catch (IOException ioe)
492      {
493        return 0;
494      }
495  }
496
497  private static class ZipEntryEnumeration implements Enumeration<ZipEntry>
498  {
499    private final Iterator<ZipEntry> elements;
500
501    public ZipEntryEnumeration(Iterator<ZipEntry> elements)
502    {
503      this.elements = elements;
504    }
505
506    public boolean hasMoreElements()
507    {
508      return elements.hasNext();
509    }
510
511    public ZipEntry nextElement()
512    {
513      /* We return a clone, just to be safe that the user doesn't
514       * change the entry.
515       */
516      return (ZipEntry) (elements.next().clone());
517    }
518  }
519
520  private static final class PartialInputStream extends InputStream
521  {
522    /**
523     * The UTF-8 charset use for decoding the filenames.
524     */
525    private static final Charset UTF8CHARSET = Charset.forName("UTF-8");
526
527    /**
528     * The actual UTF-8 decoder. Created on demand.
529     */
530    private CharsetDecoder utf8Decoder;
531
532    private final RandomAccessFile raf;
533    private final byte[] buffer;
534    private long bufferOffset;
535    private int pos;
536    private long end;
537    // We may need to supply an extra dummy byte to our reader.
538    // See Inflater.  We use a count here to simplify the logic
539    // elsewhere in this class.  Note that we ignore the dummy
540    // byte in methods where we know it is not needed.
541    private int dummyByteCount;
542
543    public PartialInputStream(RandomAccessFile raf, int bufferSize)
544      throws IOException
545    {
546      this.raf = raf;
547      buffer = new byte[bufferSize];
548      bufferOffset = -buffer.length;
549      pos = buffer.length;
550      end = raf.length();
551    }
552
553    void setLength(long length)
554    {
555      end = bufferOffset + pos + length;
556    }
557
558    private void fillBuffer() throws IOException
559    {
560      synchronized (raf)
561        {
562          long len = end - bufferOffset;
563          if (len == 0 && dummyByteCount > 0)
564            {
565              buffer[0] = 0;
566              dummyByteCount = 0;
567            }
568          else
569            {
570              raf.seek(bufferOffset);
571              raf.readFully(buffer, 0, (int) Math.min(buffer.length, len));
572            }
573        }
574    }
575
576    public int available()
577    {
578      long amount = end - (bufferOffset + pos);
579      if (amount > Integer.MAX_VALUE)
580        return Integer.MAX_VALUE;
581      return (int) amount;
582    }
583
584    public int read() throws IOException
585    {
586      if (bufferOffset + pos >= end + dummyByteCount)
587        return -1;
588      if (pos == buffer.length)
589        {
590          bufferOffset += buffer.length;
591          pos = 0;
592          fillBuffer();
593        }
594
595      return buffer[pos++] & 0xFF;
596    }
597
598    public int read(byte[] b, int off, int len) throws IOException
599    {
600      if (len > end + dummyByteCount - (bufferOffset + pos))
601        {
602          len = (int) (end + dummyByteCount - (bufferOffset + pos));
603          if (len == 0)
604            return -1;
605        }
606
607      int totalBytesRead = Math.min(buffer.length - pos, len);
608      System.arraycopy(buffer, pos, b, off, totalBytesRead);
609      pos += totalBytesRead;
610      off += totalBytesRead;
611      len -= totalBytesRead;
612
613      while (len > 0)
614        {
615          bufferOffset += buffer.length;
616          pos = 0;
617          fillBuffer();
618          int remain = Math.min(buffer.length, len);
619          System.arraycopy(buffer, pos, b, off, remain);
620          pos += remain;
621          off += remain;
622          len -= remain;
623          totalBytesRead += remain;
624        }
625
626      return totalBytesRead;
627    }
628
629    public long skip(long amount) throws IOException
630    {
631      if (amount < 0)
632        return 0;
633      if (amount > end - (bufferOffset + pos))
634        amount = end - (bufferOffset + pos);
635      seek(bufferOffset + pos + amount);
636      return amount;
637    }
638
639    void seek(long newpos) throws IOException
640    {
641      long offset = newpos - bufferOffset;
642      if (offset >= 0 && offset <= buffer.length)
643        {
644          pos = (int) offset;
645        }
646      else
647        {
648          bufferOffset = newpos;
649          pos = 0;
650          fillBuffer();
651        }
652    }
653
654    void readFully(byte[] buf) throws IOException
655    {
656      if (read(buf, 0, buf.length) != buf.length)
657        throw new EOFException();
658    }
659
660    void readFully(byte[] buf, int off, int len) throws IOException
661    {
662      if (read(buf, off, len) != len)
663        throw new EOFException();
664    }
665
666    int readLeShort() throws IOException
667    {
668      int result;
669      if(pos + 1 < buffer.length)
670        {
671          result = ((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8);
672          pos += 2;
673        }
674      else
675        {
676          int b0 = read();
677          int b1 = read();
678          if (b1 == -1)
679            throw new EOFException();
680          result = (b0 & 0xff) | (b1 & 0xff) << 8;
681        }
682      return result;
683    }
684
685    int readLeInt() throws IOException
686    {
687      int result;
688      if(pos + 3 < buffer.length)
689        {
690          result = (((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8)
691                   | ((buffer[pos + 2] & 0xff)
692                       | (buffer[pos + 3] & 0xff) << 8) << 16);
693          pos += 4;
694        }
695      else
696        {
697          int b0 = read();
698          int b1 = read();
699          int b2 = read();
700          int b3 = read();
701          if (b3 == -1)
702            throw new EOFException();
703          result =  (((b0 & 0xff) | (b1 & 0xff) << 8) | ((b2 & 0xff)
704                    | (b3 & 0xff) << 8) << 16);
705        }
706      return result;
707    }
708
709    /**
710     * Decode chars from byte buffer using UTF8 encoding.  This
711     * operation is performance-critical since a jar file contains a
712     * large number of strings for the name of each file in the
713     * archive.  This routine therefore avoids using the expensive
714     * utf8Decoder when decoding is straightforward.
715     *
716     * @param buffer the buffer that contains the encoded character
717     *        data
718     * @param pos the index in buffer of the first byte of the encoded
719     *        data
720     * @param length the length of the encoded data in number of
721     *        bytes.
722     *
723     * @return a String that contains the decoded characters.
724     */
725    private String decodeChars(byte[] buffer, int pos, int length)
726      throws IOException
727    {
728      String result;
729      int i=length - 1;
730      while ((i >= 0) && (buffer[i] <= 0x7f))
731        {
732          i--;
733        }
734      if (i < 0)
735        {
736          result = new String(buffer, 0, pos, length);
737        }
738      else
739        {
740          ByteBuffer bufferBuffer = ByteBuffer.wrap(buffer, pos, length);
741          if (utf8Decoder == null)
742            utf8Decoder = UTF8CHARSET.newDecoder();
743          utf8Decoder.reset();
744          char [] characters = utf8Decoder.decode(bufferBuffer).array();
745          result = String.valueOf(characters);
746        }
747      return result;
748    }
749
750    String readString(int length) throws IOException
751    {
752      if (length > end - (bufferOffset + pos))
753        throw new EOFException();
754
755      String result = null;
756      try
757        {
758          if (buffer.length - pos >= length)
759            {
760              result = decodeChars(buffer, pos, length);
761              pos += length;
762            }
763          else
764            {
765              byte[] b = new byte[length];
766              readFully(b);
767              result = decodeChars(b, 0, length);
768            }
769        }
770      catch (UnsupportedEncodingException uee)
771        {
772          throw new AssertionError(uee);
773        }
774      return result;
775    }
776
777    public void addDummyByte()
778    {
779      dummyByteCount = 1;
780    }
781  }
782}