Milán Major

netscape-revival

1 branch
Code

sun-java/classsrc/sun/tools/zip/ZipFile.java

/*
 * @(#)ZipFile.java	1.4 95/11/12 David Connelly
 *
 * Copyright (c) 1995 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Permission to use, copy, modify, and distribute this software
 * and its documentation for NON-COMMERCIAL purposes and without
 * fee is hereby granted provided that this copyright notice
 * appears in all copies. Please refer to the file "copyright.html"
 * for further important copyright and licensing information.
 *
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
 * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 */

package sun.tools.zip;

import java.io.RandomAccessFile;
import java.io.IOException;
import java.io.File;
import java.util.Hashtable;
import java.util.Enumeration;

/**
 * This class represents a zip file and can be used to read its contents.
 * Currently, there is no support for compressed or encrypted zip file
 * entries.
 *
 * @see ZipInputStream
 * @see ZipEntry
 * @version	1.4, 11/12/95
 * @author	David Connelly
 */
public
class ZipFile implements ZipConstants {
    private RandomAccessFile file;  // The open zip file
    private String path;	    // Path name of the zip file
    private Hashtable table;	    // Hash table of zip file entries
    long cenpos;		    // Position of central directory
    private long endpos;	    // Position of end-of-central directory
    private long pos;		    // Current position within zip file

    /**
     * The file separator string used for zip file entry names.
     */
    public static final String separator = "/";

    /**
     * The file separator character used for zip file entry names.
     */
    public static final char separatorChar = separator.charAt(0);

    /**
     * Opens a zip file given the specified path name.
     * @param path the name of the zip file
     * @exception ZipFormatException If the file is not a zip file.
     * @exception IOException If an I/O error has occurred.
     */
    public ZipFile(String path) throws ZipFormatException, IOException {
	file = new RandomAccessFile(path, "r");
	this.path = path;
	init();
    }

    /**
     * Opens a new zip file given the specified File object.
     * @param file the zip file to be opened for reading
     * @exception ZipFormatException If the file is not a zip file.
     * @exception IOException If an I/O error has occurred.
     */
    public ZipFile(File file) throws ZipFormatException, IOException {
	this(file.getPath());
    }

    /**
     * Returns the zip file entry for the given path name or null if
     * no such entry exists.
     * @param path the '/' delimited path name of the entry
     * @return the zip file entry
     */
    public ZipEntry get(String path) {
	return (ZipEntry)table.get(path);
    }

    /**
     * Returns the system-specific path name of the zip file.
     */
    public String getPath() {
        return path;
    }

    /**
     * Returns an enumeration of the zip file entries. Use the Enumeration
     * methods on the returned object to fetch the elements sequentially.
     * @see Enumeration
     */
    public Enumeration entries() {
	return table.elements();
    }

    /*
     * Reads data at specified file position into an array of bytes.
     * This method will block until some input is available.
     */
    synchronized int read(long pos, byte b[], int off, int len)
    	    throws IOException {
        if (pos != this.pos) {
	    file.seek(pos);
	}
	int n = file.read(b, off, len);
	if (n > 0) {
	    this.pos = pos + n;
	}
	return n;
    }

    /*
     * Reads a byte of data at the specified file position. This method
     * will block until some input is available.
     */
    synchronized int read() throws IOException {
	if (pos != this.pos) {
	    file.seek(pos);
	}
	int n = file.read();
	if (n > 0) {
	    this.pos = pos + 1;
	}
	return n;
    }

    /*
     * Read contents of central directory (CEN) and build hash table of
     * zip file entries.
     */
    private void init() throws ZipFormatException, IOException {
	// Seek to beginning of END record
	if (!findEnd()) {
	    throw new ZipFormatException("Could not find END header");
	}
	// Read END header and check signature
	byte endbuf[] = new byte[ENDHDRSIZ];
	file.readFully(endbuf);
	if (!sigMatch(endbuf, ENDSIG)) {
	    throw new ZipFormatException("Invalid END header signature"); 
	}
	// Get position and length of central directory
	cenpos = get32(endbuf, ENDOFF);
	int cenlen = (int)get32(endbuf, ENDSIZ);
	if (cenpos + cenlen != endpos) {
	    throw new ZipFormatException("Invalid END header format");
	}
	// Get total number of entries
	int nent = get16(endbuf, ENDTOT);
	if (nent * CENHDRSIZ > cenlen) {
	    throw new ZipFormatException("Invalid END header format");
	}
	// Check number of drives
	if (get16(endbuf, ENDSUB) != nent) {
	    throw new ZipFormatException("Contains more than one drive");
	}
	// Seek to first CEN record and read central directory
	file.seek(cenpos);
	byte cenbuf[] = new byte[cenlen];
	file.readFully(cenbuf);
	// Scan entries in central directory and build lookup table.
	table = new Hashtable(nent);
	for (int off = 0; off < cenlen; ) {
	    // Check CEN header signature
	    if (!sigMatch(cenbuf, off, CENSIG)) {
		throw new ZipFormatException("Invalid CEN header signature");
	    }
	    ZipEntry entry = new ZipEntry();
	    entry.zip = this;
	    // Get path name of entry
	    int pathlen = get16(cenbuf, off + CENNAM);
	    if (pathlen == 0 || off + CENHDRSIZ + pathlen > cenlen) {
		throw new ZipFormatException("Invalid CEN header format");
	    }
	    // Path names ending with '/' are for directory entries.
	    char c = (char)(cenbuf[off + CENHDRSIZ + pathlen - 1] & 0xff);
	    if (c == separatorChar) {
		entry.directory = true;
	    }
	    entry.path = new String(cenbuf, 0, off + CENHDRSIZ,
				    entry.directory ? pathlen - 1 : pathlen);
	    // Get position of LOC header
	    entry.locpos = get32(cenbuf, off + CENOFF);
	    if (entry.locpos + get32(cenbuf, off + CENSIZ) > cenpos) {
		throw new ZipFormatException("Invalid CEN header format");
	    }
	    // Get uncompressed file size
	    entry.length = get32(cenbuf, off + CENLEN);
	    // Get modification time
	    entry.mtime = get32(cenbuf, off + CENTIM);
	    // Add entry to hash table
	    if (table.put(entry.path, entry) != null) {
		throw new ZipFormatException("Invalid CEN header format");
	    }
	    // Skip to next header
	    off += CENHDRSIZ + pathlen + get16(cenbuf, off + CENEXT) +
		   get16(cenbuf, off + CENCOM);
	}
	// Make sure we got the right number of entries
	if (table.size() != nent) {
	    throw new ZipFormatException("Invalid CEN header format");
	}
    }

    /*
     * Adds new entry to lookup hash table.
     */
    private static final int INBUFSIZ = 64;

    /*
     * Find end-of-central (END) directory record.
     */
    private boolean findEnd() throws IOException {
	// Start searching backwards from end of file
	long len = file.length();
	file.seek(len);
	// Set limit on how far back we need to search. The END header
	// must be located within the last 64K bytes of the file.
	long markpos = Math.max(0, len - 0xffff);
	// Search backwards INBUFSIZ bytes at a time from end of file
	// stopping when the END header signature has been found. Since
	// the signature may straddle a buffer boundary, we need to stash
	// the first SIGSIZ-1 bytes of the previous record at the end of
	// the current record so that the search may overlap.
	byte buf[] = new byte[INBUFSIZ + SIGSIZ];
	setBytes(buf, INBUFSIZ, SIGSIZ, 0);
	for (pos = len; pos > markpos; ) {
	    int n = Math.min((int)(pos - markpos), INBUFSIZ);
	    pos -= n;
	    file.seek(pos);
	    file.readFully(buf, 0, n);
	    while (--n > 0) {
		if (sigMatch(buf, n, ENDSIG)) {
		    // Could be END header, but we need to make sure that
		    // the record extends to the end of the file.
		    endpos = pos + n;
		    if (len - endpos < ENDHDRSIZ) {
			continue;
		    }
		    file.seek(endpos);
		    byte endbuf[] = new byte[ENDHDRSIZ];
		    file.readFully(endbuf);
		    int comlen = get16(endbuf, ENDCOM);
		    if (endpos + ENDHDRSIZ + comlen != len) {
			continue;
		    }
		    // This is definitely the END record, so position
		    // the file pointer at the header and return.
		    file.seek(endpos);
		    pos = endpos;
		    return true;
		}
	    }
	}
	return false;
    }

    /*
     * Check for matching header signature at specified offset within
     * byte array.
     */
    static final boolean sigMatch(byte b[], int off, byte sig[]) {
	for (int i = 0; i < SIGSIZ; i++) {
	    if (b[off+i] != sig[i]) {
		return false;
	    }
	}
	return true;
    }

    /*
     * Check for matching header signature at start of byte array.
     */
    static final boolean sigMatch(byte b[], byte sig[]) {
	return sigMatch(b, 0, sig);
    }

    /*
     * Fill part of byte array with specified byte value.
     */
    static final void setBytes(byte b[], int off, int len, int v) {
	while (len-- > 0) {
	    b[off++] = (byte)v;
	}
    }

    /*
     * Fetch unsigned 16-bit value from byte array at specified offset.
     * The bytes are assumed to be in Intel (little-endian) byte order.
     */
    static final int get16(byte b[], int off) {
	return (b[off] & 0xff) | ((b[off+1] & 0xff) << 8);
    }

    /*
     * Fetch unsigned 32-bit value from byte array at specified offset.
     * The bytes are assumed to be in Intel (little-endian) byte order.
     */
    static final long get32(byte b[], int off) {
	return (b[off] & 0xff) | ((b[off+1] & 0xff) << 8) |
	       ((b[off+2] & 0xff) << 16) | ((b[off+3] & 0xff) << 24);
    }
}