1 /* 2 * Copyright 1995-2008 Sun Microsystems, Inc. All Rights Reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Sun designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Sun in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 * CA 95054 USA or visit www.sun.com if you need additional information or 23 * have any questions. 24 */ 25 26 package java.util.zip; 27 28 import java.io.Closeable; 29 import java.io.InputStream; 30 import java.io.IOException; 31 import java.io.EOFException; 32 import java.io.File; 33 import java.nio.charset.Charset; 34 import java.util.Vector; 35 import java.util.Enumeration; 36 import java.util.Set; 37 import java.util.HashSet; 38 import java.util.NoSuchElementException; 39 import java.security.AccessController; 40 import sun.security.action.GetPropertyAction; 41 import static java.util.zip.ZipConstants64.*; 42 43 /** 44 * This class is used to read entries from a zip file. 45 * 46 * <p> Unless otherwise noted, passing a <tt>null</tt> argument to a constructor 47 * or method in this class will cause a {@link NullPointerException} to be 48 * thrown. 49 * 50 * @author David Connelly 51 */ 52 public 53 class ZipFile implements ZipConstants, Closeable { 54 private long jzfile; // address of jzfile data 55 private String name; // zip file name 56 private int total; // total number of entries 57 private boolean closeRequested; 58 59 private static final int STORED = ZipEntry.STORED; 60 private static final int DEFLATED = ZipEntry.DEFLATED; 61 62 /** 63 * Mode flag to open a zip file for reading. 64 */ 65 public static final int OPEN_READ = 0x1; 66 67 /** 68 * Mode flag to open a zip file and mark it for deletion. The file will be 69 * deleted some time between the moment that it is opened and the moment 70 * that it is closed, but its contents will remain accessible via the 71 * <tt>ZipFile</tt> object until either the close method is invoked or the 72 * virtual machine exits. 73 */ 74 public static final int OPEN_DELETE = 0x4; 75 76 static { 77 /* Zip library is loaded from System.initializeSystemClass */ 78 initIDs(); 79 } 80 81 private static native void initIDs(); 82 83 private static final boolean usemmap; 84 85 static { 86 // A system prpperty to disable mmap use to avoid vm crash when 87 // in-use zip file is accidently overwritten by others. 88 String prop = AccessController.doPrivileged( 89 new GetPropertyAction("sun.zip.disableMemoryMapping")); 90 usemmap = (prop == null || 91 !(prop.length() == 0 || prop.equalsIgnoreCase("true"))); 92 } 93 94 /** 95 * Opens a zip file for reading. 96 * 97 * <p>First, if there is a security manager, its <code>checkRead</code> 98 * method is called with the <code>name</code> argument as its argument 99 * to ensure the read is allowed. 100 * 101 * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to 102 * decode the entry names and comments. 103 * 104 * @param name the name of the zip file 105 * @throws ZipException if a ZIP format error has occurred 106 * @throws IOException if an I/O error has occurred 107 * @throws SecurityException if a security manager exists and its 108 * <code>checkRead</code> method doesn't allow read access to the file. 109 * 110 * @see SecurityManager#checkRead(java.lang.String) 111 */ 112 public ZipFile(String name) throws IOException { 113 this(new File(name), OPEN_READ); 114 } 115 116 /** 117 * Opens a new <code>ZipFile</code> to read from the specified 118 * <code>File</code> object in the specified mode. The mode argument 119 * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>. 120 * 121 * <p>First, if there is a security manager, its <code>checkRead</code> 122 * method is called with the <code>name</code> argument as its argument to 123 * ensure the read is allowed. 124 * 125 * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to 126 * decode the entry names and comments 127 * 128 * @param file the ZIP file to be opened for reading 129 * @param mode the mode in which the file is to be opened 130 * @throws ZipException if a ZIP format error has occurred 131 * @throws IOException if an I/O error has occurred 132 * @throws SecurityException if a security manager exists and 133 * its <code>checkRead</code> method 134 * doesn't allow read access to the file, 135 * or its <code>checkDelete</code> method doesn't allow deleting 136 * the file when the <tt>OPEN_DELETE</tt> flag is set. 137 * @throws IllegalArgumentException if the <tt>mode</tt> argument is invalid 138 * @see SecurityManager#checkRead(java.lang.String) 139 * @since 1.3 140 */ 141 public ZipFile(File file, int mode) throws IOException { 142 this(file, mode, Charset.forName("UTF-8")); 143 } 144 145 /** 146 * Opens a ZIP file for reading given the specified File object. 147 * 148 * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to 149 * decode the entry names and comments. 150 * 151 * @param file the ZIP file to be opened for reading 152 * @throws ZipException if a ZIP format error has occurred 153 * @throws IOException if an I/O error has occurred 154 */ 155 public ZipFile(File file) throws ZipException, IOException { 156 this(file, OPEN_READ); 157 } 158 159 private ZipCoder zc; 160 161 /** 162 * Opens a new <code>ZipFile</code> to read from the specified 163 * <code>File</code> object in the specified mode. The mode argument 164 * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>. 165 * 166 * <p>First, if there is a security manager, its <code>checkRead</code> 167 * method is called with the <code>name</code> argument as its argument to 168 * ensure the read is allowed. 169 * 170 * @param file the ZIP file to be opened for reading 171 * @param mode the mode in which the file is to be opened 172 * @param charset 173 * the {@linkplain java.nio.charset.Charset charset} to 174 * be used to decode the ZIP entry name and comment that are not 175 * encoded by using UTF-8 encoding (indicated by entry's general 176 * purpose flag). 177 * 178 * @throws ZipException if a ZIP format error has occurred 179 * @throws IOException if an I/O error has occurred 180 * 181 * @throws SecurityException 182 * if a security manager exists and its <code>checkRead</code> 183 * method doesn't allow read access to the file,or its 184 * <code>checkDelete</code> method doesn't allow deleting the 185 * file when the <tt>OPEN_DELETE</tt> flag is set 186 * 187 * @throws IllegalArgumentException if the <tt>mode</tt> argument is invalid 188 * 189 * @see SecurityManager#checkRead(java.lang.String) 190 * 191 * @since 1.7 192 */ 193 public ZipFile(File file, int mode, Charset charset) throws IOException 194 { 195 if (((mode & OPEN_READ) == 0) || 196 ((mode & ~(OPEN_READ | OPEN_DELETE)) != 0)) { 197 throw new IllegalArgumentException("Illegal mode: 0x"+ 198 Integer.toHexString(mode)); 199 } 200 String name = file.getPath(); 201 SecurityManager sm = System.getSecurityManager(); 202 if (sm != null) { 203 sm.checkRead(name); 204 if ((mode & OPEN_DELETE) != 0) { 205 sm.checkDelete(name); 206 } 207 } 208 if (charset == null) 209 throw new NullPointerException("charset is null"); 210 this.zc = ZipCoder.get(charset); 211 long t0 = System.nanoTime(); 212 jzfile = open(name, mode, file.lastModified(), usemmap); 213 sun.misc.PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0); 214 sun.misc.PerfCounter.getZipFileCount().increment(); 215 this.name = name; 216 this.total = getTotal(jzfile); 217 } 218 219 /** 220 * Opens a zip file for reading. 221 * 222 * <p>First, if there is a security manager, its <code>checkRead</code> 223 * method is called with the <code>name</code> argument as its argument 224 * to ensure the read is allowed. 225 * 226 * @param name the name of the zip file 227 * @param charset 228 * the {@linkplain java.nio.charset.Charset charset} to 229 * be used to decode the ZIP entry name and comment that are not 230 * encoded by using UTF-8 encoding (indicated by entry's general 231 * purpose flag). 232 * 233 * @throws ZipException if a ZIP format error has occurred 234 * @throws IOException if an I/O error has occurred 235 * @throws SecurityException 236 * if a security manager exists and its <code>checkRead</code> 237 * method doesn't allow read access to the file 238 * 239 * @see SecurityManager#checkRead(java.lang.String) 240 * 241 * @since 1.7 242 */ 243 public ZipFile(String name, Charset charset) throws IOException 244 { 245 this(new File(name), OPEN_READ, charset); 246 } 247 248 /** 249 * Opens a ZIP file for reading given the specified File object. 250 * @param file the ZIP file to be opened for reading 251 * @param charset 252 * The {@linkplain java.nio.charset.Charset charset} to be 253 * used to decode the ZIP entry name and comment (ignored if 254 * the <a href="package-summary.html#lang_encoding"> language 255 * encoding bit</a> of the ZIP entry's general purpose bit 256 * flag is set). 257 * 258 * @throws ZipException if a ZIP format error has occurred 259 * @throws IOException if an I/O error has occurred 260 * 261 * @since 1.7 262 */ 263 public ZipFile(File file, Charset charset) throws IOException 264 { 265 this(file, OPEN_READ, charset); 266 } 267 268 /** 269 * Returns the zip file comment, or null if none. 270 * 271 * @return the comment string for the zip file, or null if none 272 * 273 * @throws IllegalStateException if the zip file has been closed 274 * 275 * Since 1.7 276 */ 277 public String getComment() { 278 synchronized (this) { 279 ensureOpen(); 280 byte[] bcomm = getCommentBytes(jzfile); 281 if (bcomm == null) 282 return null; 283 return zc.toString(bcomm, bcomm.length); 284 } 285 } 286 287 /** 288 * Returns the zip file entry for the specified name, or null 289 * if not found. 290 * 291 * @param name the name of the entry 292 * @return the zip file entry, or null if not found 293 * @throws IllegalStateException if the zip file has been closed 294 */ 295 public ZipEntry getEntry(String name) { 296 if (name == null) { 297 throw new NullPointerException("name"); 298 } 299 long jzentry = 0; 300 synchronized (this) { 301 ensureOpen(); 302 jzentry = getEntry(jzfile, zc.getBytes(name), true); 303 if (jzentry != 0) { 304 ZipEntry ze = getZipEntry(name, jzentry); 305 freeEntry(jzfile, jzentry); 306 return ze; 307 } 308 } 309 return null; 310 } 311 312 private static native long getEntry(long jzfile, byte[] name, 313 boolean addSlash); 314 315 // freeEntry releases the C jzentry struct. 316 private static native void freeEntry(long jzfile, long jzentry); 317 318 // the outstanding inputstreams that need to be closed. 319 private Set<ZipFileInputStream> streams = new HashSet<ZipFileInputStream>(); 320 321 /** 322 * Returns an input stream for reading the contents of the specified 323 * zip file entry. 324 * 325 * <p> Closing this ZIP file will, in turn, close all input 326 * streams that have been returned by invocations of this method. 327 * 328 * @param entry the zip file entry 329 * @return the input stream for reading the contents of the specified 330 * zip file entry. 331 * @throws ZipException if a ZIP format error has occurred 332 * @throws IOException if an I/O error has occurred 333 * @throws IllegalStateException if the zip file has been closed 334 */ 335 public InputStream getInputStream(ZipEntry entry) throws IOException { 336 if (entry == null) { 337 throw new NullPointerException("entry"); 338 } 339 long jzentry = 0; 340 ZipFileInputStream in = null; 341 synchronized (this) { 342 ensureOpen(); 343 if (!zc.isUTF8() && (entry.flag & EFS) != 0) { 344 jzentry = getEntry(jzfile, zc.getBytesUTF8(entry.name), false); 345 } else { 346 jzentry = getEntry(jzfile, zc.getBytes(entry.name), false); 347 } 348 if (jzentry == 0) { 349 return null; 350 } 351 in = new ZipFileInputStream(jzentry); 352 streams.add(in); 353 } 354 final ZipFileInputStream zfin = in; 355 switch (getEntryMethod(jzentry)) { 356 case STORED: 357 return zfin; 358 case DEFLATED: 359 // MORE: Compute good size for inflater stream: 360 long size = getEntrySize(jzentry) + 2; // Inflater likes a bit of slack 361 if (size > 65536) size = 8192; 362 if (size <= 0) size = 4096; 363 return new InflaterInputStream(zfin, getInflater(), (int)size) { 364 private boolean isClosed = false; 365 366 public void close() throws IOException { 367 if (!isClosed) { 368 releaseInflater(inf); 369 this.in.close(); 370 isClosed = true; 371 } 372 } 373 // Override fill() method to provide an extra "dummy" byte 374 // at the end of the input stream. This is required when 375 // using the "nowrap" Inflater option. 376 protected void fill() throws IOException { 377 if (eof) { 378 throw new EOFException( 379 "Unexpected end of ZLIB input stream"); 380 } 381 len = this.in.read(buf, 0, buf.length); 382 if (len == -1) { 383 buf[0] = 0; 384 len = 1; 385 eof = true; 386 } 387 inf.setInput(buf, 0, len); 388 } 389 private boolean eof; 390 391 public int available() throws IOException { 392 if (isClosed) 393 return 0; 394 long avail = zfin.size() - inf.getBytesWritten(); 395 return avail > (long) Integer.MAX_VALUE ? 396 Integer.MAX_VALUE : (int) avail; 397 } 398 }; 399 default: 400 throw new ZipException("invalid compression method"); 401 } 402 } 403 404 /* 405 * Gets an inflater from the list of available inflaters or allocates 406 * a new one. 407 */ 408 private Inflater getInflater() { 409 synchronized (inflaters) { 410 int size = inflaters.size(); 411 if (size > 0) { 412 Inflater inf = (Inflater)inflaters.remove(size - 1); 413 return inf; 414 } else { 415 return new Inflater(true); 416 } 417 } 418 } 419 420 /* 421 * Releases the specified inflater to the list of available inflaters. 422 */ 423 private void releaseInflater(Inflater inf) { 424 synchronized (inflaters) { 425 inf.reset(); 426 inflaters.add(inf); 427 } 428 } 429 430 // List of available Inflater objects for decompression 431 private Vector inflaters = new Vector(); 432 433 /** 434 * Returns the path name of the ZIP file. 435 * @return the path name of the ZIP file 436 */ 437 public String getName() { 438 return name; 439 } 440 441 /** 442 * Returns an enumeration of the ZIP file entries. 443 * @return an enumeration of the ZIP file entries 444 * @throws IllegalStateException if the zip file has been closed 445 */ 446 public Enumeration<? extends ZipEntry> entries() { 447 ensureOpen(); 448 return new Enumeration<ZipEntry>() { 449 private int i = 0; 450 public boolean hasMoreElements() { 451 synchronized (ZipFile.this) { 452 ensureOpen(); 453 return i < total; 454 } 455 } 456 public ZipEntry nextElement() throws NoSuchElementException { 457 synchronized (ZipFile.this) { 458 ensureOpen(); 459 if (i >= total) { 460 throw new NoSuchElementException(); 461 } 462 long jzentry = getNextEntry(jzfile, i++); 463 if (jzentry == 0) { 464 String message; 465 if (closeRequested) { 466 message = "ZipFile concurrently closed"; 467 } else { 468 message = getZipMessage(ZipFile.this.jzfile); 469 } 470 throw new ZipError("jzentry == 0" + 471 ",\n jzfile = " + ZipFile.this.jzfile + 472 ",\n total = " + ZipFile.this.total + 473 ",\n name = " + ZipFile.this.name + 474 ",\n i = " + i + 475 ",\n message = " + message 476 ); 477 } 478 ZipEntry ze = getZipEntry(null, jzentry); 479 freeEntry(jzfile, jzentry); 480 return ze; 481 } 482 } 483 }; 484 } 485 486 private ZipEntry getZipEntry(String name, long jzentry) { 487 ZipEntry e = new ZipEntry(); 488 e.flag = getEntryFlag(jzentry); // get the flag first 489 if (name != null) { 490 e.name = name; 491 } else { 492 byte[] bname = getEntryBytes(jzentry, JZENTRY_NAME); 493 if (!zc.isUTF8() && (e.flag & EFS) != 0) { 494 e.name = zc.toStringUTF8(bname, bname.length); 495 } else { 496 e.name = zc.toString(bname, bname.length); 497 } 498 } 499 e.time = getEntryTime(jzentry); 500 e.crc = getEntryCrc(jzentry); 501 e.size = getEntrySize(jzentry); 502 e. csize = getEntryCSize(jzentry); 503 e.method = getEntryMethod(jzentry); 504 e.extra = getEntryBytes(jzentry, JZENTRY_EXTRA); 505 byte[] bcomm = getEntryBytes(jzentry, JZENTRY_COMMENT); 506 if (bcomm == null) { 507 e.comment = null; 508 } else { 509 if (!zc.isUTF8() && (e.flag & EFS) != 0) { 510 e.comment = zc.toStringUTF8(bcomm, bcomm.length); 511 } else { 512 e.comment = zc.toString(bcomm, bcomm.length); 513 } 514 } 515 return e; 516 } 517 518 private static native long getNextEntry(long jzfile, int i); 519 520 /** 521 * Returns the number of entries in the ZIP file. 522 * @return the number of entries in the ZIP file 523 * @throws IllegalStateException if the zip file has been closed 524 */ 525 public int size() { 526 ensureOpen(); 527 return total; 528 } 529 530 /** 531 * Closes the ZIP file. 532 * <p> Closing this ZIP file will close all of the input streams 533 * previously returned by invocations of the {@link #getInputStream 534 * getInputStream} method. 535 * 536 * @throws IOException if an I/O error has occurred 537 */ 538 public void close() throws IOException { 539 synchronized (this) { 540 closeRequested = true; 541 542 if (streams.size() !=0) { 543 Set<ZipFileInputStream> copy = streams; 544 streams = new HashSet<ZipFileInputStream>(); 545 for (ZipFileInputStream is: copy) 546 is.close(); 547 } 548 549 if (jzfile != 0) { 550 // Close the zip file 551 long zf = this.jzfile; 552 jzfile = 0; 553 554 close(zf); 555 556 // Release inflaters 557 synchronized (inflaters) { 558 int size = inflaters.size(); 559 for (int i = 0; i < size; i++) { 560 Inflater inf = (Inflater)inflaters.get(i); 561 inf.end(); 562 } 563 } 564 } 565 } 566 } 567 568 569 /** 570 * Ensures that the <code>close</code> method of this ZIP file is 571 * called when there are no more references to it. 572 * 573 * <p> 574 * Since the time when GC would invoke this method is undetermined, 575 * it is strongly recommended that applications invoke the <code>close</code> 576 * method as soon they have finished accessing this <code>ZipFile</code>. 577 * This will prevent holding up system resources for an undetermined 578 * length of time. 579 * 580 * @throws IOException if an I/O error has occurred 581 * @see java.util.zip.ZipFile#close() 582 */ 583 protected void finalize() throws IOException { 584 close(); 585 } 586 587 private static native void close(long jzfile); 588 589 private void ensureOpen() { 590 if (closeRequested) { 591 throw new IllegalStateException("zip file closed"); 592 } 593 594 if (jzfile == 0) { 595 throw new IllegalStateException("The object is not initialized."); 596 } 597 } 598 599 private void ensureOpenOrZipException() throws IOException { 600 if (closeRequested) { 601 throw new ZipException("ZipFile closed"); 602 } 603 } 604 605 /* 606 * Inner class implementing the input stream used to read a 607 * (possibly compressed) zip file entry. 608 */ 609 private class ZipFileInputStream extends InputStream { 610 protected long jzentry; // address of jzentry data 611 private long pos; // current position within entry data 612 protected long rem; // number of remaining bytes within entry 613 protected long size; // uncompressed size of this entry 614 615 ZipFileInputStream(long jzentry) { 616 pos = 0; 617 rem = getEntryCSize(jzentry); 618 size = getEntrySize(jzentry); 619 this.jzentry = jzentry; 620 } 621 622 public int read(byte b[], int off, int len) throws IOException { 623 if (rem == 0) { 624 return -1; 625 } 626 if (len <= 0) { 627 return 0; 628 } 629 if (len > rem) { 630 len = (int) rem; 631 } 632 synchronized (ZipFile.this) { 633 ensureOpenOrZipException(); 634 635 len = ZipFile.read(ZipFile.this.jzfile, jzentry, pos, b, 636 off, len); 637 } 638 if (len > 0) { 639 pos += len; 640 rem -= len; 641 } 642 if (rem == 0) { 643 close(); 644 } 645 return len; 646 } 647 648 public int read() throws IOException { 649 byte[] b = new byte[1]; 650 if (read(b, 0, 1) == 1) { 651 return b[0] & 0xff; 652 } else { 653 return -1; 654 } 655 } 656 657 public long skip(long n) { 658 if (n > rem) 659 n = rem; 660 pos += n; 661 rem -= n; 662 if (rem == 0) { 663 close(); 664 } 665 return n; 666 } 667 668 public int available() { 669 return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem; 670 } 671 672 public long size() { 673 return size; 674 } 675 676 public void close() { 677 rem = 0; 678 synchronized (ZipFile.this) { 679 if (jzentry != 0 && ZipFile.this.jzfile != 0) { 680 freeEntry(ZipFile.this.jzfile, jzentry); 681 jzentry = 0; 682 } 683 streams.remove(this); 684 } 685 } 686 } 687 688 689 private static native long open(String name, int mode, long lastModified, 690 boolean usemmap) throws IOException; 691 private static native int getTotal(long jzfile); 692 private static native int read(long jzfile, long jzentry, 693 long pos, byte[] b, int off, int len); 694 695 // access to the native zentry object 696 private static native long getEntryTime(long jzentry); 697 private static native long getEntryCrc(long jzentry); 698 private static native long getEntryCSize(long jzentry); 699 private static native long getEntrySize(long jzentry); 700 private static native int getEntryMethod(long jzentry); 701 private static native int getEntryFlag(long jzentry); 702 private static native byte[] getCommentBytes(long jzfile); 703 704 private static final int JZENTRY_NAME = 0; 705 private static final int JZENTRY_EXTRA = 1; 706 private static final int JZENTRY_COMMENT = 2; 707 private static native byte[] getEntryBytes(long jzentry, int type); 708 709 private static native String getZipMessage(long jzfile); 710 }