001    /* ZipFile.java --
002       Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006
003       Free Software Foundation, Inc.
004    
005    This file is part of GNU Classpath.
006    
007    GNU Classpath is free software; you can redistribute it and/or modify
008    it under the terms of the GNU General Public License as published by
009    the Free Software Foundation; either version 2, or (at your option)
010    any later version.
011    
012    GNU Classpath is distributed in the hope that it will be useful, but
013    WITHOUT ANY WARRANTY; without even the implied warranty of
014    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015    General Public License for more details.
016    
017    You should have received a copy of the GNU General Public License
018    along with GNU Classpath; see the file COPYING.  If not, write to the
019    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
020    02110-1301 USA.
021    
022    Linking this library statically or dynamically with other modules is
023    making a combined work based on this library.  Thus, the terms and
024    conditions of the GNU General Public License cover the whole
025    combination.
026    
027    As a special exception, the copyright holders of this library give you
028    permission to link this library with independent modules to produce an
029    executable, regardless of the license terms of these independent
030    modules, and to copy and distribute the resulting executable under
031    terms of your choice, provided that you also meet, for each linked
032    independent module, the terms and conditions of the license of that
033    module.  An independent module is a module which is not derived from
034    or based on this library.  If you modify this library, you may extend
035    this exception to your version of the library, but you are not
036    obligated to do so.  If you do not wish to do so, delete this
037    exception statement from your version. */
038    
039    
040    package java.util.zip;
041    
042    import gnu.java.util.EmptyEnumeration;
043    
044    import java.io.EOFException;
045    import java.io.File;
046    import java.io.FileNotFoundException;
047    import java.io.IOException;
048    import java.io.InputStream;
049    import java.io.RandomAccessFile;
050    import java.io.UnsupportedEncodingException;
051    import java.nio.ByteBuffer;
052    import java.nio.charset.Charset;
053    import java.nio.charset.CharsetDecoder;
054    import java.util.Enumeration;
055    import java.util.Iterator;
056    import 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     */
069    public 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    }