View Javadoc

1   /*
2    * @(#)ZipInputStream.java	1.43 06/07/31
3    *
4    * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
5    * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6    */
7   
8   package org.varienaja.util;
9   
10  import java.io.EOFException;
11  import java.io.IOException;
12  import java.io.InputStream;
13  import java.io.PushbackInputStream;
14  import java.util.zip.CRC32;
15  import java.util.zip.Inflater;
16  import java.util.zip.InflaterInputStream;
17  import java.util.zip.ZipEntry;
18  import java.util.zip.ZipException;
19  
20  
21  /**
22   * This class implements an input stream filter for reading files in the
23   * ZIP file format. Includes support for both compressed and uncompressed
24   * entries.
25   *
26   * Taken from Sun, and extended it to 'support' odd filenames in .zip files. 
27   * @author	David Connelly
28   * @version	1.43, 07/31/06
29   * @author Varienaja
30   */
31  public
32  class TolerantZipInputStream extends InflaterInputStream  implements ZipConstants {
33      private ZipEntry entry;
34      private int flag;
35      private CRC32 crc = new CRC32();
36      private long remaining;
37      private byte[] tmpbuf = new byte[512];
38  
39      private static final int STORED = ZipEntry.STORED;
40      private static final int DEFLATED = ZipEntry.DEFLATED;
41  
42      private boolean closed = false;
43      // this flag is set to true after EOF has reached for
44      // one entry
45      private boolean entryEOF = false;
46  
47      /**
48       * Check to make sure that this stream has not been closed
49       */
50      private void ensureOpen() throws IOException {
51  	if (closed) {
52  	    throw new IOException("Stream closed");
53          }
54      }
55  
56      /**
57       * Creates a new ZIP input stream.
58       * @param in the actual input stream
59       */
60      public TolerantZipInputStream(InputStream in) {
61      	super(new PushbackInputStream(in, 512), new Inflater(true), 512);
62          if(in == null) {
63              throw new NullPointerException("in is null");
64          }
65      }
66      
67      /**
68       * Reads the next ZIP file entry and positions the stream at the
69       * beginning of the entry data.
70       * @return the next ZIP file entry, or null if there are no more entries
71       * @exception ZipException if a ZIP file error has occurred
72       * @exception IOException if an I/O error has occurred
73       */
74      public ZipEntry getNextEntry() throws IOException {
75          ensureOpen();
76  	if (entry != null) {
77  	    closeEntry();
78  	}
79  	crc.reset();
80  	inf.reset();
81  	if ((entry = readLOC()) == null) {
82  	    return null;
83  	}
84  	if (entry.getMethod() == STORED) {
85  	    remaining = entry.getSize();
86  	}
87          entryEOF = false;
88  	return entry;
89      }
90  
91      /**
92       * Closes the current ZIP entry and positions the stream for reading the
93       * next entry.
94       * @exception ZipException if a ZIP file error has occurred
95       * @exception IOException if an I/O error has occurred
96       */
97      public void closeEntry() throws IOException {
98          ensureOpen();
99  	while (read(tmpbuf, 0, tmpbuf.length) != -1) ;
100         entryEOF = true;
101     }
102 
103     /**
104      * Returns 0 after EOF has reached for the current entry data,
105      * otherwise always return 1.
106      * <p>
107      * Programs should not count on this method to return the actual number
108      * of bytes that could be read without blocking.
109      *
110      * @return     1 before EOF and 0 after EOF has reached for current entry.
111      * @exception  IOException  if an I/O error occurs.
112      *
113      */
114     public int available() throws IOException {
115         ensureOpen();
116         if (entryEOF) {
117             return 0;
118         } else {
119             return 1;
120         }
121     }
122 
123     /**
124      * Reads from the current ZIP entry into an array of bytes.
125      * If <code>len</code> is not zero, the method
126      * blocks until some input is available; otherwise, no
127      * bytes are read and <code>0</code> is returned.
128      * @param b the buffer into which the data is read
129      * @param off the start offset in the destination array <code>b</code>
130      * @param len the maximum number of bytes read
131      * @return the actual number of bytes read, or -1 if the end of the
132      *         entry is reached
133      * @exception  NullPointerException If <code>b</code> is <code>null</code>.
134      * @exception  IndexOutOfBoundsException If <code>off</code> is negative,
135      * <code>len</code> is negative, or <code>len</code> is greater than
136      * <code>b.length - off</code>
137      * @exception ZipException if a ZIP file error has occurred
138      * @exception IOException if an I/O error has occurred
139      */
140     public int read(byte[] b, int off, int len) throws IOException {
141         ensureOpen();
142         if (off < 0 || len < 0 || off > b.length - len) {
143 	    throw new IndexOutOfBoundsException();
144 	} else if (len == 0) {
145 	    return 0;
146 	}
147 
148 	if (entry == null) {
149 	    return -1;
150 	}
151 	switch (entry.getMethod()) {
152 	case DEFLATED:
153 	    len = super.read(b, off, len);
154 	    if (len == -1) {
155 		readEnd(entry);
156                 entryEOF = true;
157 		entry = null;
158 	    } else {
159 		crc.update(b, off, len);
160 	    }
161 	    return len;
162 	case STORED:
163 	    if (remaining <= 0) {
164                 entryEOF = true;
165 		entry = null;
166 		return -1;
167 	    }
168 	    if (len > remaining) {
169 		len = (int)remaining;
170 	    }
171 	    len = in.read(b, off, len);
172 	    if (len == -1) {
173 		throw new ZipException("unexpected EOF");
174 	    }
175 	    crc.update(b, off, len);
176 	    remaining -= len;
177 	    return len;
178 	default:
179 	    throw new ZipException("invalid compression method");
180 	}
181     }
182 
183     /**
184      * Skips specified number of bytes in the current ZIP entry.
185      * @param n the number of bytes to skip
186      * @return the actual number of bytes skipped
187      * @exception ZipException if a ZIP file error has occurred
188      * @exception IOException if an I/O error has occurred
189      * @exception IllegalArgumentException if n < 0
190      */
191     public long skip(long n) throws IOException {
192         if (n < 0) {
193             throw new IllegalArgumentException("negative skip length");
194         }
195         ensureOpen();
196 	int max = (int)Math.min(n, Integer.MAX_VALUE);
197 	int total = 0;
198 	while (total < max) {
199 	    int len = max - total;
200 	    if (len > tmpbuf.length) {
201 		len = tmpbuf.length;
202 	    }
203 	    len = read(tmpbuf, 0, len);
204 	    if (len == -1) {
205                 entryEOF = true;
206 		break;
207 	    }
208 	    total += len;
209 	}
210 	return total;
211     }
212 
213     /**
214      * Closes this input stream and releases any system resources associated
215      * with the stream.
216      * @exception IOException if an I/O error has occurred
217      */
218     public void close() throws IOException {
219         if (!closed) {
220 	    super.close();
221             closed = true;
222         }
223     }
224 
225     private byte[] b = new byte[256];
226 
227     /*
228      * Reads local file (LOC) header for next entry.
229      */
230     private ZipEntry readLOC() throws IOException {
231 	try {
232 	    readFully(tmpbuf, 0, LOCHDR);
233 	} catch (EOFException e) {
234 	    return null;
235 	}
236 	if (get32(tmpbuf, 0) != LOCSIG) {
237 	    return null;
238 	}
239 	// get the entry name and create the ZipEntry first
240 	int len = get16(tmpbuf, LOCNAM);
241         int blen = b.length;
242         if (len > blen) {
243             do
244                 blen = blen * 2;
245             while (len > blen);
246             b = new byte[blen];
247         }
248 	readFully(b, 0, len);
249 	ZipEntry e = createZipEntry(getUTF8String(b, 0, len));
250 	// now get the remaining fields for the entry
251 	flag = get16(tmpbuf, LOCFLG);
252 	if ((flag & 1) == 1) {
253 	    throw new ZipException("encrypted ZIP entry not supported");
254 	}
255 	e.setMethod(get16(tmpbuf, LOCHOW));
256 	e.setTime(get32(tmpbuf, LOCTIM));
257 	if ((flag & 8) == 8) {
258 	    /* "Data Descriptor" present */
259 	    if (e.getMethod() != DEFLATED) {
260 		throw new ZipException(
261 			"only DEFLATED entries can have EXT descriptor");
262 	    }
263 	} else {
264 	    e.setCrc(get32(tmpbuf, LOCCRC));
265 	    e.setCompressedSize(get32(tmpbuf, LOCSIZ));
266 	    e.setSize(get32(tmpbuf, LOCLEN));
267 	}
268 	len = get16(tmpbuf, LOCEXT);
269 	if (len > 0) {
270 	    byte[] bb = new byte[len];
271 	    readFully(bb, 0, len);
272 	    e.setExtra(bb);
273 	}
274 	return e;
275     }
276 
277     /*
278      * Fetches a UTF8-encoded String from the specified byte array.
279      */
280     private static String getUTF8String(byte[] b, int off, int len) {
281 	// First, count the number of characters in the sequence
282 	int count = 0;
283 	int max = off + len;
284 	int i = off;
285 	while (i < max) {
286 	    int c = b[i++] & 0xff;
287 	    switch (c >> 4) {
288 	    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
289 		// 0xxxxxxx
290 		count++;
291 		break;
292 	    case 12: case 13:
293 		// 110xxxxx 10xxxxxx
294 		if ((int)(b[i++] & 0xc0) != 0x80) {
295 		    throw new IllegalArgumentException();
296 		}
297 		count++;
298 		break;
299 	    case 14:
300 		// 1110xxxx 10xxxxxx 10xxxxxx
301 		if (((int)(b[i++] & 0xc0) != 0x80) ||
302 		    ((int)(b[i++] & 0xc0) != 0x80)) {
303 		    throw new IllegalArgumentException();
304 		}
305 		count++;
306 		break;
307 	    default:
308 		// 10xxxxxx, 1111xxxx
309 	    	count++; //Varienaja: throw new IllegalArgumentException();
310 	    }
311 	}
312 	if (i != max) {
313 	    throw new IllegalArgumentException();
314 	}
315 	// Now decode the characters...
316 	char[] cs = new char[count];
317 	i = 0;
318 	while (off < max) {
319 	    int c = b[off++] & 0xff;
320 	    switch (c >> 4) {
321 	    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
322 		// 0xxxxxxx
323 		cs[i++] = (char)c;
324 		break;
325 	    case 12: case 13:
326 		// 110xxxxx 10xxxxxx
327 		cs[i++] = (char)(((c & 0x1f) << 6) | (b[off++] & 0x3f));
328 		break;
329 	    case 14:
330 		// 1110xxxx 10xxxxxx 10xxxxxx
331 		int t = (b[off++] & 0x3f) << 6;
332 		cs[i++] = (char)(((c & 0x0f) << 12) | t | (b[off++] & 0x3f));
333 		break;
334 	    default:
335 		// 10xxxxxx, 1111xxxx
336 			cs[i++] = 'X'; // Varienaja: throw new IllegalArgumentException();
337 	    }
338 	}
339 	return new String(cs, 0, count);
340     }
341 
342     /**
343      * Creates a new <code>ZipEntry</code> object for the specified
344      * entry name.
345      *
346      * @param name the ZIP file entry name
347      * @return the ZipEntry just created
348      */
349     protected ZipEntry createZipEntry(String name) {
350 	return new ZipEntry(name);
351     }
352 
353     /*
354      * Reads end of deflated entry as well as EXT descriptor if present.
355      */
356     private void readEnd(ZipEntry e) throws IOException {
357 	int n = inf.getRemaining();
358 	if (n > 0) {
359 	    ((PushbackInputStream)in).unread(buf, len - n, n);
360 	}
361 	if ((flag & 8) == 8) {
362 	    /* "Data Descriptor" present */
363 	    readFully(tmpbuf, 0, EXTHDR);
364 	    long sig = get32(tmpbuf, 0);
365             if (sig != EXTSIG) { // no EXTSIG present
366                 e.setCrc(sig);
367                 e.setCompressedSize(get32(tmpbuf, EXTSIZ - EXTCRC));
368                 e.setSize(get32(tmpbuf, EXTLEN - EXTCRC));
369                 ((PushbackInputStream)in).unread(
370                                            tmpbuf, EXTHDR - EXTCRC - 1, EXTCRC);
371             } else {
372                 e.setCrc(get32(tmpbuf, EXTCRC));
373                 e.setCompressedSize(get32(tmpbuf, EXTSIZ));
374                 e.setSize(get32(tmpbuf, EXTLEN));
375             }
376 	}
377 	if (e.getSize() != inf.getBytesWritten()) {
378 	    throw new ZipException(
379 		"invalid entry size (expected " + e.getSize() +
380 		" but got " + inf.getBytesWritten() + " bytes)");
381 	}
382 	if (e.getCompressedSize() != inf.getBytesRead()) {
383 	    throw new ZipException(
384 		"invalid entry compressed size (expected " + e.getCompressedSize() +
385 		" but got " + inf.getBytesRead() + " bytes)");
386 	}
387 	if (e.getCrc() != crc.getValue()) {
388 	    throw new ZipException(
389 		"invalid entry CRC (expected 0x" + Long.toHexString(e.getCrc()) +
390 		" but got 0x" + Long.toHexString(crc.getValue()) + ")");
391 	}
392     }
393 
394     /*
395      * Reads bytes, blocking until all bytes are read.
396      */
397     private void readFully(byte[] b, int off, int len) throws IOException {
398 	while (len > 0) {
399 	    int n = in.read(b, off, len);
400 	    if (n == -1) {
401 		throw new EOFException();
402 	    }
403 	    off += n;
404 	    len -= n;
405 	}
406     }
407 
408     /*
409      * Fetches unsigned 16-bit value from byte array at specified offset.
410      * The bytes are assumed to be in Intel (little-endian) byte order.
411      */
412     private static final int get16(byte b[], int off) {
413 	return (b[off] & 0xff) | ((b[off+1] & 0xff) << 8);
414     }
415 
416     /*
417      * Fetches unsigned 32-bit value from byte array at specified offset.
418      * The bytes are assumed to be in Intel (little-endian) byte order.
419      */
420     private static final long get32(byte b[], int off) {
421 	return get16(b, off) | ((long)get16(b, off+2) << 16);
422     }
423 }