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.HashMap;
  53: import java.util.Iterator;
  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 HashMap 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 HashMap(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 HashMap 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:     HashMap 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:     HashMap 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:         final Inflater inf = new Inflater(true);
 449:         final int sz = (int) entry.getSize();
 450:         return new InflaterInputStream(inp, inf)
 451:         {
 452:           public int available() throws IOException
 453:           {
 454:             if (sz == -1)
 455:               return super.available();
 456:             if (super.available() != 0)
 457:               return sz - inf.getTotalOut();
 458:             return 0;
 459:           }
 460:         };
 461:       default:
 462:     throw new ZipException("Unknown compression method " + method);
 463:       }
 464:   }
 465:   
 466:   /**
 467:    * Returns the (path) name of this zip file.
 468:    */
 469:   public String getName()
 470:   {
 471:     return name;
 472:   }
 473: 
 474:   /**
 475:    * Returns the number of entries in this zip file.
 476:    *
 477:    * @exception IllegalStateException when the ZipFile has already been closed
 478:    */
 479:   public int size()
 480:   {
 481:     checkClosed();
 482:     
 483:     try
 484:       {
 485:     return getEntries().size();
 486:       }
 487:     catch (IOException ioe)
 488:       {
 489:     return 0;
 490:       }
 491:   }
 492:   
 493:   private static class ZipEntryEnumeration implements Enumeration
 494:   {
 495:     private final Iterator elements;
 496: 
 497:     public ZipEntryEnumeration(Iterator elements)
 498:     {
 499:       this.elements = elements;
 500:     }
 501: 
 502:     public boolean hasMoreElements()
 503:     {
 504:       return elements.hasNext();
 505:     }
 506: 
 507:     public Object nextElement()
 508:     {
 509:       /* We return a clone, just to be safe that the user doesn't
 510:        * change the entry.  
 511:        */
 512:       return ((ZipEntry)elements.next()).clone();
 513:     }
 514:   }
 515: 
 516:   private static final class PartialInputStream extends InputStream
 517:   {
 518:     private final RandomAccessFile raf;
 519:     private final byte[] buffer;
 520:     private long bufferOffset;
 521:     private int pos;
 522:     private long end;
 523: 
 524:     public PartialInputStream(RandomAccessFile raf, int bufferSize)
 525:       throws IOException
 526:     {
 527:       this.raf = raf;
 528:       buffer = new byte[bufferSize];
 529:       bufferOffset = -buffer.length;
 530:       pos = buffer.length;
 531:       end = raf.length();
 532:     }
 533: 
 534:     void setLength(long length)
 535:     {
 536:       end = bufferOffset + pos + length;
 537:     }
 538: 
 539:     private void fillBuffer() throws IOException
 540:     {
 541:       synchronized (raf)
 542:         {
 543:           raf.seek(bufferOffset);
 544:           raf.readFully(buffer, 0, (int) Math.min(buffer.length, end - bufferOffset));
 545:         }
 546:     }
 547:     
 548:     public int available()
 549:     {
 550:       long amount = end - (bufferOffset + pos);
 551:       if (amount > Integer.MAX_VALUE)
 552:     return Integer.MAX_VALUE;
 553:       return (int) amount;
 554:     }
 555:     
 556:     public int read() throws IOException
 557:     {
 558:       if (bufferOffset + pos >= end)
 559:     return -1;
 560:       if (pos == buffer.length)
 561:         {
 562:           bufferOffset += buffer.length;
 563:           pos = 0;
 564:           fillBuffer();
 565:         }
 566:       
 567:       return buffer[pos++] & 0xFF;
 568:     }
 569: 
 570:     public int read(byte[] b, int off, int len) throws IOException
 571:     {
 572:       if (len > end - (bufferOffset + pos))
 573:     {
 574:       len = (int) (end - (bufferOffset + pos));
 575:       if (len == 0)
 576:         return -1;
 577:     }
 578: 
 579:       int totalBytesRead = Math.min(buffer.length - pos, len);
 580:       System.arraycopy(buffer, pos, b, off, totalBytesRead);
 581:       pos += totalBytesRead;
 582:       off += totalBytesRead;
 583:       len -= totalBytesRead;
 584: 
 585:       while (len > 0)
 586:         {
 587:           bufferOffset += buffer.length;
 588:           pos = 0;
 589:           fillBuffer();
 590:           int remain = Math.min(buffer.length, len);
 591:           System.arraycopy(buffer, pos, b, off, remain);
 592:           pos += remain;
 593:           off += remain;
 594:           len -= remain;
 595:           totalBytesRead += remain;
 596:         }
 597:       
 598:       return totalBytesRead;
 599:     }
 600: 
 601:     public long skip(long amount) throws IOException
 602:     {
 603:       if (amount < 0)
 604:     return 0;
 605:       if (amount > end - (bufferOffset + pos))
 606:     amount = end - (bufferOffset + pos);
 607:       seek(bufferOffset + pos + amount);
 608:       return amount;
 609:     }
 610: 
 611:     void seek(long newpos) throws IOException
 612:     {
 613:       long offset = newpos - bufferOffset;
 614:       if (offset >= 0 && offset <= buffer.length)
 615:         {
 616:           pos = (int) offset;
 617:         }
 618:       else
 619:         {
 620:           bufferOffset = newpos;
 621:           pos = 0;
 622:           fillBuffer();
 623:         }
 624:     }
 625: 
 626:     void readFully(byte[] buf) throws IOException
 627:     {
 628:       if (read(buf, 0, buf.length) != buf.length)
 629:         throw new EOFException();
 630:     }
 631: 
 632:     void readFully(byte[] buf, int off, int len) throws IOException
 633:     {
 634:       if (read(buf, off, len) != len)
 635:         throw new EOFException();
 636:     }
 637: 
 638:     int readLeShort() throws IOException
 639:     {
 640:       int b0 = read();
 641:       int b1 = read();
 642:       if (b1 == -1)
 643:         throw new EOFException();
 644:       return (b0 & 0xff) | (b1 & 0xff) << 8;
 645:     }
 646: 
 647:     int readLeInt() throws IOException
 648:     {
 649:       int b0 = read();
 650:       int b1 = read();
 651:       int b2 = read();
 652:       int b3 = read();
 653:       if (b3 == -1)
 654:         throw new EOFException();
 655:       return ((b0 & 0xff) | (b1 & 0xff) << 8)
 656:             | ((b2 & 0xff) | (b3 & 0xff) << 8) << 16;
 657:     }
 658: 
 659:     String readString(int length) throws IOException
 660:     {
 661:       if (length > end - (bufferOffset + pos))
 662:         throw new EOFException();
 663: 
 664:       try
 665:         {
 666:           if (buffer.length - pos >= length)
 667:             {
 668:               String s = new String(buffer, pos, length, "UTF-8");
 669:               pos += length;
 670:               return s;
 671:             }
 672:           else
 673:             {
 674:               byte[] b = new byte[length];
 675:               readFully(b);
 676:               return new String(b, 0, length, "UTF-8");
 677:             }
 678:         }
 679:       catch (UnsupportedEncodingException uee)
 680:         {
 681:           throw new AssertionError(uee);
 682:         }
 683:     }
 684:   }
 685: }