| /* ZipFile.java -- | |
| Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006 | |
| Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath 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 for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| exception statement from your version. */ | |
| package java.util.zip; | |
| import gnu.java.util.EmptyEnumeration; | |
| import java.io.EOFException; | |
| import java.io.File; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.RandomAccessFile; | |
| import java.io.UnsupportedEncodingException; | |
| import java.util.Enumeration; | |
| import java.util.Iterator; | |
| import java.util.LinkedHashMap; | |
| /** | |
| * This class represents a Zip archive. You can ask for the contained | |
| * entries, or get an input stream for a file entry. The entry is | |
| * automatically decompressed. | |
| * | |
| * This class is thread safe: You can open input streams for arbitrary | |
| * entries in different threads. | |
| * | |
| * @author Jochen Hoenicke | |
| * @author Artur Biesiadowski | |
| */ | |
| public class ZipFile implements ZipConstants | |
| { | |
| /** | |
| * Mode flag to open a zip file for reading. | |
| */ | |
| public static final int OPEN_READ = 0x1; | |
| /** | |
| * Mode flag to delete a zip file after reading. | |
| */ | |
| public static final int OPEN_DELETE = 0x4; | |
| /** | |
| * This field isn't defined in the JDK's ZipConstants, but should be. | |
| */ | |
| static final int ENDNRD = 4; | |
| // Name of this zip file. | |
| private final String name; | |
| // File from which zip entries are read. | |
| private final RandomAccessFile raf; | |
| // The entries of this zip file when initialized and not yet closed. | |
| private LinkedHashMap entries; | |
| private boolean closed = false; | |
| /** | |
| * Helper function to open RandomAccessFile and throw the proper | |
| * ZipException in case opening the file fails. | |
| * | |
| * @param name the file name, or null if file is provided | |
| * | |
| * @param file the file, or null if name is provided | |
| * | |
| * @return the newly open RandomAccessFile, never null | |
| */ | |
| private RandomAccessFile openFile(String name, | |
| File file) | |
| throws ZipException, IOException | |
| { | |
| try | |
| { | |
| return | |
| (name != null) | |
| ? new RandomAccessFile(name, "r") | |
| : new RandomAccessFile(file, "r"); | |
| } | |
| catch (FileNotFoundException f) | |
| { | |
| ZipException ze = new ZipException(f.getMessage()); | |
| ze.initCause(f); | |
| throw ze; | |
| } | |
| } | |
| /** | |
| * Opens a Zip file with the given name for reading. | |
| * @exception IOException if a i/o error occured. | |
| * @exception ZipException if the file doesn't contain a valid zip | |
| * archive. | |
| */ | |
| public ZipFile(String name) throws ZipException, IOException | |
| { | |
| this.raf = openFile(name,null); | |
| this.name = name; | |
| checkZipFile(); | |
| } | |
| /** | |
| * Opens a Zip file reading the given File. | |
| * @exception IOException if a i/o error occured. | |
| * @exception ZipException if the file doesn't contain a valid zip | |
| * archive. | |
| */ | |
| public ZipFile(File file) throws ZipException, IOException | |
| { | |
| this.raf = openFile(null,file); | |
| this.name = file.getPath(); | |
| checkZipFile(); | |
| } | |
| /** | |
| * Opens a Zip file reading the given File in the given mode. | |
| * | |
| * If the OPEN_DELETE mode is specified, the zip file will be deleted at | |
| * some time moment after it is opened. It will be deleted before the zip | |
| * file is closed or the Virtual Machine exits. | |
| * | |
| * The contents of the zip file will be accessible until it is closed. | |
| * | |
| * @since JDK1.3 | |
| * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE | |
| * | |
| * @exception IOException if a i/o error occured. | |
| * @exception ZipException if the file doesn't contain a valid zip | |
| * archive. | |
| */ | |
| public ZipFile(File file, int mode) throws ZipException, IOException | |
| { | |
| if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) | |
| throw new IllegalArgumentException("invalid mode"); | |
| if ((mode & OPEN_DELETE) != 0) | |
| file.deleteOnExit(); | |
| this.raf = openFile(null,file); | |
| this.name = file.getPath(); | |
| checkZipFile(); | |
| } | |
| private void checkZipFile() throws ZipException | |
| { | |
| boolean valid = false; | |
| try | |
| { | |
| byte[] buf = new byte[4]; | |
| raf.readFully(buf); | |
| int sig = buf[0] & 0xFF | |
| | ((buf[1] & 0xFF) << 8) | |
| | ((buf[2] & 0xFF) << 16) | |
| | ((buf[3] & 0xFF) << 24); | |
| valid = sig == LOCSIG; | |
| } | |
| catch (IOException _) | |
| { | |
| } | |
| if (!valid) | |
| { | |
| try | |
| { | |
| raf.close(); | |
| } | |
| catch (IOException _) | |
| { | |
| } | |
| throw new ZipException("Not a valid zip file"); | |
| } | |
| } | |
| /** | |
| * Checks if file is closed and throws an exception. | |
| */ | |
| private void checkClosed() | |
| { | |
| if (closed) | |
| throw new IllegalStateException("ZipFile has closed: " + name); | |
| } | |
| /** | |
| * Read the central directory of a zip file and fill the entries | |
| * array. This is called exactly once when first needed. It is called | |
| * while holding the lock on <code>raf</code>. | |
| * | |
| * @exception IOException if a i/o error occured. | |
| * @exception ZipException if the central directory is malformed | |
| */ | |
| private void readEntries() throws ZipException, IOException | |
| { | |
| /* Search for the End Of Central Directory. When a zip comment is | |
| * present the directory may start earlier. | |
| * Note that a comment has a maximum length of 64K, so that is the | |
| * maximum we search backwards. | |
| */ | |
| PartialInputStream inp = new PartialInputStream(raf, 4096); | |
| long pos = raf.length() - ENDHDR; | |
| long top = Math.max(0, pos - 65536); | |
| do | |
| { | |
| if (pos < top) | |
| throw new ZipException | |
| ("central directory not found, probably not a zip file: " + name); | |
| inp.seek(pos--); | |
| } | |
| while (inp.readLeInt() != ENDSIG); | |
| if (inp.skip(ENDTOT - ENDNRD) != ENDTOT - ENDNRD) | |
| throw new EOFException(name); | |
| int count = inp.readLeShort(); | |
| if (inp.skip(ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ) | |
| throw new EOFException(name); | |
| int centralOffset = inp.readLeInt(); | |
| entries = new LinkedHashMap(count+count/2); | |
| inp.seek(centralOffset); | |
| for (int i = 0; i < count; i++) | |
| { | |
| if (inp.readLeInt() != CENSIG) | |
| throw new ZipException("Wrong Central Directory signature: " + name); | |
| inp.skip(6); | |
| int method = inp.readLeShort(); | |
| int dostime = inp.readLeInt(); | |
| int crc = inp.readLeInt(); | |
| int csize = inp.readLeInt(); | |
| int size = inp.readLeInt(); | |
| int nameLen = inp.readLeShort(); | |
| int extraLen = inp.readLeShort(); | |
| int commentLen = inp.readLeShort(); | |
| inp.skip(8); | |
| int offset = inp.readLeInt(); | |
| String name = inp.readString(nameLen); | |
| ZipEntry entry = new ZipEntry(name); | |
| entry.setMethod(method); | |
| entry.setCrc(crc & 0xffffffffL); | |
| entry.setSize(size & 0xffffffffL); | |
| entry.setCompressedSize(csize & 0xffffffffL); | |
| entry.setDOSTime(dostime); | |
| if (extraLen > 0) | |
| { | |
| byte[] extra = new byte[extraLen]; | |
| inp.readFully(extra); | |
| entry.setExtra(extra); | |
| } | |
| if (commentLen > 0) | |
| { | |
| entry.setComment(inp.readString(commentLen)); | |
| } | |
| entry.offset = offset; | |
| entries.put(name, entry); | |
| } | |
| } | |
| /** | |
| * Closes the ZipFile. This also closes all input streams given by | |
| * this class. After this is called, no further method should be | |
| * called. | |
| * | |
| * @exception IOException if a i/o error occured. | |
| */ | |
| public void close() throws IOException | |
| { | |
| RandomAccessFile raf = this.raf; | |
| if (raf == null) | |
| return; | |
| synchronized (raf) | |
| { | |
| closed = true; | |
| entries = null; | |
| raf.close(); | |
| } | |
| } | |
| /** | |
| * Calls the <code>close()</code> method when this ZipFile has not yet | |
| * been explicitly closed. | |
| */ | |
| protected void finalize() throws IOException | |
| { | |
| if (!closed && raf != null) close(); | |
| } | |
| /** | |
| * Returns an enumeration of all Zip entries in this Zip file. | |
| * | |
| * @exception IllegalStateException when the ZipFile has already been closed | |
| */ | |
| public Enumeration entries() | |
| { | |
| checkClosed(); | |
| try | |
| { | |
| return new ZipEntryEnumeration(getEntries().values().iterator()); | |
| } | |
| catch (IOException ioe) | |
| { | |
| return EmptyEnumeration.getInstance(); | |
| } | |
| } | |
| /** | |
| * Checks that the ZipFile is still open and reads entries when necessary. | |
| * | |
| * @exception IllegalStateException when the ZipFile has already been closed. | |
| * @exception IOException when the entries could not be read. | |
| */ | |
| private LinkedHashMap getEntries() throws IOException | |
| { | |
| synchronized(raf) | |
| { | |
| checkClosed(); | |
| if (entries == null) | |
| readEntries(); | |
| return entries; | |
| } | |
| } | |
| /** | |
| * Searches for a zip entry in this archive with the given name. | |
| * | |
| * @param name the name. May contain directory components separated by | |
| * slashes ('/'). | |
| * @return the zip entry, or null if no entry with that name exists. | |
| * | |
| * @exception IllegalStateException when the ZipFile has already been closed | |
| */ | |
| public ZipEntry getEntry(String name) | |
| { | |
| checkClosed(); | |
| try | |
| { | |
| LinkedHashMap entries = getEntries(); | |
| ZipEntry entry = (ZipEntry) entries.get(name); | |
| // If we didn't find it, maybe it's a directory. | |
| if (entry == null && !name.endsWith("/")) | |
| entry = (ZipEntry) entries.get(name + '/'); | |
| return entry != null ? new ZipEntry(entry, name) : null; | |
| } | |
| catch (IOException ioe) | |
| { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Creates an input stream reading the given zip entry as | |
| * uncompressed data. Normally zip entry should be an entry | |
| * returned by getEntry() or entries(). | |
| * | |
| * This implementation returns null if the requested entry does not | |
| * exist. This decision is not obviously correct, however, it does | |
| * appear to mirror Sun's implementation, and it is consistant with | |
| * their javadoc. On the other hand, the old JCL book, 2nd Edition, | |
| * claims that this should return a "non-null ZIP entry". We have | |
| * chosen for now ignore the old book, as modern versions of Ant (an | |
| * important application) depend on this behaviour. See discussion | |
| * in this thread: | |
| * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html | |
| * | |
| * @param entry the entry to create an InputStream for. | |
| * @return the input stream, or null if the requested entry does not exist. | |
| * | |
| * @exception IllegalStateException when the ZipFile has already been closed | |
| * @exception IOException if a i/o error occured. | |
| * @exception ZipException if the Zip archive is malformed. | |
| */ | |
| public InputStream getInputStream(ZipEntry entry) throws IOException | |
| { | |
| checkClosed(); | |
| LinkedHashMap entries = getEntries(); | |
| String name = entry.getName(); | |
| ZipEntry zipEntry = (ZipEntry) entries.get(name); | |
| if (zipEntry == null) | |
| return null; | |
| PartialInputStream inp = new PartialInputStream(raf, 1024); | |
| inp.seek(zipEntry.offset); | |
| if (inp.readLeInt() != LOCSIG) | |
| throw new ZipException("Wrong Local header signature: " + name); | |
| inp.skip(4); | |
| if (zipEntry.getMethod() != inp.readLeShort()) | |
| throw new ZipException("Compression method mismatch: " + name); | |
| inp.skip(16); | |
| int nameLen = inp.readLeShort(); | |
| int extraLen = inp.readLeShort(); | |
| inp.skip(nameLen + extraLen); | |
| inp.setLength(zipEntry.getCompressedSize()); | |
| int method = zipEntry.getMethod(); | |
| switch (method) | |
| { | |
| case ZipOutputStream.STORED: | |
| return inp; | |
| case ZipOutputStream.DEFLATED: | |
| inp.addDummyByte(); | |
| final Inflater inf = new Inflater(true); | |
| final int sz = (int) entry.getSize(); | |
| return new InflaterInputStream(inp, inf) | |
| { | |
| public int available() throws IOException | |
| { | |
| if (sz == -1) | |
| return super.available(); | |
| if (super.available() != 0) | |
| return sz - inf.getTotalOut(); | |
| return 0; | |
| } | |
| }; | |
| default: | |
| throw new ZipException("Unknown compression method " + method); | |
| } | |
| } | |
| /** | |
| * Returns the (path) name of this zip file. | |
| */ | |
| public String getName() | |
| { | |
| return name; | |
| } | |
| /** | |
| * Returns the number of entries in this zip file. | |
| * | |
| * @exception IllegalStateException when the ZipFile has already been closed | |
| */ | |
| public int size() | |
| { | |
| checkClosed(); | |
| try | |
| { | |
| return getEntries().size(); | |
| } | |
| catch (IOException ioe) | |
| { | |
| return 0; | |
| } | |
| } | |
| private static class ZipEntryEnumeration implements Enumeration | |
| { | |
| private final Iterator elements; | |
| public ZipEntryEnumeration(Iterator elements) | |
| { | |
| this.elements = elements; | |
| } | |
| public boolean hasMoreElements() | |
| { | |
| return elements.hasNext(); | |
| } | |
| public Object nextElement() | |
| { | |
| /* We return a clone, just to be safe that the user doesn't | |
| * change the entry. | |
| */ | |
| return ((ZipEntry)elements.next()).clone(); | |
| } | |
| } | |
| private static final class PartialInputStream extends InputStream | |
| { | |
| private final RandomAccessFile raf; | |
| private final byte[] buffer; | |
| private long bufferOffset; | |
| private int pos; | |
| private long end; | |
| // We may need to supply an extra dummy byte to our reader. | |
| // See Inflater. We use a count here to simplify the logic | |
| // elsewhere in this class. Note that we ignore the dummy | |
| // byte in methods where we know it is not needed. | |
| private int dummyByteCount; | |
| public PartialInputStream(RandomAccessFile raf, int bufferSize) | |
| throws IOException | |
| { | |
| this.raf = raf; | |
| buffer = new byte[bufferSize]; | |
| bufferOffset = -buffer.length; | |
| pos = buffer.length; | |
| end = raf.length(); | |
| } | |
| void setLength(long length) | |
| { | |
| end = bufferOffset + pos + length; | |
| } | |
| private void fillBuffer() throws IOException | |
| { | |
| synchronized (raf) | |
| { | |
| long len = end - bufferOffset; | |
| if (len == 0 && dummyByteCount > 0) | |
| { | |
| buffer[0] = 0; | |
| dummyByteCount = 0; | |
| } | |
| else | |
| { | |
| raf.seek(bufferOffset); | |
| raf.readFully(buffer, 0, (int) Math.min(buffer.length, len)); | |
| } | |
| } | |
| } | |
| public int available() | |
| { | |
| long amount = end - (bufferOffset + pos); | |
| if (amount > Integer.MAX_VALUE) | |
| return Integer.MAX_VALUE; | |
| return (int) amount; | |
| } | |
| public int read() throws IOException | |
| { | |
| if (bufferOffset + pos >= end + dummyByteCount) | |
| return -1; | |
| if (pos == buffer.length) | |
| { | |
| bufferOffset += buffer.length; | |
| pos = 0; | |
| fillBuffer(); | |
| } | |
| return buffer[pos++] & 0xFF; | |
| } | |
| public int read(byte[] b, int off, int len) throws IOException | |
| { | |
| if (len > end + dummyByteCount - (bufferOffset + pos)) | |
| { | |
| len = (int) (end + dummyByteCount - (bufferOffset + pos)); | |
| if (len == 0) | |
| return -1; | |
| } | |
| int totalBytesRead = Math.min(buffer.length - pos, len); | |
| System.arraycopy(buffer, pos, b, off, totalBytesRead); | |
| pos += totalBytesRead; | |
| off += totalBytesRead; | |
| len -= totalBytesRead; | |
| while (len > 0) | |
| { | |
| bufferOffset += buffer.length; | |
| pos = 0; | |
| fillBuffer(); | |
| int remain = Math.min(buffer.length, len); | |
| System.arraycopy(buffer, pos, b, off, remain); | |
| pos += remain; | |
| off += remain; | |
| len -= remain; | |
| totalBytesRead += remain; | |
| } | |
| return totalBytesRead; | |
| } | |
| public long skip(long amount) throws IOException | |
| { | |
| if (amount < 0) | |
| return 0; | |
| if (amount > end - (bufferOffset + pos)) | |
| amount = end - (bufferOffset + pos); | |
| seek(bufferOffset + pos + amount); | |
| return amount; | |
| } | |
| void seek(long newpos) throws IOException | |
| { | |
| long offset = newpos - bufferOffset; | |
| if (offset >= 0 && offset <= buffer.length) | |
| { | |
| pos = (int) offset; | |
| } | |
| else | |
| { | |
| bufferOffset = newpos; | |
| pos = 0; | |
| fillBuffer(); | |
| } | |
| } | |
| void readFully(byte[] buf) throws IOException | |
| { | |
| if (read(buf, 0, buf.length) != buf.length) | |
| throw new EOFException(); | |
| } | |
| void readFully(byte[] buf, int off, int len) throws IOException | |
| { | |
| if (read(buf, off, len) != len) | |
| throw new EOFException(); | |
| } | |
| int readLeShort() throws IOException | |
| { | |
| int b0 = read(); | |
| int b1 = read(); | |
| if (b1 == -1) | |
| throw new EOFException(); | |
| return (b0 & 0xff) | (b1 & 0xff) << 8; | |
| } | |
| int readLeInt() throws IOException | |
| { | |
| int b0 = read(); | |
| int b1 = read(); | |
| int b2 = read(); | |
| int b3 = read(); | |
| if (b3 == -1) | |
| throw new EOFException(); | |
| return ((b0 & 0xff) | (b1 & 0xff) << 8) | |
| | ((b2 & 0xff) | (b3 & 0xff) << 8) << 16; | |
| } | |
| String readString(int length) throws IOException | |
| { | |
| if (length > end - (bufferOffset + pos)) | |
| throw new EOFException(); | |
| try | |
| { | |
| if (buffer.length - pos >= length) | |
| { | |
| String s = new String(buffer, pos, length, "UTF-8"); | |
| pos += length; | |
| return s; | |
| } | |
| else | |
| { | |
| byte[] b = new byte[length]; | |
| readFully(b); | |
| return new String(b, 0, length, "UTF-8"); | |
| } | |
| } | |
| catch (UnsupportedEncodingException uee) | |
| { | |
| throw new AssertionError(uee); | |
| } | |
| } | |
| public void addDummyByte() | |
| { | |
| dummyByteCount = 1; | |
| } | |
| } | |
| } |