Source for java.util.zip.ZipFile

   1: /* ZipFile.java --
   2:    Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006
   3:    Free Software Foundation, Inc.
   4: 
   5: This file is part of GNU Classpath.
   6: 
   7: GNU Classpath is free software; you can redistribute it and/or modify
   8: it under the terms of the GNU General Public License as published by
   9: the Free Software Foundation; either version 2, or (at your option)
  10: any later version.
  11: 
  12: GNU Classpath is distributed in the hope that it will be useful, but
  13: WITHOUT ANY WARRANTY; without even the implied warranty of
  14: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  15: General Public License for more details.
  16: 
  17: You should have received a copy of the GNU General Public License
  18: along with GNU Classpath; see the file COPYING.  If not, write to the
  19: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  20: 02110-1301 USA.
  21: 
  22: Linking this library statically or dynamically with other modules is
  23: making a combined work based on this library.  Thus, the terms and
  24: conditions of the GNU General Public License cover the whole
  25: combination.
  26: 
  27: As a special exception, the copyright holders of this library give you
  28: permission to link this library with independent modules to produce an
  29: executable, regardless of the license terms of these independent
  30: modules, and to copy and distribute the resulting executable under
  31: terms of your choice, provided that you also meet, for each linked
  32: independent module, the terms and conditions of the license of that
  33: module.  An independent module is a module which is not derived from
  34: or based on this library.  If you modify this library, you may extend
  35: this exception to your version of the library, but you are not
  36: obligated to do so.  If you do not wish to do so, delete this
  37: exception statement from your version. */
  38: 
  39: 
  40: package java.util.zip;
  41: 
  42: import gnu.java.util.EmptyEnumeration;
  43: 
  44: import java.io.EOFException;
  45: import java.io.File;
  46: import java.io.FileNotFoundException;
  47: import java.io.IOException;
  48: import java.io.InputStream;
  49: import java.io.RandomAccessFile;
  50: import java.io.UnsupportedEncodingException;
  51: import java.util.Enumeration;
  52: import java.util.Iterator;
  53: import java.util.LinkedHashMap;
  54: 
  55: /**
  56:  * This class represents a Zip archive.  You can ask for the contained
  57:  * entries, or get an input stream for a file entry.  The entry is
  58:  * automatically decompressed.
  59:  *
  60:  * This class is thread safe:  You can open input streams for arbitrary
  61:  * entries in different threads.
  62:  *
  63:  * @author Jochen Hoenicke
  64:  * @author Artur Biesiadowski
  65:  */
  66: public class ZipFile implements ZipConstants
  67: {
  68: 
  69:   /**
  70:    * Mode flag to open a zip file for reading.
  71:    */
  72:   public static final int OPEN_READ = 0x1;
  73: 
  74:   /**
  75:    * Mode flag to delete a zip file after reading.
  76:    */
  77:   public static final int OPEN_DELETE = 0x4;
  78: 
  79:   /**
  80:    * This field isn't defined in the JDK's ZipConstants, but should be.
  81:    */
  82:   static final int ENDNRD =  4;
  83: 
  84:   // Name of this zip file.
  85:   private final String name;
  86: 
  87:   // File from which zip entries are read.
  88:   private final RandomAccessFile raf;
  89: 
  90:   // The entries of this zip file when initialized and not yet closed.
  91:   private LinkedHashMap entries;
  92: 
  93:   private boolean closed = false;
  94: 
  95: 
  96:   /**
  97:    * Helper function to open RandomAccessFile and throw the proper
  98:    * ZipException in case opening the file fails.
  99:    *
 100:    * @param name the file name, or null if file is provided
 101:    *
 102:    * @param file the file, or null if name is provided
 103:    *
 104:    * @return the newly open RandomAccessFile, never null
 105:    */
 106:   private RandomAccessFile openFile(String name, 
 107:                                     File file) 
 108:     throws ZipException, IOException
 109:   {                                       
 110:     try 
 111:       {
 112:         return 
 113:           (name != null)
 114:           ? new RandomAccessFile(name, "r")
 115:           : new RandomAccessFile(file, "r");
 116:       }
 117:     catch (FileNotFoundException f)
 118:       { 
 119:         ZipException ze = new ZipException(f.getMessage());
 120:         ze.initCause(f);
 121:         throw ze;
 122:       }
 123:   }
 124: 
 125: 
 126:   /**
 127:    * Opens a Zip file with the given name for reading.
 128:    * @exception IOException if a i/o error occured.
 129:    * @exception ZipException if the file doesn't contain a valid zip
 130:    * archive.  
 131:    */
 132:   public ZipFile(String name) throws ZipException, IOException
 133:   {
 134:     this.raf = openFile(name,null);
 135:     this.name = name;
 136:     checkZipFile();
 137:   }
 138: 
 139:   /**
 140:    * Opens a Zip file reading the given File.
 141:    * @exception IOException if a i/o error occured.
 142:    * @exception ZipException if the file doesn't contain a valid zip
 143:    * archive.  
 144:    */
 145:   public ZipFile(File file) throws ZipException, IOException
 146:   {
 147:     this.raf = openFile(null,file);
 148:     this.name = file.getPath();
 149:     checkZipFile();
 150:   }
 151: 
 152:   /**
 153:    * Opens a Zip file reading the given File in the given mode.
 154:    *
 155:    * If the OPEN_DELETE mode is specified, the zip file will be deleted at
 156:    * some time moment after it is opened. It will be deleted before the zip
 157:    * file is closed or the Virtual Machine exits.
 158:    * 
 159:    * The contents of the zip file will be accessible until it is closed.
 160:    *
 161:    * @since JDK1.3
 162:    * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
 163:    *
 164:    * @exception IOException if a i/o error occured.
 165:    * @exception ZipException if the file doesn't contain a valid zip
 166:    * archive.  
 167:    */
 168:   public ZipFile(File file, int mode) throws ZipException, IOException
 169:   {
 170:     if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE))
 171:       throw new IllegalArgumentException("invalid mode");
 172:     if ((mode & OPEN_DELETE) != 0)
 173:       file.deleteOnExit();
 174:     this.raf = openFile(null,file);
 175:     this.name = file.getPath();
 176:     checkZipFile();
 177:   }
 178: 
 179:   private void checkZipFile() throws ZipException
 180:   {
 181:     boolean valid = false;
 182: 
 183:     try 
 184:       {
 185:         byte[] buf = new byte[4];
 186:         raf.readFully(buf);
 187:         int sig = buf[0] & 0xFF
 188:                 | ((buf[1] & 0xFF) << 8)
 189:                 | ((buf[2] & 0xFF) << 16)
 190:                 | ((buf[3] & 0xFF) << 24);
 191:         valid = sig == LOCSIG;
 192:       }
 193:     catch (IOException _)
 194:       {
 195:       } 
 196: 
 197:     if (!valid)
 198:       {
 199:         try
 200:           {
 201:         raf.close();
 202:           }
 203:         catch (IOException _)
 204:           {
 205:           }
 206:     throw new ZipException("Not a valid zip file");
 207:       }
 208:   }
 209: 
 210:   /**
 211:    * Checks if file is closed and throws an exception.
 212:    */
 213:   private void checkClosed()
 214:   {
 215:     if (closed)
 216:       throw new IllegalStateException("ZipFile has closed: " + name);
 217:   }
 218: 
 219:   /**
 220:    * Read the central directory of a zip file and fill the entries
 221:    * array.  This is called exactly once when first needed. It is called
 222:    * while holding the lock on <code>raf</code>.
 223:    *
 224:    * @exception IOException if a i/o error occured.
 225:    * @exception ZipException if the central directory is malformed 
 226:    */
 227:   private void readEntries() throws ZipException, IOException
 228:   {
 229:     /* Search for the End Of Central Directory.  When a zip comment is 
 230:      * present the directory may start earlier.
 231:      * Note that a comment has a maximum length of 64K, so that is the
 232:      * maximum we search backwards.
 233:      */
 234:     PartialInputStream inp = new PartialInputStream(raf, 4096);
 235:     long pos = raf.length() - ENDHDR;
 236:     long top = Math.max(0, pos - 65536);
 237:     do
 238:       {
 239:     if (pos < top)
 240:       throw new ZipException
 241:         ("central directory not found, probably not a zip file: " + name);
 242:     inp.seek(pos--);
 243:       }
 244:     while (inp.readLeInt() != ENDSIG);
 245:     
 246:     if (inp.skip(ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
 247:       throw new EOFException(name);
 248:     int count = inp.readLeShort();
 249:     if (inp.skip(ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
 250:       throw new EOFException(name);
 251:     int centralOffset = inp.readLeInt();
 252: 
 253:     entries = new LinkedHashMap(count+count/2);
 254:     inp.seek(centralOffset);
 255:     
 256:     for (int i = 0; i < count; i++)
 257:       {
 258:     if (inp.readLeInt() != CENSIG)
 259:       throw new ZipException("Wrong Central Directory signature: " + name);
 260: 
 261:         inp.skip(6);
 262:     int method = inp.readLeShort();
 263:     int dostime = inp.readLeInt();
 264:     int crc = inp.readLeInt();
 265:     int csize = inp.readLeInt();
 266:     int size = inp.readLeInt();
 267:     int nameLen = inp.readLeShort();
 268:     int extraLen = inp.readLeShort();
 269:     int commentLen = inp.readLeShort();
 270:         inp.skip(8);
 271:     int offset = inp.readLeInt();
 272:     String name = inp.readString(nameLen);
 273: 
 274:     ZipEntry entry = new ZipEntry(name);
 275:     entry.setMethod(method);
 276:     entry.setCrc(crc & 0xffffffffL);
 277:     entry.setSize(size & 0xffffffffL);
 278:     entry.setCompressedSize(csize & 0xffffffffL);
 279:     entry.setDOSTime(dostime);
 280:     if (extraLen > 0)
 281:       {
 282:         byte[] extra = new byte[extraLen];
 283:         inp.readFully(extra);
 284:         entry.setExtra(extra);
 285:       }
 286:     if (commentLen > 0)
 287:       {
 288:             entry.setComment(inp.readString(commentLen));
 289:       }
 290:     entry.offset = offset;
 291:     entries.put(name, entry);
 292:       }
 293:   }
 294: 
 295:   /**
 296:    * Closes the ZipFile.  This also closes all input streams given by
 297:    * this class.  After this is called, no further method should be
 298:    * called.
 299:    * 
 300:    * @exception IOException if a i/o error occured.
 301:    */
 302:   public void close() throws IOException
 303:   {
 304:     RandomAccessFile raf = this.raf;
 305:     if (raf == null)
 306:       return;
 307: 
 308:     synchronized (raf)
 309:       {
 310:     closed = true;
 311:     entries = null;
 312:     raf.close();
 313:       }
 314:   }
 315: 
 316:   /**
 317:    * Calls the <code>close()</code> method when this ZipFile has not yet
 318:    * been explicitly closed.
 319:    */
 320:   protected void finalize() throws IOException
 321:   {
 322:     if (!closed && raf != null) close();
 323:   }
 324: 
 325:   /**
 326:    * Returns an enumeration of all Zip entries in this Zip file.
 327:    *
 328:    * @exception IllegalStateException when the ZipFile has already been closed
 329:    */
 330:   public Enumeration entries()
 331:   {
 332:     checkClosed();
 333:     
 334:     try
 335:       {
 336:     return new ZipEntryEnumeration(getEntries().values().iterator());
 337:       }
 338:     catch (IOException ioe)
 339:       {
 340:     return EmptyEnumeration.getInstance();
 341:       }
 342:   }
 343: 
 344:   /**
 345:    * Checks that the ZipFile is still open and reads entries when necessary.
 346:    *
 347:    * @exception IllegalStateException when the ZipFile has already been closed.
 348:    * @exception IOException when the entries could not be read.
 349:    */
 350:   private LinkedHashMap getEntries() throws IOException
 351:   {
 352:     synchronized(raf)
 353:       {
 354:     checkClosed();
 355: 
 356:     if (entries == null)
 357:       readEntries();
 358: 
 359:     return entries;
 360:       }
 361:   }
 362: 
 363:   /**
 364:    * Searches for a zip entry in this archive with the given name.
 365:    *
 366:    * @param name the name. May contain directory components separated by
 367:    * slashes ('/').
 368:    * @return the zip entry, or null if no entry with that name exists.
 369:    *
 370:    * @exception IllegalStateException when the ZipFile has already been closed
 371:    */
 372:   public ZipEntry getEntry(String name)
 373:   {
 374:     checkClosed();
 375: 
 376:     try
 377:       {
 378:     LinkedHashMap entries = getEntries();
 379:     ZipEntry entry = (ZipEntry) entries.get(name);
 380:         // If we didn't find it, maybe it's a directory.
 381:         if (entry == null && !name.endsWith("/"))
 382:             entry = (ZipEntry) entries.get(name + '/');
 383:     return entry != null ? new ZipEntry(entry, name) : null;
 384:       }
 385:     catch (IOException ioe)
 386:       {
 387:     return null;
 388:       }
 389:   }
 390: 
 391:   /**
 392:    * Creates an input stream reading the given zip entry as
 393:    * uncompressed data.  Normally zip entry should be an entry
 394:    * returned by getEntry() or entries().
 395:    *
 396:    * This implementation returns null if the requested entry does not
 397:    * exist.  This decision is not obviously correct, however, it does
 398:    * appear to mirror Sun's implementation, and it is consistant with
 399:    * their javadoc.  On the other hand, the old JCL book, 2nd Edition,
 400:    * claims that this should return a "non-null ZIP entry".  We have
 401:    * chosen for now ignore the old book, as modern versions of Ant (an
 402:    * important application) depend on this behaviour.  See discussion
 403:    * in this thread:
 404:    * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html
 405:    *
 406:    * @param entry the entry to create an InputStream for.
 407:    * @return the input stream, or null if the requested entry does not exist.
 408:    *
 409:    * @exception IllegalStateException when the ZipFile has already been closed
 410:    * @exception IOException if a i/o error occured.
 411:    * @exception ZipException if the Zip archive is malformed.  
 412:    */
 413:   public InputStream getInputStream(ZipEntry entry) throws IOException
 414:   {
 415:     checkClosed();
 416: 
 417:     LinkedHashMap entries = getEntries();
 418:     String name = entry.getName();
 419:     ZipEntry zipEntry = (ZipEntry) entries.get(name);
 420:     if (zipEntry == null)
 421:       return null;
 422: 
 423:     PartialInputStream inp = new PartialInputStream(raf, 1024);
 424:     inp.seek(zipEntry.offset);
 425: 
 426:     if (inp.readLeInt() != LOCSIG)
 427:       throw new ZipException("Wrong Local header signature: " + name);
 428: 
 429:     inp.skip(4);
 430: 
 431:     if (zipEntry.getMethod() != inp.readLeShort())
 432:       throw new ZipException("Compression method mismatch: " + name);
 433: 
 434:     inp.skip(16);
 435: 
 436:     int nameLen = inp.readLeShort();
 437:     int extraLen = inp.readLeShort();
 438:     inp.skip(nameLen + extraLen);
 439: 
 440:     inp.setLength(zipEntry.getCompressedSize());
 441: 
 442:     int method = zipEntry.getMethod();
 443:     switch (method)
 444:       {
 445:       case ZipOutputStream.STORED:
 446:     return inp;
 447:       case ZipOutputStream.DEFLATED:
 448:         inp.addDummyByte();
 449:         final Inflater inf = new Inflater(true);
 450:         final int sz = (int) entry.getSize();
 451:         return new InflaterInputStream(inp, inf)
 452:         {
 453:           public int available() throws IOException
 454:           {
 455:             if (sz == -1)
 456:               return super.available();
 457:             if (super.available() != 0)
 458:               return sz - inf.getTotalOut();
 459:             return 0;
 460:           }
 461:         };
 462:       default:
 463:     throw new ZipException("Unknown compression method " + method);
 464:       }
 465:   }
 466:   
 467:   /**
 468:    * Returns the (path) name of this zip file.
 469:    */
 470:   public String getName()
 471:   {
 472:     return name;
 473:   }
 474: 
 475:   /**
 476:    * Returns the number of entries in this zip file.
 477:    *
 478:    * @exception IllegalStateException when the ZipFile has already been closed
 479:    */
 480:   public int size()
 481:   {
 482:     checkClosed();
 483:     
 484:     try
 485:       {
 486:     return getEntries().size();
 487:       }
 488:     catch (IOException ioe)
 489:       {
 490:     return 0;
 491:       }
 492:   }
 493:   
 494:   private static class ZipEntryEnumeration implements Enumeration
 495:   {
 496:     private final Iterator elements;
 497: 
 498:     public ZipEntryEnumeration(Iterator elements)
 499:     {
 500:       this.elements = elements;
 501:     }
 502: 
 503:     public boolean hasMoreElements()
 504:     {
 505:       return elements.hasNext();
 506:     }
 507: 
 508:     public Object nextElement()
 509:     {
 510:       /* We return a clone, just to be safe that the user doesn't
 511:        * change the entry.  
 512:        */
 513:       return ((ZipEntry)elements.next()).clone();
 514:     }
 515:   }
 516: 
 517:   private static final class PartialInputStream extends InputStream
 518:   {
 519:     private final RandomAccessFile raf;
 520:     private final byte[] buffer;
 521:     private long bufferOffset;
 522:     private int pos;
 523:     private long end;
 524:     // We may need to supply an extra dummy byte to our reader.
 525:     // See Inflater.  We use a count here to simplify the logic
 526:     // elsewhere in this class.  Note that we ignore the dummy
 527:     // byte in methods where we know it is not needed.
 528:     private int dummyByteCount;
 529: 
 530:     public PartialInputStream(RandomAccessFile raf, int bufferSize)
 531:       throws IOException
 532:     {
 533:       this.raf = raf;
 534:       buffer = new byte[bufferSize];
 535:       bufferOffset = -buffer.length;
 536:       pos = buffer.length;
 537:       end = raf.length();
 538:     }
 539: 
 540:     void setLength(long length)
 541:     {
 542:       end = bufferOffset + pos + length;
 543:     }
 544: 
 545:     private void fillBuffer() throws IOException
 546:     {
 547:       synchronized (raf)
 548:         {
 549:           long len = end - bufferOffset;
 550:           if (len == 0 && dummyByteCount > 0)
 551:             {
 552:               buffer[0] = 0;
 553:               dummyByteCount = 0;
 554:             }
 555:           else
 556:             {
 557:               raf.seek(bufferOffset);
 558:               raf.readFully(buffer, 0, (int) Math.min(buffer.length, len));
 559:             }
 560:         }
 561:     }
 562:     
 563:     public int available()
 564:     {
 565:       long amount = end - (bufferOffset + pos);
 566:       if (amount > Integer.MAX_VALUE)
 567:     return Integer.MAX_VALUE;
 568:       return (int) amount;
 569:     }
 570:     
 571:     public int read() throws IOException
 572:     {
 573:       if (bufferOffset + pos >= end + dummyByteCount)
 574:     return -1;
 575:       if (pos == buffer.length)
 576:         {
 577:           bufferOffset += buffer.length;
 578:           pos = 0;
 579:           fillBuffer();
 580:         }
 581:       
 582:       return buffer[pos++] & 0xFF;
 583:     }
 584: 
 585:     public int read(byte[] b, int off, int len) throws IOException
 586:     {
 587:       if (len > end + dummyByteCount - (bufferOffset + pos))
 588:     {
 589:       len = (int) (end + dummyByteCount - (bufferOffset + pos));
 590:       if (len == 0)
 591:         return -1;
 592:     }
 593: 
 594:       int totalBytesRead = Math.min(buffer.length - pos, len);
 595:       System.arraycopy(buffer, pos, b, off, totalBytesRead);
 596:       pos += totalBytesRead;
 597:       off += totalBytesRead;
 598:       len -= totalBytesRead;
 599: 
 600:       while (len > 0)
 601:         {
 602:           bufferOffset += buffer.length;
 603:           pos = 0;
 604:           fillBuffer();
 605:           int remain = Math.min(buffer.length, len);
 606:           System.arraycopy(buffer, pos, b, off, remain);
 607:           pos += remain;
 608:           off += remain;
 609:           len -= remain;
 610:           totalBytesRead += remain;
 611:         }
 612:       
 613:       return totalBytesRead;
 614:     }
 615: 
 616:     public long skip(long amount) throws IOException
 617:     {
 618:       if (amount < 0)
 619:     return 0;
 620:       if (amount > end - (bufferOffset + pos))
 621:     amount = end - (bufferOffset + pos);
 622:       seek(bufferOffset + pos + amount);
 623:       return amount;
 624:     }
 625: 
 626:     void seek(long newpos) throws IOException
 627:     {
 628:       long offset = newpos - bufferOffset;
 629:       if (offset >= 0 && offset <= buffer.length)
 630:         {
 631:           pos = (int) offset;
 632:         }
 633:       else
 634:         {
 635:           bufferOffset = newpos;
 636:           pos = 0;
 637:           fillBuffer();
 638:         }
 639:     }
 640: 
 641:     void readFully(byte[] buf) throws IOException
 642:     {
 643:       if (read(buf, 0, buf.length) != buf.length)
 644:         throw new EOFException();
 645:     }
 646: 
 647:     void readFully(byte[] buf, int off, int len) throws IOException
 648:     {
 649:       if (read(buf, off, len) != len)
 650:         throw new EOFException();
 651:     }
 652: 
 653:     int readLeShort() throws IOException
 654:     {
 655:       int b0 = read();
 656:       int b1 = read();
 657:       if (b1 == -1)
 658:         throw new EOFException();
 659:       return (b0 & 0xff) | (b1 & 0xff) << 8;
 660:     }
 661: 
 662:     int readLeInt() throws IOException
 663:     {
 664:       int b0 = read();
 665:       int b1 = read();
 666:       int b2 = read();
 667:       int b3 = read();
 668:       if (b3 == -1)
 669:         throw new EOFException();
 670:       return ((b0 & 0xff) | (b1 & 0xff) << 8)
 671:             | ((b2 & 0xff) | (b3 & 0xff) << 8) << 16;
 672:     }
 673: 
 674:     String readString(int length) throws IOException
 675:     {
 676:       if (length > end - (bufferOffset + pos))
 677:         throw new EOFException();
 678: 
 679:       try
 680:         {
 681:           if (buffer.length - pos >= length)
 682:             {
 683:               String s = new String(buffer, pos, length, "UTF-8");
 684:               pos += length;
 685:               return s;
 686:             }
 687:           else
 688:             {
 689:               byte[] b = new byte[length];
 690:               readFully(b);
 691:               return new String(b, 0, length, "UTF-8");
 692:             }
 693:         }
 694:       catch (UnsupportedEncodingException uee)
 695:         {
 696:           throw new AssertionError(uee);
 697:         }
 698:     }
 699: 
 700:     public void addDummyByte()
 701:     {
 702:       dummyByteCount = 1;
 703:     }
 704:   }
 705: }