ZipFile.java revision a8ed084745590c5e4a0e8559b5821809d60fe242
1/* 2 * Copyright (c) 1995, 2011, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package java.util.zip; 27 28import java.io.Closeable; 29import java.io.InputStream; 30import java.io.IOException; 31import java.io.EOFException; 32import java.io.File; 33import java.nio.charset.Charset; 34import java.nio.charset.StandardCharsets; 35import java.util.ArrayDeque; 36import java.util.Deque; 37import java.util.Enumeration; 38import java.util.HashMap; 39import java.util.Map; 40import java.util.NoSuchElementException; 41import java.util.WeakHashMap; 42import java.security.AccessController; 43import sun.security.action.GetPropertyAction; 44import static java.util.zip.ZipConstants64.*; 45 46/** 47 * This class is used to read entries from a zip file. 48 * 49 * <p> Unless otherwise noted, passing a <tt>null</tt> argument to a constructor 50 * or method in this class will cause a {@link NullPointerException} to be 51 * thrown. 52 * 53 * @author David Connelly 54 */ 55public 56class ZipFile implements ZipConstants, Closeable { 57 private long jzfile; // address of jzfile data 58 private final String name; // zip file name 59 private final int total; // total number of entries 60 private final boolean locsig; // if zip file starts with LOCSIG (usually true) 61 private volatile boolean closeRequested = false; 62 63 private static final int STORED = ZipEntry.STORED; 64 private static final int DEFLATED = ZipEntry.DEFLATED; 65 66 /** 67 * Mode flag to open a zip file for reading. 68 */ 69 public static final int OPEN_READ = 0x1; 70 71 /** 72 * Mode flag to open a zip file and mark it for deletion. The file will be 73 * deleted some time between the moment that it is opened and the moment 74 * that it is closed, but its contents will remain accessible via the 75 * <tt>ZipFile</tt> object until either the close method is invoked or the 76 * virtual machine exits. 77 */ 78 public static final int OPEN_DELETE = 0x4; 79 80 static { 81 /* Zip library is loaded from System.initializeSystemClass */ 82 initIDs(); 83 } 84 85 private static native void initIDs(); 86 87 private static final boolean usemmap; 88 89 static { 90 // Android-changed: always use mmap. 91 usemmap = 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, StandardCharsets.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 this.name = name; 214 this.total = getTotal(jzfile); 215 this.locsig = startsWithLOC(jzfile); 216 } 217 218 /** 219 * Opens a zip file for reading. 220 * 221 * <p>First, if there is a security manager, its <code>checkRead</code> 222 * method is called with the <code>name</code> argument as its argument 223 * to ensure the read is allowed. 224 * 225 * @param name the name of the zip file 226 * @param charset 227 * the {@linkplain java.nio.charset.Charset charset} to 228 * be used to decode the ZIP entry name and comment that are not 229 * encoded by using UTF-8 encoding (indicated by entry's general 230 * purpose flag). 231 * 232 * @throws ZipException if a ZIP format error has occurred 233 * @throws IOException if an I/O error has occurred 234 * @throws SecurityException 235 * if a security manager exists and its <code>checkRead</code> 236 * method doesn't allow read access to the file 237 * 238 * @see SecurityManager#checkRead(java.lang.String) 239 * 240 * @since 1.7 241 */ 242 public ZipFile(String name, Charset charset) throws IOException 243 { 244 this(new File(name), OPEN_READ, charset); 245 } 246 247 /** 248 * Opens a ZIP file for reading given the specified File object. 249 * @param file the ZIP file to be opened for reading 250 * @param charset 251 * The {@linkplain java.nio.charset.Charset charset} to be 252 * used to decode the ZIP entry name and comment (ignored if 253 * the <a href="package-summary.html#lang_encoding"> language 254 * encoding bit</a> of the ZIP entry's general purpose bit 255 * flag is set). 256 * 257 * @throws ZipException if a ZIP format error has occurred 258 * @throws IOException if an I/O error has occurred 259 * 260 * @since 1.7 261 */ 262 public ZipFile(File file, Charset charset) throws IOException 263 { 264 this(file, OPEN_READ, charset); 265 } 266 267 /** 268 * Returns the zip file comment, or null if none. 269 * 270 * @return the comment string for the zip file, or null if none 271 * 272 * @throws IllegalStateException if the zip file has been closed 273 * 274 * Since 1.7 275 */ 276 public String getComment() { 277 synchronized (this) { 278 ensureOpen(); 279 byte[] bcomm = getCommentBytes(jzfile); 280 if (bcomm == null) 281 return null; 282 return zc.toString(bcomm, bcomm.length); 283 } 284 } 285 286 /** 287 * Returns the zip file entry for the specified name, or null 288 * if not found. 289 * 290 * @param name the name of the entry 291 * @return the zip file entry, or null if not found 292 * @throws IllegalStateException if the zip file has been closed 293 */ 294 public ZipEntry getEntry(String name) { 295 if (name == null) { 296 throw new NullPointerException("name"); 297 } 298 long jzentry = 0; 299 synchronized (this) { 300 ensureOpen(); 301 jzentry = getEntry(jzfile, zc.getBytes(name), true); 302 if (jzentry != 0) { 303 ZipEntry ze = getZipEntry(name, jzentry); 304 freeEntry(jzfile, jzentry); 305 return ze; 306 } 307 } 308 return null; 309 } 310 311 private static native long getEntry(long jzfile, byte[] name, 312 boolean addSlash); 313 314 // freeEntry releases the C jzentry struct. 315 private static native void freeEntry(long jzfile, long jzentry); 316 317 // the outstanding inputstreams that need to be closed, 318 // mapped to the inflater objects they use. 319 private final Map<InputStream, Inflater> streams = new WeakHashMap<>(); 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 353 switch (getEntryMethod(jzentry)) { 354 case STORED: 355 synchronized (streams) { 356 streams.put(in, null); 357 } 358 return in; 359 case DEFLATED: 360 // MORE: Compute good size for inflater stream: 361 long size = getEntrySize(jzentry) + 2; // Inflater likes a bit of slack 362 if (size > 65536) size = 8192; 363 if (size <= 0) size = 4096; 364 Inflater inf = getInflater(); 365 InputStream is = 366 new ZipFileInflaterInputStream(in, inf, (int)size); 367 synchronized (streams) { 368 streams.put(is, inf); 369 } 370 return is; 371 default: 372 throw new ZipException("invalid compression method"); 373 } 374 } 375 } 376 377 private class ZipFileInflaterInputStream extends InflaterInputStream { 378 private volatile boolean closeRequested = false; 379 private boolean eof = false; 380 private final ZipFileInputStream zfin; 381 382 ZipFileInflaterInputStream(ZipFileInputStream zfin, Inflater inf, 383 int size) { 384 super(zfin, inf, size); 385 this.zfin = zfin; 386 } 387 388 public void close() throws IOException { 389 if (closeRequested) 390 return; 391 closeRequested = true; 392 393 super.close(); 394 Inflater inf; 395 synchronized (streams) { 396 inf = streams.remove(this); 397 } 398 if (inf != null) { 399 releaseInflater(inf); 400 } 401 } 402 403 // Override fill() method to provide an extra "dummy" byte 404 // at the end of the input stream. This is required when 405 // using the "nowrap" Inflater option. 406 protected void fill() throws IOException { 407 if (eof) { 408 throw new EOFException("Unexpected end of ZLIB input stream"); 409 } 410 len = in.read(buf, 0, buf.length); 411 if (len == -1) { 412 buf[0] = 0; 413 len = 1; 414 eof = true; 415 } 416 inf.setInput(buf, 0, len); 417 } 418 419 public int available() throws IOException { 420 if (closeRequested) 421 return 0; 422 long avail = zfin.size() - inf.getBytesWritten(); 423 return (avail > (long) Integer.MAX_VALUE ? 424 Integer.MAX_VALUE : (int) avail); 425 } 426 427 protected void finalize() throws Throwable { 428 close(); 429 } 430 } 431 432 /* 433 * Gets an inflater from the list of available inflaters or allocates 434 * a new one. 435 */ 436 private Inflater getInflater() { 437 Inflater inf; 438 synchronized (inflaterCache) { 439 while (null != (inf = inflaterCache.poll())) { 440 if (false == inf.ended()) { 441 return inf; 442 } 443 } 444 } 445 return new Inflater(true); 446 } 447 448 /* 449 * Releases the specified inflater to the list of available inflaters. 450 */ 451 private void releaseInflater(Inflater inf) { 452 if (false == inf.ended()) { 453 inf.reset(); 454 synchronized (inflaterCache) { 455 inflaterCache.add(inf); 456 } 457 } 458 } 459 460 // List of available Inflater objects for decompression 461 private Deque<Inflater> inflaterCache = new ArrayDeque<>(); 462 463 /** 464 * Returns the path name of the ZIP file. 465 * @return the path name of the ZIP file 466 */ 467 public String getName() { 468 return name; 469 } 470 471 /** 472 * Returns an enumeration of the ZIP file entries. 473 * @return an enumeration of the ZIP file entries 474 * @throws IllegalStateException if the zip file has been closed 475 */ 476 public Enumeration<? extends ZipEntry> entries() { 477 ensureOpen(); 478 return new Enumeration<ZipEntry>() { 479 private int i = 0; 480 public boolean hasMoreElements() { 481 synchronized (ZipFile.this) { 482 ensureOpen(); 483 return i < total; 484 } 485 } 486 public ZipEntry nextElement() throws NoSuchElementException { 487 synchronized (ZipFile.this) { 488 ensureOpen(); 489 if (i >= total) { 490 throw new NoSuchElementException(); 491 } 492 long jzentry = getNextEntry(jzfile, i++); 493 if (jzentry == 0) { 494 String message; 495 if (closeRequested) { 496 message = "ZipFile concurrently closed"; 497 } else { 498 message = getZipMessage(ZipFile.this.jzfile); 499 } 500 throw new ZipError("jzentry == 0" + 501 ",\n jzfile = " + ZipFile.this.jzfile + 502 ",\n total = " + ZipFile.this.total + 503 ",\n name = " + ZipFile.this.name + 504 ",\n i = " + i + 505 ",\n message = " + message 506 ); 507 } 508 ZipEntry ze = getZipEntry(null, jzentry); 509 freeEntry(jzfile, jzentry); 510 return ze; 511 } 512 } 513 }; 514 } 515 516 private ZipEntry getZipEntry(String name, long jzentry) { 517 ZipEntry e = new ZipEntry(); 518 e.flag = getEntryFlag(jzentry); // get the flag first 519 if (name != null) { 520 e.name = name; 521 } else { 522 byte[] bname = getEntryBytes(jzentry, JZENTRY_NAME); 523 if (!zc.isUTF8() && (e.flag & EFS) != 0) { 524 e.name = zc.toStringUTF8(bname, bname.length); 525 } else { 526 e.name = zc.toString(bname, bname.length); 527 } 528 } 529 e.time = getEntryTime(jzentry); 530 e.crc = getEntryCrc(jzentry); 531 e.size = getEntrySize(jzentry); 532 e. csize = getEntryCSize(jzentry); 533 e.method = getEntryMethod(jzentry); 534 e.extra = getEntryBytes(jzentry, JZENTRY_EXTRA); 535 byte[] bcomm = getEntryBytes(jzentry, JZENTRY_COMMENT); 536 if (bcomm == null) { 537 e.comment = null; 538 } else { 539 if (!zc.isUTF8() && (e.flag & EFS) != 0) { 540 e.comment = zc.toStringUTF8(bcomm, bcomm.length); 541 } else { 542 e.comment = zc.toString(bcomm, bcomm.length); 543 } 544 } 545 return e; 546 } 547 548 private static native long getNextEntry(long jzfile, int i); 549 550 /** 551 * Returns the number of entries in the ZIP file. 552 * @return the number of entries in the ZIP file 553 * @throws IllegalStateException if the zip file has been closed 554 */ 555 public int size() { 556 ensureOpen(); 557 return total; 558 } 559 560 /** 561 * Closes the ZIP file. 562 * <p> Closing this ZIP file will close all of the input streams 563 * previously returned by invocations of the {@link #getInputStream 564 * getInputStream} method. 565 * 566 * @throws IOException if an I/O error has occurred 567 */ 568 public void close() throws IOException { 569 if (closeRequested) 570 return; 571 closeRequested = true; 572 573 synchronized (this) { 574 // Close streams, release their inflaters 575 synchronized (streams) { 576 if (false == streams.isEmpty()) { 577 Map<InputStream, Inflater> copy = new HashMap<>(streams); 578 streams.clear(); 579 for (Map.Entry<InputStream, Inflater> e : copy.entrySet()) { 580 e.getKey().close(); 581 Inflater inf = e.getValue(); 582 if (inf != null) { 583 inf.end(); 584 } 585 } 586 } 587 } 588 589 // Release cached inflaters 590 Inflater inf; 591 synchronized (inflaterCache) { 592 while (null != (inf = inflaterCache.poll())) { 593 inf.end(); 594 } 595 } 596 597 if (jzfile != 0) { 598 // Close the zip file 599 long zf = this.jzfile; 600 jzfile = 0; 601 602 close(zf); 603 } 604 } 605 } 606 607 /** 608 * Ensures that the system resources held by this ZipFile object are 609 * released when there are no more references to it. 610 * 611 * <p> 612 * Since the time when GC would invoke this method is undetermined, 613 * it is strongly recommended that applications invoke the <code>close</code> 614 * method as soon they have finished accessing this <code>ZipFile</code>. 615 * This will prevent holding up system resources for an undetermined 616 * length of time. 617 * 618 * @throws IOException if an I/O error has occurred 619 * @see java.util.zip.ZipFile#close() 620 */ 621 protected void finalize() throws IOException { 622 close(); 623 } 624 625 private static native void close(long jzfile); 626 627 private void ensureOpen() { 628 if (closeRequested) { 629 throw new IllegalStateException("zip file closed"); 630 } 631 632 if (jzfile == 0) { 633 throw new IllegalStateException("The object is not initialized."); 634 } 635 } 636 637 private void ensureOpenOrZipException() throws IOException { 638 if (closeRequested) { 639 throw new ZipException("ZipFile closed"); 640 } 641 } 642 643 /* 644 * Inner class implementing the input stream used to read a 645 * (possibly compressed) zip file entry. 646 */ 647 private class ZipFileInputStream extends InputStream { 648 private volatile boolean closeRequested = false; 649 protected long jzentry; // address of jzentry data 650 private long pos; // current position within entry data 651 protected long rem; // number of remaining bytes within entry 652 protected long size; // uncompressed size of this entry 653 654 ZipFileInputStream(long jzentry) { 655 pos = 0; 656 rem = getEntryCSize(jzentry); 657 size = getEntrySize(jzentry); 658 this.jzentry = jzentry; 659 } 660 661 public int read(byte b[], int off, int len) throws IOException { 662 if (rem == 0) { 663 return -1; 664 } 665 if (len <= 0) { 666 return 0; 667 } 668 if (len > rem) { 669 len = (int) rem; 670 } 671 synchronized (ZipFile.this) { 672 ensureOpenOrZipException(); 673 674 len = ZipFile.read(ZipFile.this.jzfile, jzentry, pos, b, 675 off, len); 676 } 677 if (len > 0) { 678 pos += len; 679 rem -= len; 680 } 681 if (rem == 0) { 682 close(); 683 } 684 return len; 685 } 686 687 public int read() throws IOException { 688 byte[] b = new byte[1]; 689 if (read(b, 0, 1) == 1) { 690 return b[0] & 0xff; 691 } else { 692 return -1; 693 } 694 } 695 696 public long skip(long n) { 697 if (n > rem) 698 n = rem; 699 pos += n; 700 rem -= n; 701 if (rem == 0) { 702 close(); 703 } 704 return n; 705 } 706 707 public int available() { 708 return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem; 709 } 710 711 public long size() { 712 return size; 713 } 714 715 public void close() { 716 if (closeRequested) 717 return; 718 closeRequested = true; 719 720 rem = 0; 721 synchronized (ZipFile.this) { 722 if (jzentry != 0 && ZipFile.this.jzfile != 0) { 723 freeEntry(ZipFile.this.jzfile, jzentry); 724 jzentry = 0; 725 } 726 } 727 synchronized (streams) { 728 streams.remove(this); 729 } 730 } 731 732 protected void finalize() { 733 close(); 734 } 735 } 736 737 /** 738 * Returns {@code true} if, and only if, the zip file begins with {@code 739 * LOCSIG}. 740 */ 741 private boolean startsWithLocHeader() { 742 return locsig; 743 } 744 745 private static native long open(String name, int mode, long lastModified, 746 boolean usemmap) throws IOException; 747 private static native int getTotal(long jzfile); 748 private static native boolean startsWithLOC(long jzfile); 749 private static native int read(long jzfile, long jzentry, 750 long pos, byte[] b, int off, int len); 751 752 // access to the native zentry object 753 private static native long getEntryTime(long jzentry); 754 private static native long getEntryCrc(long jzentry); 755 private static native long getEntryCSize(long jzentry); 756 private static native long getEntrySize(long jzentry); 757 private static native int getEntryMethod(long jzentry); 758 private static native int getEntryFlag(long jzentry); 759 private static native byte[] getCommentBytes(long jzfile); 760 761 private static final int JZENTRY_NAME = 0; 762 private static final int JZENTRY_EXTRA = 1; 763 private static final int JZENTRY_COMMENT = 2; 764 private static native byte[] getEntryBytes(long jzentry, int type); 765 766 private static native String getZipMessage(long jzfile); 767} 768