/* * Copyright (c) 1995, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util.zip; import java.io.Closeable; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.UncheckedIOException; import java.lang.ref.Cleaner.Cleanable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; import java.util.WeakHashMap; import java.util.stream.Stream; import java.util.stream.StreamSupport; import jdk.internal.misc.JavaUtilZipFileAccess; import jdk.internal.misc.SharedSecrets; import jdk.internal.misc.VM; import jdk.internal.perf.PerfCounter; import jdk.internal.ref.CleanerFactory; import static java.util.zip.ZipConstants64.*; import static java.util.zip.ZipUtils.*; /** * This class is used to read entries from a zip file. * *
Unless otherwise noted, passing a {@code null} argument to a constructor * or method in this class will cause a {@link NullPointerException} to be * thrown. * *
* @apiNote * In earlier versions, the {@link Object#finalize} method was overridden and * specified to close the ZipFile object and release its system resource when * the instance becomes unreachable. The {@code finalize} method is no longer * defined. The recommended cleanup for ZipFile object is to explicitly invoke * {@code close} method when it is no longer in use, or use try-with-resources. * * @implNote * The resources held by this object will be released when the instance becomes * phantom-reachable, if the {@code close} is not invoked explicitly. *
* * @author David Connelly * @since 1.1 */ public class ZipFile implements ZipConstants, Closeable { private final String name; // zip file name private volatile boolean closeRequested; private ZipCoder zc; // The "resource" used by this zip file that needs to be // cleaned after use. // a) the input streams that need to be closed // b) the list of cached Inflater objects // c) the "native" source of this zip file. private final CleanableResource res; private static final int STORED = ZipEntry.STORED; private static final int DEFLATED = ZipEntry.DEFLATED; /** * Mode flag to open a zip file for reading. */ public static final int OPEN_READ = 0x1; /** * Mode flag to open a zip file and mark it for deletion. The file will be * deleted some time between the moment that it is opened and the moment * that it is closed, but its contents will remain accessible via the * {@code ZipFile} object until either the close method is invoked or the * virtual machine exits. */ public static final int OPEN_DELETE = 0x4; /** * Opens a zip file for reading. * *
First, if there is a security manager, its {@code checkRead} * method is called with the {@code name} argument as its argument * to ensure the read is allowed. * *
The UTF-8 {@link java.nio.charset.Charset charset} is used to * decode the entry names and comments. * * @param name the name of the zip file * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred * @throws SecurityException if a security manager exists and its * {@code checkRead} method doesn't allow read access to the file. * * @see SecurityManager#checkRead(java.lang.String) */ public ZipFile(String name) throws IOException { this(new File(name), OPEN_READ); } /** * Opens a new {@code ZipFile} to read from the specified * {@code File} object in the specified mode. The mode argument * must be either {@code OPEN_READ} or {@code OPEN_READ | OPEN_DELETE}. * *
First, if there is a security manager, its {@code checkRead} * method is called with the {@code name} argument as its argument to * ensure the read is allowed. * *
The UTF-8 {@link java.nio.charset.Charset charset} is used to * decode the entry names and comments * * @param file the ZIP file to be opened for reading * @param mode the mode in which the file is to be opened * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred * @throws SecurityException if a security manager exists and * its {@code checkRead} method * doesn't allow read access to the file, * or its {@code checkDelete} method doesn't allow deleting * the file when the {@code OPEN_DELETE} flag is set. * @throws IllegalArgumentException if the {@code mode} argument is invalid * @see SecurityManager#checkRead(java.lang.String) * @since 1.3 */ public ZipFile(File file, int mode) throws IOException { this(file, mode, StandardCharsets.UTF_8); } /** * Opens a ZIP file for reading given the specified File object. * *
The UTF-8 {@link java.nio.charset.Charset charset} is used to * decode the entry names and comments. * * @param file the ZIP file to be opened for reading * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred */ public ZipFile(File file) throws ZipException, IOException { this(file, OPEN_READ); } /** * Opens a new {@code ZipFile} to read from the specified * {@code File} object in the specified mode. The mode argument * must be either {@code OPEN_READ} or {@code OPEN_READ | OPEN_DELETE}. * *
First, if there is a security manager, its {@code checkRead} * method is called with the {@code name} argument as its argument to * ensure the read is allowed. * * @param file the ZIP file to be opened for reading * @param mode the mode in which the file is to be opened * @param charset * the {@linkplain java.nio.charset.Charset charset} to * be used to decode the ZIP entry name and comment that are not * encoded by using UTF-8 encoding (indicated by entry's general * purpose flag). * * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred * * @throws SecurityException * if a security manager exists and its {@code checkRead} * method doesn't allow read access to the file,or its * {@code checkDelete} method doesn't allow deleting the * file when the {@code OPEN_DELETE} flag is set * * @throws IllegalArgumentException if the {@code mode} argument is invalid * * @see SecurityManager#checkRead(java.lang.String) * * @since 1.7 */ public ZipFile(File file, int mode, Charset charset) throws IOException { if (((mode & OPEN_READ) == 0) || ((mode & ~(OPEN_READ | OPEN_DELETE)) != 0)) { throw new IllegalArgumentException("Illegal mode: 0x"+ Integer.toHexString(mode)); } String name = file.getPath(); SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkRead(name); if ((mode & OPEN_DELETE) != 0) { sm.checkDelete(name); } } Objects.requireNonNull(charset, "charset"); this.zc = ZipCoder.get(charset); this.name = name; long t0 = System.nanoTime(); this.res = new CleanableResource(this, file, mode); PerfCounter.getZipFileOpenTime().addElapsedTimeFrom(t0); PerfCounter.getZipFileCount().increment(); } /** * Opens a zip file for reading. * *
First, if there is a security manager, its {@code checkRead} * method is called with the {@code name} argument as its argument * to ensure the read is allowed. * * @param name the name of the zip file * @param charset * the {@linkplain java.nio.charset.Charset charset} to * be used to decode the ZIP entry name and comment that are not * encoded by using UTF-8 encoding (indicated by entry's general * purpose flag). * * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred * @throws SecurityException * if a security manager exists and its {@code checkRead} * method doesn't allow read access to the file * * @see SecurityManager#checkRead(java.lang.String) * * @since 1.7 */ public ZipFile(String name, Charset charset) throws IOException { this(new File(name), OPEN_READ, charset); } /** * Opens a ZIP file for reading given the specified File object. * * @param file the ZIP file to be opened for reading * @param charset * The {@linkplain java.nio.charset.Charset charset} to be * used to decode the ZIP entry name and comment (ignored if * the language * encoding bit of the ZIP entry's general purpose bit * flag is set). * * @throws ZipException if a ZIP format error has occurred * @throws IOException if an I/O error has occurred * * @since 1.7 */ public ZipFile(File file, Charset charset) throws IOException { this(file, OPEN_READ, charset); } /** * Returns the zip file comment, or null if none. * * @return the comment string for the zip file, or null if none * * @throws IllegalStateException if the zip file has been closed * * @since 1.7 */ public String getComment() { synchronized (this) { ensureOpen(); if (res.zsrc.comment == null) { return null; } return zc.toString(res.zsrc.comment); } } /** * Returns the zip file entry for the specified name, or null * if not found. * * @param name the name of the entry * @return the zip file entry, or null if not found * @throws IllegalStateException if the zip file has been closed */ public ZipEntry getEntry(String name) { Objects.requireNonNull(name, "name"); synchronized (this) { ensureOpen(); byte[] bname = zc.getBytes(name); int pos = res.zsrc.getEntryPos(bname, true); if (pos != -1) { return getZipEntry(name, bname, pos); } } return null; } /** * Returns an input stream for reading the contents of the specified * zip file entry. *
* Closing this ZIP file will, in turn, close all input streams that
* have been returned by invocations of this method.
*
* @param entry the zip file entry
* @return the input stream for reading the contents of the specified
* zip file entry.
* @throws ZipException if a ZIP format error has occurred
* @throws IOException if an I/O error has occurred
* @throws IllegalStateException if the zip file has been closed
*/
public InputStream getInputStream(ZipEntry entry) throws IOException {
Objects.requireNonNull(entry, "entry");
int pos = -1;
ZipFileInputStream in = null;
Source zsrc = res.zsrc;
Set Closing this ZIP file will close all of the input streams
* previously returned by invocations of the {@link #getInputStream
* getInputStream} method.
*
* @throws IOException if an I/O error has occurred
*/
public void close() throws IOException {
if (closeRequested) {
return;
}
closeRequested = true;
synchronized (this) {
// Close streams, release their inflaters, release cached inflaters
// and release zip source
res.clean();
}
}
private void ensureOpen() {
if (closeRequested) {
throw new IllegalStateException("zip file closed");
}
}
private void ensureOpenOrZipException() throws IOException {
if (closeRequested) {
throw new ZipException("ZipFile closed");
}
}
/*
* Inner class implementing the input stream used to read a
* (possibly compressed) zip file entry.
*/
private class ZipFileInputStream extends InputStream {
private volatile boolean closeRequested;
private long pos; // current position within entry data
protected long rem; // number of remaining bytes within entry
protected long size; // uncompressed size of this entry
ZipFileInputStream(byte[] cen, int cenpos) throws IOException {
rem = CENSIZ(cen, cenpos);
size = CENLEN(cen, cenpos);
pos = CENOFF(cen, cenpos);
// zip64
if (rem == ZIP64_MAGICVAL || size == ZIP64_MAGICVAL ||
pos == ZIP64_MAGICVAL) {
checkZIP64(cen, cenpos);
}
// negative for lazy initialization, see getDataOffset();
pos = - (pos + ZipFile.this.res.zsrc.locpos);
}
private void checkZIP64(byte[] cen, int cenpos) throws IOException {
int off = cenpos + CENHDR + CENNAM(cen, cenpos);
int end = off + CENEXT(cen, cenpos);
while (off + 4 < end) {
int tag = get16(cen, off);
int sz = get16(cen, off + 2);
off += 4;
if (off + sz > end) // invalid data
break;
if (tag == EXTID_ZIP64) {
if (size == ZIP64_MAGICVAL) {
if (sz < 8 || (off + 8) > end)
break;
size = get64(cen, off);
sz -= 8;
off += 8;
}
if (rem == ZIP64_MAGICVAL) {
if (sz < 8 || (off + 8) > end)
break;
rem = get64(cen, off);
sz -= 8;
off += 8;
}
if (pos == ZIP64_MAGICVAL) {
if (sz < 8 || (off + 8) > end)
break;
pos = get64(cen, off);
sz -= 8;
off += 8;
}
break;
}
off += sz;
}
}
/* The Zip file spec explicitly allows the LOC extra data size to
* be different from the CEN extra data size. Since we cannot trust
* the CEN extra data size, we need to read the LOC to determine
* the entry data offset.
*/
private long initDataOffset() throws IOException {
if (pos <= 0) {
byte[] loc = new byte[LOCHDR];
pos = -pos;
int len = ZipFile.this.res.zsrc.readFullyAt(loc, 0, loc.length, pos);
if (len != LOCHDR) {
throw new ZipException("ZipFile error reading zip file");
}
if (LOCSIG(loc) != LOCSIG) {
throw new ZipException("ZipFile invalid LOC header (bad signature)");
}
pos += LOCHDR + LOCNAM(loc) + LOCEXT(loc);
}
return pos;
}
public int read(byte b[], int off, int len) throws IOException {
synchronized (ZipFile.this) {
ensureOpenOrZipException();
initDataOffset();
if (rem == 0) {
return -1;
}
if (len > rem) {
len = (int) rem;
}
if (len <= 0) {
return 0;
}
len = ZipFile.this.res.zsrc.readAt(b, off, len, pos);
if (len > 0) {
pos += len;
rem -= len;
}
}
if (rem == 0) {
close();
}
return len;
}
public int read() throws IOException {
byte[] b = new byte[1];
if (read(b, 0, 1) == 1) {
return b[0] & 0xff;
} else {
return -1;
}
}
public long skip(long n) throws IOException {
synchronized (ZipFile.this) {
ensureOpenOrZipException();
initDataOffset();
if (n > rem) {
n = rem;
}
pos += n;
rem -= n;
}
if (rem == 0) {
close();
}
return n;
}
public int available() {
return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
}
public long size() {
return size;
}
public void close() {
if (closeRequested) {
return;
}
closeRequested = true;
rem = 0;
synchronized (res.istreams) {
res.istreams.remove(this);
}
}
}
/**
* Returns the names of all non-directory entries that begin with
* "META-INF/" (case ignored). This method is used in JarFile, via
* SharedSecrets, as an optimization when looking up manifest and
* signature file entries. Returns null if no entries were found.
*/
private String[] getMetaInfEntryNames() {
synchronized (this) {
ensureOpen();
Source zsrc = res.zsrc;
if (zsrc.metanames == null) {
return null;
}
String[] names = new String[zsrc.metanames.length];
byte[] cen = zsrc.cen;
for (int i = 0; i < names.length; i++) {
int pos = zsrc.metanames[i];
names[i] = new String(cen, pos + CENHDR, CENNAM(cen, pos),
StandardCharsets.UTF_8);
}
return names;
}
}
private static boolean isWindows;
static {
SharedSecrets.setJavaUtilZipFileAccess(
new JavaUtilZipFileAccess() {
public boolean startsWithLocHeader(ZipFile zip) {
return zip.res.zsrc.startsWithLoc;
}
public String[] getMetaInfEntryNames(ZipFile zip) {
return zip.getMetaInfEntryNames();
}
}
);
isWindows = VM.getSavedProperty("os.name").contains("Windows");
}
private static class Source {
private final Key key; // the key in files
private int refs = 1;
private RandomAccessFile zfile; // zfile of the underlying zip file
private byte[] cen; // CEN & ENDHDR
private long locpos; // position of first LOC header (usually 0)
private byte[] comment; // zip file comment
// list of meta entries in META-INF dir
private int[] metanames;
private final boolean startsWithLoc; // true, if zip file starts with LOCSIG (usually true)
// A Hashmap for all entries.
//
// A cen entry of Zip/JAR file. As we have one for every entry in every active Zip/JAR,
// We might have a lot of these in a typical system. In order to save space we don't
// keep the name in memory, but merely remember a 32 bit {@code hash} value of the
// entry name and its offset {@code pos} in the central directory hdeader.
//
// private static class Entry {
// int hash; // 32 bit hashcode on name
// int next; // hash chain: index into entries
// int pos; // Offset of central directory file header
// }
// private Entry[] entries; // array of hashed cen entry
//
// To reduce the total size of entries further, we use a int[] here to store 3 "int"
// {@code hash}, {@code next and {@code "pos for each entry. The entry can then be
// referred by their index of their positions in the {@code entries}.
//
private int[] entries; // array of hashed cen entry
private int addEntry(int index, int hash, int next, int pos) {
entries[index++] = hash;
entries[index++] = next;
entries[index++] = pos;
return index;
}
private int getEntryHash(int index) { return entries[index]; }
private int getEntryNext(int index) { return entries[index + 1]; }
private int getEntryPos(int index) { return entries[index + 2]; }
private static final int ZIP_ENDCHAIN = -1;
private int total; // total number of entries
private int[] table; // Hash chain heads: indexes into entries
private int tablelen; // number of hash heads
private static class Key {
BasicFileAttributes attrs;
File file;
public Key(File file, BasicFileAttributes attrs) {
this.attrs = attrs;
this.file = file;
}
public int hashCode() {
long t = attrs.lastModifiedTime().toMillis();
return ((int)(t ^ (t >>> 32))) + file.hashCode();
}
public boolean equals(Object obj) {
if (obj instanceof Key) {
Key key = (Key)obj;
if (!attrs.lastModifiedTime().equals(key.attrs.lastModifiedTime())) {
return false;
}
Object fk = attrs.fileKey();
if (fk != null) {
return fk.equals(key.attrs.fileKey());
} else {
return file.equals(key.file);
}
}
return false;
}
}
private static final HashMap