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}