/**
* This class is a complete rewrite which tries to provide most basic
* functionality for reading zip files in J2ME environments. It tries
* to adhere to the handling set forth by java.util.zip of SUN's JDK.
* The implementation was written from scratch using:
* http://www.pkware.com/documents/casestudies/APPNOTE.TXT
*
* Copyright (c) 2009 Christian M�ller <cmue81 at \g\m\x dot \d\e>
* <trendypack at users dot sourceforge dot net>
*
* This program 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 of the License, or
* (at your option) any later version.
*/
package net.sourceforge.util.zip;
//#if polish.api.fileconnection
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import de.enough.polish.util.HashMap;
import de.enough.polish.util.zip.GZipInputStream;
//#endif
//#if polish.android
import java.io.File;
import java.io.FileInputStream;
import java.io.RandomAccessFile;
//#endif
public class ZipFile {
//#if polish.api.fileconnection
//#if polish.android
private File file;
private RandomAccessFile raFile;
private InputStream ais;
//#else
private FileConnection fc;
//#endif
private HashMap contents;
private int contents_limit;
/*private DataOutputStream log;*/
public ZipFile(String fileUrl, int limitIndexCache) throws IOException {
/*
FileConnection fc2 = (FileConnection) Connector.open("file:///E:/log.txt");
if (fc2.exists())
fc2.delete();
fc2.create();
log = fc2.openDataOutputStream();
*/
//#if polish.android
// strip file:// prefix
file = new File(fileUrl.substring("file://".length()));
if (file == null)
throw new IOException("file unreadable");
//ais = new FileInputStream(file);
raFile = new RandomAccessFile(file, "r");
if (raFile == null)
throw new IOException("file unreadable");
//if (ais == null)
// throw new IOException("file unreadable");
//#else
fc = (FileConnection) Connector.open(fileUrl, Connector.READ);
if (fc == null || !fc.canRead())
throw new IOException("file unreadable");
//#endif
contents = new HashMap();
contents_limit = limitIndexCache;
if (contents_limit<0 && !(readEntries()>0))
throw new IOException("no entries in file");
/*
InputStream tmp;
for (int v, i=2; i!=0; i--) {
log.writeChars("before reopen META-INF/MANIFEST.MF\n\n\n"); log.flush();
tmp = getInputStream(getEntry("META-INF/MANIFEST.MF"));
while ((v=tmp.read())!=-1)
log.writeChar(v);
tmp.close();
tmp = null;
}
log.writeChars("before reopen for printing index\n\n\n"); log.flush();
for (de.enough.polish.util.Iterator i = contents.keysIterator();i.hasNext();) {
String s = i.next();
log.writeChars(s+"\n size: "+((ZipEntry)contents.get(s)).getSize()+"\n\n");
}
log.close();
log = null;
contents = new HashMap(); // empty HashMap, don't hand out streams while testing
*/
}
public ZipFile(String fileUrl) throws IOException {
this(fileUrl, -1);
}
/**
* Get a @ZipEntry for the given name, use different methods depending on
* whether there is a limited index cache or all of it. If @limitIndexCache
* is negative, the whole index of the ZipFile was read into a HashMap at
* constructor time. The elements in the HashMap are of type ZipEntry.
*
* If @limitIndexCache is zero, no caching will be used at all and the
* lookup is done reading out sequentially the ZipFile's central directory
* each time this method is called.
*
* If @limitIndexCache is a positive number above zero, the number of items
* in the HashMap will not exceed this number and a new item not yet in the
* HashMap will displace exactly this item which was accessed before all
* other items in the HashMap. The elements in the HashMap are of type
* LinkedZipEntry in this case.
*
* @param name The filename of the entry to look for.
* @return a @ZipEntry or null
*/
public ZipEntry getEntry(String name) {
return
(contents_limit<0)
? (ZipEntry) contents.get(name)
: findEntry(name);
}
public synchronized InputStream getInputStream(ZipEntry e) throws IOException {
InputStream ret = null;
if (e != null) {
byte [] b = new byte [(int)e.getCompressedSize()+8];
int i = b.length-8;
/*log.writeChars("attempting "+e.getSize()); log.flush();*/
//#if polish.android
raFile.seek(e.offset);
raFile.read(b, 0, i);
//#else
ret = fc.openInputStream();
ret.skip(e.offset);
ret.read(b, 0, i);
ret.close();
//#endif
ret = null;
/*log.writeChars("done "+e.getSize()); log.flush();*/
switch (e.getMethod()) {
case ZipEntry.TYPE_STORED: {
ret = new ByteArrayInputStream(b, 0, i);
break;
}
case ZipEntry.TYPE_DEFLATE: {
long n = e.getSize(); n <<= 32;
do {
b[i++] = (byte)(n&0xFF); n >>>= 8;
} while (i<b.length);
ret = new GZipInputStream(new ByteArrayInputStream(b, 0, i),
GZipInputStream.TYPE_DEFLATE, false);
break;
}
default:
throw new IOException("invalid encoding");
}
}
return ret;
}
private ZipEntry readEntry(TinyBufInputStream f, boolean cendir) throws IOException {
ZipEntry e = new ZipEntry("");
int fnlen, eflen, colen=0, gpbits=0; // fnlen, eflen are reused occasionally..
byte [] fn = new byte [32];
if (cendir) f.skip(2); // version made by
f.skip(2); // version needed to extract
gpbits = f.readShortLE(); // gp bit flag
e.setMethod(f.readShortLE()); // compression method
f.skip(8); // mod time and date, CRC-32
e.setCompressedSize(f.readIntLE()); // compressed size
e.setSize(f.readIntLE()); // uncompressed size
fnlen = f.readShortLE(); // filename length
eflen = f.readShortLE(); // extra field length
if (cendir) {
colen = f.readShortLE(); // comment length
f.skip(8); // disknumstart, file attr
e.offset = f.readIntLE()+ZipConstants.LOCHDR+fnlen+eflen;
}
// read filename for entry
while (fnlen > fn.length)
fn = new byte [2*fn.length];
f.read(fn, 0, fnlen);
e.setName(new String(fn, 0, fnlen, "ISO-8859-1")); // "UTF-8", "US-ASCII", ..
// read extra field(s) if present
while (eflen > 0) {
fnlen = f.readShortLE(); // header id of (header,data) pairs
if (fnlen == 1) // look for Zip64 header for pair
e.setZip64Entry(fnlen == 1);
fnlen = f.readShortLE(); // get length of data for pair
f.skip(fnlen); // and skip that data
eflen -= (4 + fnlen); // proceed with next extra field
}
if (cendir) {
f.skip(colen);
}
else {
// save file data offset
e.offset = f.tell();
// read data descriptor and skip to next LOCHDR
if ((gpbits & 0x08) != 0) {
/* crc-32 and file sizes not yet known,
* find data descriptor that succeeds file data
*
* there are two issues using the following method
* 1) an EXTSIG can be found, so that the 8-12 byte read after EXTSIG
* equals actual data_bytes read, but it's just part of the stream
* 2) if (actual data_bytes != compressedSize), we resume looking for the
* signature after already having skipped 4 bytes (the crc-32 field)
*/
fnlen = f.readIntLE(); // last 4 bytes of data, little endian
do {
while (fnlen != ZipConstants.EXTSIG) {
fnlen >>>= 8;
fnlen |= (f.read()<<24);
}
eflen = f.tell()-e.offset-4; // actual data_bytes we read
f.skip(4); // skip crc-32 entry
if (e.isZip64Entry()) // read compressedSize
fnlen = (int)f.readLongLE();
else
fnlen = f.readIntLE();
} while (fnlen != eflen); // if sanity check fails, keep looking
// data descriptor found (compressedSize == actual data_bytes)
e.setCompressedSize(fnlen);
if (e.isZip64Entry())
e.setSize((int)f.readLongLE());
else
e.setSize(f.readIntLE());
}
else {
/* crc-32 and file sizes known and valid,
* no data descriptor present, skip to next LOCHDR
*/
f.skip(e.getCompressedSize());
}
}
return e;
}
private int readEntries() {
try {
TinyBufInputStream f = new TinyBufInputStream(1600);
ZipEntry e;
/* if there is a central directory, use it for more speed,
* otherwise run through the file to discover all entries
*/
boolean cdir = f.seekCENDIR();
int oursig = cdir ? ZipConstants.CENSIG : ZipConstants.LOCSIG;
while (f.readIntLE()==oursig) {
e = readEntry(f, cdir);
contents.put(e.getName(), e);
}
f.close();
}
catch (IOException e) { }
return contents.size();
}
/**
* This method is used to fetch an entry if we do not (want to) hold an
* index of all contents of the ZipFile in memory. It parses the ZipFile for
* an entry and caches up to @contents_size entries for later reference in
* the hope that this is of any use for the higher level code using ZipFile.
*
* If the usage pattern is such, that the probability of reading a file
* twice is very low, i.e. it is more probable that the entry is kicked out
* of the cache most of the time before being read a second time, then it
* is suggested to either increase @contents_size at constructor time or set
* it to zero to not use the cache at all.
*
* Note that the lookup is still done using a HashMap, so cached entries are
* found in O(log n) time. Also, for small caches it might be more memory
* effective to just use Vector instead of a Double Linked List. Using
* Vector might save some memory at the cost of CPU cycles, but it is not
* implemented at this time.
*/
private ZipEntry findEntry(String name) {
ZipEntry e = (ZipEntry) contents.get(name);
if (e != null) {
((LinkedZipEntry) e).moveToStart();
}
else {
try {
TinyBufInputStream f = new TinyBufInputStream(1600);
boolean cdir = f.seekCENDIR();
int oursig = cdir ? ZipConstants.CENSIG : ZipConstants.LOCSIG;
while (f.readIntLE()==oursig) {
e = readEntry(f, cdir);
if (e.getName().equals(name)) {
// requested entry found
if (contents_limit>1) {
// use humble LRU cache
LinkedZipEntry le = new LinkedZipEntry(e);
if (contents.size() >= contents_limit)
contents.remove(LinkedZipEntry.removeEnd().getName());
contents.put(le.getName(), le);
}
break;
}
}
f.close();
}
catch (IOException ex) { }
}
return e;
}
private class TinyBufInputStream extends InputStream {
private InputStream is;
private byte [] buf;
private int bufp, buflen, buflen_orig, fp, n;
int cendir_entries, cendir_size;
TinyBufInputStream(int buffer_length) throws IOException {
buflen_orig = buffer_length;
reset();
}
public void close() throws IOException {
is.close();
is = null;
}
public void reset() throws IOException {
if (is != null) close();
//#if polish.android
is = new FileInputStream(file);
//#else
is = fc.openInputStream();
//#endif
buf = new byte [buflen_orig];
bufp = buflen = buflen_orig;
fp = -buflen_orig;
}
/**
* Returns the byte read or throws an IOException on EOF or
* other error cases. We do not handle EOF by returning -1
* to ease the implementation of readIntLE, etc. methods.
*
* @return the byte read (0..255)
* @throws IOException
*/
public int read() throws IOException {
if (bufp>=buflen)
if (!dobuf())
// due to no error checking in read{Short,Int,Long}LE
throw new IOException("eof or error");
return buf[bufp++]&0xFF;
}
public int read(byte[] b) throws IOException {
return this.read(b, 0, b.length);
}
/**
* Reads bytes from stream into <b>, starting with <b[off]> and
* ending with <b[off+bytes_read]>. bytes_read can be less
* than the <len> requested bytes.
*
* @return the number of bytes read into b
*/
public int read(byte[] b, int off, int len) throws IOException {
int ln = len;
while (ln>0) {
if (bufp>=buflen)
if (!dobuf())
break;
n = buflen-bufp;
if (ln<n) n=ln;
System.arraycopy(buf, bufp, b, off, n);
off += n;
ln -= n;
bufp += n;
}
return len-ln;
}
int readShortLE() throws IOException {
return read()|(read()<<8);
}
int readIntLE() throws IOException {
return read()|(read()<<8)|(read()<<16)|(read()<<24);
}
long readLongLE() throws IOException {
return read()|(read()<<8)|(read()<<16)|(read()<<24)
|(((long)read())<<32)|(((long)read())<<40)
|(((long)read())<<48)|(((long)read())<<56);
}
int skip(int n) { // does not override skip(long) on purpose
bufp+=n;
return n;
}
int tell() {
return fp+bufp;
}
/**
* Seeks to the offset of the central directory if it is a ZipFile.
* Calling this method resets the InputStream.
* @return offset
* @throws IOException
*/
boolean seekCENDIR() throws IOException {
int ret=0;
if (tell()>0) reset();
if (readIntLE()==ZipConstants.LOCSIG) {
// we do not parse archive comments,
// this is just a quick shot to get to the directory
//#if polish.android
skip((int)file.length()-22-4);
//#else
skip((int)fc.fileSize()-22-4);
//#endif
if (readIntLE()==ZipConstants.ENDSIG) {
skip(6);
cendir_entries = readShortLE();
cendir_size = readIntLE();
ret = readIntLE();
reset();
skip(ret);
}
}
return ret>0;
}
private boolean dobuf() throws IOException {
is.skip(bufp-buflen);
n = is.read(buf, 0, buflen);
if (n<=0)
return false;
if (n<buflen)
buflen=n;
fp+=bufp;
bufp=0;
return true;
}
}
//#endif
}