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(4);
265        int flags = inp.readLeShort();
266        if ((flags & 1) != 0)
267          throw new ZipException("invalid CEN header (encrypted entry)");
268        int method = inp.readLeShort();
269        int dostime = inp.readLeInt();
270        int crc = inp.readLeInt();
271        int csize = inp.readLeInt();
272        int size = inp.readLeInt();
273        int nameLen = inp.readLeShort();
274        int extraLen = inp.readLeShort();
275        int commentLen = inp.readLeShort();
276        inp.skip(8);
277        int offset = inp.readLeInt();
278        String name = inp.readString(nameLen);
279
280        ZipEntry entry = new ZipEntry(name);
281        entry.setMethod(method);
282        entry.setCrc(crc & 0xffffffffL);
283        entry.setSize(size & 0xffffffffL);
284        entry.setCompressedSize(csize & 0xffffffffL);
285        entry.setDOSTime(dostime);
286        if (extraLen > 0)
287          {
288            byte[] extra = new byte[extraLen];
289            inp.readFully(extra);
290            entry.setExtra(extra);
291          }
292        if (commentLen > 0)
293          {
294            entry.setComment(inp.readString(commentLen));
295          }
296        entry.offset = offset;
297        entries.put(name, entry);
298      }
299  }
300
301  /**
302   * Closes the ZipFile.  This also closes all input streams given by
303   * this class.  After this is called, no further method should be
304   * called.
305   *
306   * @exception IOException if a i/o error occured.
307   */
308  public void close() throws IOException
309  {
310    RandomAccessFile raf = this.raf;
311    if (raf == null)
312      return;
313
314    synchronized (raf)
315      {
316        closed = true;
317        entries = null;
318        raf.close();
319      }
320  }
321
322  /**
323   * Calls the <code>close()</code> method when this ZipFile has not yet
324   * been explicitly closed.
325   */
326  protected void finalize() throws IOException
327  {
328    if (!closed && raf != null) close();
329  }
330
331  /**
332   * Returns an enumeration of all Zip entries in this Zip file.
333   *
334   * @exception IllegalStateException when the ZipFile has already been closed
335   */
336  public Enumeration<? extends ZipEntry> entries()
337  {
338    checkClosed();
339
340    try
341      {
342        return new ZipEntryEnumeration(getEntries().values().iterator());
343      }
344    catch (IOException ioe)
345      {
346        return new EmptyEnumeration<ZipEntry>();
347      }
348  }
349
350  /**
351   * Checks that the ZipFile is still open and reads entries when necessary.
352   *
353   * @exception IllegalStateException when the ZipFile has already been closed.
354   * @exception IOException when the entries could not be read.
355   */
356  private LinkedHashMap<String, ZipEntry> getEntries() throws IOException
357  {
358    synchronized(raf)
359      {
360        checkClosed();
361
362        if (entries == null)
363          readEntries();
364
365        return entries;
366      }
367  }
368
369  /**
370   * Searches for a zip entry in this archive with the given name.
371   *
372   * @param name the name. May contain directory components separated by
373   * slashes ('/').
374   * @return the zip entry, or null if no entry with that name exists.
375   *
376   * @exception IllegalStateException when the ZipFile has already been closed
377   */
378  public ZipEntry getEntry(String name)
379  {
380    checkClosed();
381
382    try
383      {
384        LinkedHashMap<String, ZipEntry> entries = getEntries();
385        ZipEntry entry = entries.get(name);
386        // If we didn't find it, maybe it's a directory.
387        if (entry == null && !name.endsWith("/"))
388          entry = entries.get(name + '/');
389        return entry != null ? new ZipEntry(entry, name) : null;
390      }
391    catch (IOException ioe)
392      {
393        return null;
394      }
395  }
396
397  /**
398   * Creates an input stream reading the given zip entry as
399   * uncompressed data.  Normally zip entry should be an entry
400   * returned by getEntry() or entries().
401   *
402   * This implementation returns null if the requested entry does not
403   * exist.  This decision is not obviously correct, however, it does
404   * appear to mirror Sun's implementation, and it is consistant with
405   * their javadoc.  On the other hand, the old JCL book, 2nd Edition,
406   * claims that this should return a "non-null ZIP entry".  We have
407   * chosen for now ignore the old book, as modern versions of Ant (an
408   * important application) depend on this behaviour.  See discussion
409   * in this thread:
410   * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html
411   *
412   * @param entry the entry to create an InputStream for.
413   * @return the input stream, or null if the requested entry does not exist.
414   *
415   * @exception IllegalStateException when the ZipFile has already been closed
416   * @exception IOException if a i/o error occured.
417   * @exception ZipException if the Zip archive is malformed.
418   */
419  public InputStream getInputStream(ZipEntry entry) throws IOException
420  {
421    checkClosed();
422
423    LinkedHashMap<String, ZipEntry> entries = getEntries();
424    String name = entry.getName();
425    ZipEntry zipEntry = entries.get(name);
426    if (zipEntry == null)
427      return null;
428
429    PartialInputStream inp = new PartialInputStream(raf, 1024);
430    inp.seek(zipEntry.offset);
431
432    if (inp.readLeInt() != LOCSIG)
433      throw new ZipException("Wrong Local header signature: " + name);
434
435    inp.skip(4);
436
437    if (zipEntry.getMethod() != inp.readLeShort())
438      throw new ZipException("Compression method mismatch: " + name);
439
440    inp.skip(16);
441
442    int nameLen = inp.readLeShort();
443    int extraLen = inp.readLeShort();
444    inp.skip(nameLen + extraLen);
445
446    inp.setLength(zipEntry.getCompressedSize());
447
448    int method = zipEntry.getMethod();
449    switch (method)
450      {
451      case ZipOutputStream.STORED:
452        return inp;
453      case ZipOutputStream.DEFLATED:
454        inp.addDummyByte();
455        final Inflater inf = new Inflater(true);
456        final int sz = (int) entry.getSize();
457        return new InflaterInputStream(inp, inf)
458        {
459          public int available() throws IOException
460          {
461            if (sz == -1)
462              return super.available();
463            if (super.available() != 0)
464              return sz - inf.getTotalOut();
465            return 0;
466          }
467        };
468      default:
469        throw new ZipException("Unknown compression method " + method);
470      }
471  }
472
473  /**
474   * Returns the (path) name of this zip file.
475   */
476  public String getName()
477  {
478    return name;
479  }
480
481  /**
482   * Returns the number of entries in this zip file.
483   *
484   * @exception IllegalStateException when the ZipFile has already been closed
485   */
486  public int size()
487  {
488    checkClosed();
489
490    try
491      {
492        return getEntries().size();
493      }
494    catch (IOException ioe)
495      {
496        return 0;
497      }
498  }
499
500  private static class ZipEntryEnumeration implements Enumeration<ZipEntry>
501  {
502    private final Iterator<ZipEntry> elements;
503
504    public ZipEntryEnumeration(Iterator<ZipEntry> elements)
505    {
506      this.elements = elements;
507    }
508
509    public boolean hasMoreElements()
510    {
511      return elements.hasNext();
512    }
513
514    public ZipEntry nextElement()
515    {
516      /* We return a clone, just to be safe that the user doesn't
517       * change the entry.
518       */
519      return (ZipEntry) (elements.next().clone());
520    }
521  }
522
523  private static final class PartialInputStream extends InputStream
524  {
525    /**
526     * The UTF-8 charset use for decoding the filenames.
527     */
528    private static final Charset UTF8CHARSET = Charset.forName("UTF-8");
529
530    /**
531     * The actual UTF-8 decoder. Created on demand.
532     */
533    private CharsetDecoder utf8Decoder;
534
535    private final RandomAccessFile raf;
536    private final byte[] buffer;
537    private long bufferOffset;
538    private int pos;
539    private long end;
540    // We may need to supply an extra dummy byte to our reader.
541    // See Inflater.  We use a count here to simplify the logic
542    // elsewhere in this class.  Note that we ignore the dummy
543    // byte in methods where we know it is not needed.
544    private int dummyByteCount;
545
546    public PartialInputStream(RandomAccessFile raf, int bufferSize)
547      throws IOException
548    {
549      this.raf = raf;
550      buffer = new byte[bufferSize];
551      bufferOffset = -buffer.length;
552      pos = buffer.length;
553      end = raf.length();
554    }
555
556    void setLength(long length)
557    {
558      end = bufferOffset + pos + length;
559    }
560
561    private void fillBuffer() throws IOException
562    {
563      synchronized (raf)
564        {
565          long len = end - bufferOffset;
566          if (len == 0 && dummyByteCount > 0)
567            {
568              buffer[0] = 0;
569              dummyByteCount = 0;
570            }
571          else
572            {
573              raf.seek(bufferOffset);
574              raf.readFully(buffer, 0, (int) Math.min(buffer.length, len));
575            }
576        }
577    }
578
579    public int available()
580    {
581      long amount = end - (bufferOffset + pos);
582      if (amount > Integer.MAX_VALUE)
583        return Integer.MAX_VALUE;
584      return (int) amount;
585    }
586
587    public int read() throws IOException
588    {
589      if (bufferOffset + pos >= end + dummyByteCount)
590        return -1;
591      if (pos == buffer.length)
592        {
593          bufferOffset += buffer.length;
594          pos = 0;
595          fillBuffer();
596        }
597
598      return buffer[pos++] & 0xFF;
599    }
600
601    public int read(byte[] b, int off, int len) throws IOException
602    {
603      if (len > end + dummyByteCount - (bufferOffset + pos))
604        {
605          len = (int) (end + dummyByteCount - (bufferOffset + pos));
606          if (len == 0)
607            return -1;
608        }
609
610      int totalBytesRead = Math.min(buffer.length - pos, len);
611      System.arraycopy(buffer, pos, b, off, totalBytesRead);
612      pos += totalBytesRead;
613      off += totalBytesRead;
614      len -= totalBytesRead;
615
616      while (len > 0)
617        {
618          bufferOffset += buffer.length;
619          pos = 0;
620          fillBuffer();
621          int remain = Math.min(buffer.length, len);
622          System.arraycopy(buffer, pos, b, off, remain);
623          pos += remain;
624          off += remain;
625          len -= remain;
626          totalBytesRead += remain;
627        }
628
629      return totalBytesRead;
630    }
631
632    public long skip(long amount) throws IOException
633    {
634      if (amount < 0)
635        return 0;
636      if (amount > end - (bufferOffset + pos))
637        amount = end - (bufferOffset + pos);
638      seek(bufferOffset + pos + amount);
639      return amount;
640    }
641
642    void seek(long newpos) throws IOException
643    {
644      long offset = newpos - bufferOffset;
645      if (offset >= 0 && offset <= buffer.length)
646        {
647          pos = (int) offset;
648        }
649      else
650        {
651          bufferOffset = newpos;
652          pos = 0;
653          fillBuffer();
654        }
655    }
656
657    void readFully(byte[] buf) throws IOException
658    {
659      if (read(buf, 0, buf.length) != buf.length)
660        throw new EOFException();
661    }
662
663    void readFully(byte[] buf, int off, int len) throws IOException
664    {
665      if (read(buf, off, len) != len)
666        throw new EOFException();
667    }
668
669    int readLeShort() throws IOException
670    {
671      int result;
672      if(pos + 1 < buffer.length)
673        {
674          result = ((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8);
675          pos += 2;
676        }
677      else
678        {
679          int b0 = read();
680          int b1 = read();
681          if (b1 == -1)
682            throw new EOFException();
683          result = (b0 & 0xff) | (b1 & 0xff) << 8;
684        }
685      return result;
686    }
687
688    int readLeInt() throws IOException
689    {
690      int result;
691      if(pos + 3 < buffer.length)
692        {
693          result = (((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8)
694                   | ((buffer[pos + 2] & 0xff)
695                       | (buffer[pos + 3] & 0xff) << 8) << 16);
696          pos += 4;
697        }
698      else
699        {
700          int b0 = read();
701          int b1 = read();
702          int b2 = read();
703          int b3 = read();
704          if (b3 == -1)
705            throw new EOFException();
706          result =  (((b0 & 0xff) | (b1 & 0xff) << 8) | ((b2 & 0xff)
707                    | (b3 & 0xff) << 8) << 16);
708        }
709      return result;
710    }
711
712    /**
713     * Decode chars from byte buffer using UTF8 encoding.  This
714     * operation is performance-critical since a jar file contains a
715     * large number of strings for the name of each file in the
716     * archive.  This routine therefore avoids using the expensive
717     * utf8Decoder when decoding is straightforward.
718     *
719     * @param buffer the buffer that contains the encoded character
720     *        data
721     * @param pos the index in buffer of the first byte of the encoded
722     *        data
723     * @param length the length of the encoded data in number of
724     *        bytes.
725     *
726     * @return a String that contains the decoded characters.
727     */
728    private String decodeChars(byte[] buffer, int pos, int length)
729      throws IOException
730    {
731      String result;
732      int i=length - 1;
733      while ((i >= 0) && (buffer[i] <= 0x7f))
734        {
735          i--;
736        }
737      if (i < 0)
738        {
739          result = new String(buffer, 0, pos, length);
740        }
741      else
742        {
743          ByteBuffer bufferBuffer = ByteBuffer.wrap(buffer, pos, length);
744          if (utf8Decoder == null)
745            utf8Decoder = UTF8CHARSET.newDecoder();
746          utf8Decoder.reset();
747          char [] characters = utf8Decoder.decode(bufferBuffer).array();
748          result = String.valueOf(characters);
749        }
750      return result;
751    }
752
753    String readString(int length) throws IOException
754    {
755      if (length > end - (bufferOffset + pos))
756        throw new EOFException();
757
758      String result = null;
759      try
760        {
761          if (buffer.length - pos >= length)
762            {
763              result = decodeChars(buffer, pos, length);
764              pos += length;
765            }
766          else
767            {
768              byte[] b = new byte[length];
769              readFully(b);
770              result = decodeChars(b, 0, length);
771            }
772        }
773      catch (UnsupportedEncodingException uee)
774        {
775          throw new AssertionError(uee);
776        }
777      return result;
778    }
779
780    public void addDummyByte()
781    {
782      dummyByteCount = 1;
783    }
784  }
785}