001 /* ZipFile.java --
002 Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006
003 Free Software Foundation, Inc.
004
005 This file is part of GNU Classpath.
006
007 GNU Classpath is free software; you can redistribute it and/or modify
008 it under the terms of the GNU General Public License as published by
009 the Free Software Foundation; either version 2, or (at your option)
010 any later version.
011
012 GNU Classpath is distributed in the hope that it will be useful, but
013 WITHOUT ANY WARRANTY; without even the implied warranty of
014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 General Public License for more details.
016
017 You should have received a copy of the GNU General Public License
018 along with GNU Classpath; see the file COPYING. If not, write to the
019 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
020 02110-1301 USA.
021
022 Linking this library statically or dynamically with other modules is
023 making a combined work based on this library. Thus, the terms and
024 conditions of the GNU General Public License cover the whole
025 combination.
026
027 As a special exception, the copyright holders of this library give you
028 permission to link this library with independent modules to produce an
029 executable, regardless of the license terms of these independent
030 modules, and to copy and distribute the resulting executable under
031 terms of your choice, provided that you also meet, for each linked
032 independent module, the terms and conditions of the license of that
033 module. An independent module is a module which is not derived from
034 or based on this library. If you modify this library, you may extend
035 this exception to your version of the library, but you are not
036 obligated to do so. If you do not wish to do so, delete this
037 exception statement from your version. */
038
039
040 package java.util.zip;
041
042 import gnu.java.util.EmptyEnumeration;
043
044 import java.io.EOFException;
045 import java.io.File;
046 import java.io.FileNotFoundException;
047 import java.io.IOException;
048 import java.io.InputStream;
049 import java.io.RandomAccessFile;
050 import java.io.UnsupportedEncodingException;
051 import java.nio.ByteBuffer;
052 import java.nio.charset.Charset;
053 import java.nio.charset.CharsetDecoder;
054 import java.util.Enumeration;
055 import java.util.Iterator;
056 import java.util.LinkedHashMap;
057
058 /**
059 * This class represents a Zip archive. You can ask for the contained
060 * entries, or get an input stream for a file entry. The entry is
061 * automatically decompressed.
062 *
063 * This class is thread safe: You can open input streams for arbitrary
064 * entries in different threads.
065 *
066 * @author Jochen Hoenicke
067 * @author Artur Biesiadowski
068 */
069 public class ZipFile implements ZipConstants
070 {
071
072 /**
073 * Mode flag to open a zip file for reading.
074 */
075 public static final int OPEN_READ = 0x1;
076
077 /**
078 * Mode flag to delete a zip file after reading.
079 */
080 public static final int OPEN_DELETE = 0x4;
081
082 /**
083 * This field isn't defined in the JDK's ZipConstants, but should be.
084 */
085 static final int ENDNRD = 4;
086
087 // Name of this zip file.
088 private final String name;
089
090 // File from which zip entries are read.
091 private final RandomAccessFile raf;
092
093 // The entries of this zip file when initialized and not yet closed.
094 private LinkedHashMap<String, ZipEntry> entries;
095
096 private boolean closed = false;
097
098
099 /**
100 * Helper function to open RandomAccessFile and throw the proper
101 * ZipException in case opening the file fails.
102 *
103 * @param name the file name, or null if file is provided
104 *
105 * @param file the file, or null if name is provided
106 *
107 * @return the newly open RandomAccessFile, never null
108 */
109 private RandomAccessFile openFile(String name,
110 File file)
111 throws ZipException, IOException
112 {
113 try
114 {
115 return
116 (name != null)
117 ? new RandomAccessFile(name, "r")
118 : new RandomAccessFile(file, "r");
119 }
120 catch (FileNotFoundException f)
121 {
122 ZipException ze = new ZipException(f.getMessage());
123 ze.initCause(f);
124 throw ze;
125 }
126 }
127
128
129 /**
130 * Opens a Zip file with the given name for reading.
131 * @exception IOException if a i/o error occured.
132 * @exception ZipException if the file doesn't contain a valid zip
133 * archive.
134 */
135 public ZipFile(String name) throws ZipException, IOException
136 {
137 this.raf = openFile(name,null);
138 this.name = name;
139 checkZipFile();
140 }
141
142 /**
143 * Opens a Zip file reading the given File.
144 * @exception IOException if a i/o error occured.
145 * @exception ZipException if the file doesn't contain a valid zip
146 * archive.
147 */
148 public ZipFile(File file) throws ZipException, IOException
149 {
150 this.raf = openFile(null,file);
151 this.name = file.getPath();
152 checkZipFile();
153 }
154
155 /**
156 * Opens a Zip file reading the given File in the given mode.
157 *
158 * If the OPEN_DELETE mode is specified, the zip file will be deleted at
159 * some time moment after it is opened. It will be deleted before the zip
160 * file is closed or the Virtual Machine exits.
161 *
162 * The contents of the zip file will be accessible until it is closed.
163 *
164 * @since JDK1.3
165 * @param mode Must be one of OPEN_READ or OPEN_READ | OPEN_DELETE
166 *
167 * @exception IOException if a i/o error occured.
168 * @exception ZipException if the file doesn't contain a valid zip
169 * archive.
170 */
171 public ZipFile(File file, int mode) throws ZipException, IOException
172 {
173 if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE))
174 throw new IllegalArgumentException("invalid mode");
175 if ((mode & OPEN_DELETE) != 0)
176 file.deleteOnExit();
177 this.raf = openFile(null,file);
178 this.name = file.getPath();
179 checkZipFile();
180 }
181
182 private void checkZipFile() throws ZipException
183 {
184 boolean valid = false;
185
186 try
187 {
188 byte[] buf = new byte[4];
189 raf.readFully(buf);
190 int sig = buf[0] & 0xFF
191 | ((buf[1] & 0xFF) << 8)
192 | ((buf[2] & 0xFF) << 16)
193 | ((buf[3] & 0xFF) << 24);
194 valid = sig == LOCSIG;
195 }
196 catch (IOException _)
197 {
198 }
199
200 if (!valid)
201 {
202 try
203 {
204 raf.close();
205 }
206 catch (IOException _)
207 {
208 }
209 throw new ZipException("Not a valid zip file");
210 }
211 }
212
213 /**
214 * Checks if file is closed and throws an exception.
215 */
216 private void checkClosed()
217 {
218 if (closed)
219 throw new IllegalStateException("ZipFile has closed: " + name);
220 }
221
222 /**
223 * Read the central directory of a zip file and fill the entries
224 * array. This is called exactly once when first needed. It is called
225 * while holding the lock on <code>raf</code>.
226 *
227 * @exception IOException if a i/o error occured.
228 * @exception ZipException if the central directory is malformed
229 */
230 private void readEntries() throws ZipException, IOException
231 {
232 /* Search for the End Of Central Directory. When a zip comment is
233 * present the directory may start earlier.
234 * Note that a comment has a maximum length of 64K, so that is the
235 * maximum we search backwards.
236 */
237 PartialInputStream inp = new PartialInputStream(raf, 4096);
238 long pos = raf.length() - ENDHDR;
239 long top = Math.max(0, pos - 65536);
240 do
241 {
242 if (pos < top)
243 throw new ZipException
244 ("central directory not found, probably not a zip file: " + name);
245 inp.seek(pos--);
246 }
247 while (inp.readLeInt() != ENDSIG);
248
249 if (inp.skip(ENDTOT - ENDNRD) != ENDTOT - ENDNRD)
250 throw new EOFException(name);
251 int count = inp.readLeShort();
252 if (inp.skip(ENDOFF - ENDSIZ) != ENDOFF - ENDSIZ)
253 throw new EOFException(name);
254 int centralOffset = inp.readLeInt();
255
256 entries = new LinkedHashMap<String, ZipEntry> (count+count/2);
257 inp.seek(centralOffset);
258
259 for (int i = 0; i < count; i++)
260 {
261 if (inp.readLeInt() != CENSIG)
262 throw new ZipException("Wrong Central Directory signature: " + name);
263
264 inp.skip(6);
265 int method = inp.readLeShort();
266 int dostime = inp.readLeInt();
267 int crc = inp.readLeInt();
268 int csize = inp.readLeInt();
269 int size = inp.readLeInt();
270 int nameLen = inp.readLeShort();
271 int extraLen = inp.readLeShort();
272 int commentLen = inp.readLeShort();
273 inp.skip(8);
274 int offset = inp.readLeInt();
275 String name = inp.readString(nameLen);
276
277 ZipEntry entry = new ZipEntry(name);
278 entry.setMethod(method);
279 entry.setCrc(crc & 0xffffffffL);
280 entry.setSize(size & 0xffffffffL);
281 entry.setCompressedSize(csize & 0xffffffffL);
282 entry.setDOSTime(dostime);
283 if (extraLen > 0)
284 {
285 byte[] extra = new byte[extraLen];
286 inp.readFully(extra);
287 entry.setExtra(extra);
288 }
289 if (commentLen > 0)
290 {
291 entry.setComment(inp.readString(commentLen));
292 }
293 entry.offset = offset;
294 entries.put(name, entry);
295 }
296 }
297
298 /**
299 * Closes the ZipFile. This also closes all input streams given by
300 * this class. After this is called, no further method should be
301 * called.
302 *
303 * @exception IOException if a i/o error occured.
304 */
305 public void close() throws IOException
306 {
307 RandomAccessFile raf = this.raf;
308 if (raf == null)
309 return;
310
311 synchronized (raf)
312 {
313 closed = true;
314 entries = null;
315 raf.close();
316 }
317 }
318
319 /**
320 * Calls the <code>close()</code> method when this ZipFile has not yet
321 * been explicitly closed.
322 */
323 protected void finalize() throws IOException
324 {
325 if (!closed && raf != null) close();
326 }
327
328 /**
329 * Returns an enumeration of all Zip entries in this Zip file.
330 *
331 * @exception IllegalStateException when the ZipFile has already been closed
332 */
333 public Enumeration<? extends ZipEntry> entries()
334 {
335 checkClosed();
336
337 try
338 {
339 return new ZipEntryEnumeration(getEntries().values().iterator());
340 }
341 catch (IOException ioe)
342 {
343 return new EmptyEnumeration<ZipEntry>();
344 }
345 }
346
347 /**
348 * Checks that the ZipFile is still open and reads entries when necessary.
349 *
350 * @exception IllegalStateException when the ZipFile has already been closed.
351 * @exception IOException when the entries could not be read.
352 */
353 private LinkedHashMap<String, ZipEntry> getEntries() throws IOException
354 {
355 synchronized(raf)
356 {
357 checkClosed();
358
359 if (entries == null)
360 readEntries();
361
362 return entries;
363 }
364 }
365
366 /**
367 * Searches for a zip entry in this archive with the given name.
368 *
369 * @param name the name. May contain directory components separated by
370 * slashes ('/').
371 * @return the zip entry, or null if no entry with that name exists.
372 *
373 * @exception IllegalStateException when the ZipFile has already been closed
374 */
375 public ZipEntry getEntry(String name)
376 {
377 checkClosed();
378
379 try
380 {
381 LinkedHashMap<String, ZipEntry> entries = getEntries();
382 ZipEntry entry = entries.get(name);
383 // If we didn't find it, maybe it's a directory.
384 if (entry == null && !name.endsWith("/"))
385 entry = entries.get(name + '/');
386 return entry != null ? new ZipEntry(entry, name) : null;
387 }
388 catch (IOException ioe)
389 {
390 return null;
391 }
392 }
393
394 /**
395 * Creates an input stream reading the given zip entry as
396 * uncompressed data. Normally zip entry should be an entry
397 * returned by getEntry() or entries().
398 *
399 * This implementation returns null if the requested entry does not
400 * exist. This decision is not obviously correct, however, it does
401 * appear to mirror Sun's implementation, and it is consistant with
402 * their javadoc. On the other hand, the old JCL book, 2nd Edition,
403 * claims that this should return a "non-null ZIP entry". We have
404 * chosen for now ignore the old book, as modern versions of Ant (an
405 * important application) depend on this behaviour. See discussion
406 * in this thread:
407 * http://gcc.gnu.org/ml/java-patches/2004-q2/msg00602.html
408 *
409 * @param entry the entry to create an InputStream for.
410 * @return the input stream, or null if the requested entry does not exist.
411 *
412 * @exception IllegalStateException when the ZipFile has already been closed
413 * @exception IOException if a i/o error occured.
414 * @exception ZipException if the Zip archive is malformed.
415 */
416 public InputStream getInputStream(ZipEntry entry) throws IOException
417 {
418 checkClosed();
419
420 LinkedHashMap<String, ZipEntry> entries = getEntries();
421 String name = entry.getName();
422 ZipEntry zipEntry = entries.get(name);
423 if (zipEntry == null)
424 return null;
425
426 PartialInputStream inp = new PartialInputStream(raf, 1024);
427 inp.seek(zipEntry.offset);
428
429 if (inp.readLeInt() != LOCSIG)
430 throw new ZipException("Wrong Local header signature: " + name);
431
432 inp.skip(4);
433
434 if (zipEntry.getMethod() != inp.readLeShort())
435 throw new ZipException("Compression method mismatch: " + name);
436
437 inp.skip(16);
438
439 int nameLen = inp.readLeShort();
440 int extraLen = inp.readLeShort();
441 inp.skip(nameLen + extraLen);
442
443 inp.setLength(zipEntry.getCompressedSize());
444
445 int method = zipEntry.getMethod();
446 switch (method)
447 {
448 case ZipOutputStream.STORED:
449 return inp;
450 case ZipOutputStream.DEFLATED:
451 inp.addDummyByte();
452 final Inflater inf = new Inflater(true);
453 final int sz = (int) entry.getSize();
454 return new InflaterInputStream(inp, inf)
455 {
456 public int available() throws IOException
457 {
458 if (sz == -1)
459 return super.available();
460 if (super.available() != 0)
461 return sz - inf.getTotalOut();
462 return 0;
463 }
464 };
465 default:
466 throw new ZipException("Unknown compression method " + method);
467 }
468 }
469
470 /**
471 * Returns the (path) name of this zip file.
472 */
473 public String getName()
474 {
475 return name;
476 }
477
478 /**
479 * Returns the number of entries in this zip file.
480 *
481 * @exception IllegalStateException when the ZipFile has already been closed
482 */
483 public int size()
484 {
485 checkClosed();
486
487 try
488 {
489 return getEntries().size();
490 }
491 catch (IOException ioe)
492 {
493 return 0;
494 }
495 }
496
497 private static class ZipEntryEnumeration implements Enumeration<ZipEntry>
498 {
499 private final Iterator<ZipEntry> elements;
500
501 public ZipEntryEnumeration(Iterator<ZipEntry> elements)
502 {
503 this.elements = elements;
504 }
505
506 public boolean hasMoreElements()
507 {
508 return elements.hasNext();
509 }
510
511 public ZipEntry nextElement()
512 {
513 /* We return a clone, just to be safe that the user doesn't
514 * change the entry.
515 */
516 return (ZipEntry) (elements.next().clone());
517 }
518 }
519
520 private static final class PartialInputStream extends InputStream
521 {
522 /**
523 * The UTF-8 charset use for decoding the filenames.
524 */
525 private static final Charset UTF8CHARSET = Charset.forName("UTF-8");
526
527 /**
528 * The actual UTF-8 decoder. Created on demand.
529 */
530 private CharsetDecoder utf8Decoder;
531
532 private final RandomAccessFile raf;
533 private final byte[] buffer;
534 private long bufferOffset;
535 private int pos;
536 private long end;
537 // We may need to supply an extra dummy byte to our reader.
538 // See Inflater. We use a count here to simplify the logic
539 // elsewhere in this class. Note that we ignore the dummy
540 // byte in methods where we know it is not needed.
541 private int dummyByteCount;
542
543 public PartialInputStream(RandomAccessFile raf, int bufferSize)
544 throws IOException
545 {
546 this.raf = raf;
547 buffer = new byte[bufferSize];
548 bufferOffset = -buffer.length;
549 pos = buffer.length;
550 end = raf.length();
551 }
552
553 void setLength(long length)
554 {
555 end = bufferOffset + pos + length;
556 }
557
558 private void fillBuffer() throws IOException
559 {
560 synchronized (raf)
561 {
562 long len = end - bufferOffset;
563 if (len == 0 && dummyByteCount > 0)
564 {
565 buffer[0] = 0;
566 dummyByteCount = 0;
567 }
568 else
569 {
570 raf.seek(bufferOffset);
571 raf.readFully(buffer, 0, (int) Math.min(buffer.length, len));
572 }
573 }
574 }
575
576 public int available()
577 {
578 long amount = end - (bufferOffset + pos);
579 if (amount > Integer.MAX_VALUE)
580 return Integer.MAX_VALUE;
581 return (int) amount;
582 }
583
584 public int read() throws IOException
585 {
586 if (bufferOffset + pos >= end + dummyByteCount)
587 return -1;
588 if (pos == buffer.length)
589 {
590 bufferOffset += buffer.length;
591 pos = 0;
592 fillBuffer();
593 }
594
595 return buffer[pos++] & 0xFF;
596 }
597
598 public int read(byte[] b, int off, int len) throws IOException
599 {
600 if (len > end + dummyByteCount - (bufferOffset + pos))
601 {
602 len = (int) (end + dummyByteCount - (bufferOffset + pos));
603 if (len == 0)
604 return -1;
605 }
606
607 int totalBytesRead = Math.min(buffer.length - pos, len);
608 System.arraycopy(buffer, pos, b, off, totalBytesRead);
609 pos += totalBytesRead;
610 off += totalBytesRead;
611 len -= totalBytesRead;
612
613 while (len > 0)
614 {
615 bufferOffset += buffer.length;
616 pos = 0;
617 fillBuffer();
618 int remain = Math.min(buffer.length, len);
619 System.arraycopy(buffer, pos, b, off, remain);
620 pos += remain;
621 off += remain;
622 len -= remain;
623 totalBytesRead += remain;
624 }
625
626 return totalBytesRead;
627 }
628
629 public long skip(long amount) throws IOException
630 {
631 if (amount < 0)
632 return 0;
633 if (amount > end - (bufferOffset + pos))
634 amount = end - (bufferOffset + pos);
635 seek(bufferOffset + pos + amount);
636 return amount;
637 }
638
639 void seek(long newpos) throws IOException
640 {
641 long offset = newpos - bufferOffset;
642 if (offset >= 0 && offset <= buffer.length)
643 {
644 pos = (int) offset;
645 }
646 else
647 {
648 bufferOffset = newpos;
649 pos = 0;
650 fillBuffer();
651 }
652 }
653
654 void readFully(byte[] buf) throws IOException
655 {
656 if (read(buf, 0, buf.length) != buf.length)
657 throw new EOFException();
658 }
659
660 void readFully(byte[] buf, int off, int len) throws IOException
661 {
662 if (read(buf, off, len) != len)
663 throw new EOFException();
664 }
665
666 int readLeShort() throws IOException
667 {
668 int result;
669 if(pos + 1 < buffer.length)
670 {
671 result = ((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8);
672 pos += 2;
673 }
674 else
675 {
676 int b0 = read();
677 int b1 = read();
678 if (b1 == -1)
679 throw new EOFException();
680 result = (b0 & 0xff) | (b1 & 0xff) << 8;
681 }
682 return result;
683 }
684
685 int readLeInt() throws IOException
686 {
687 int result;
688 if(pos + 3 < buffer.length)
689 {
690 result = (((buffer[pos + 0] & 0xff) | (buffer[pos + 1] & 0xff) << 8)
691 | ((buffer[pos + 2] & 0xff)
692 | (buffer[pos + 3] & 0xff) << 8) << 16);
693 pos += 4;
694 }
695 else
696 {
697 int b0 = read();
698 int b1 = read();
699 int b2 = read();
700 int b3 = read();
701 if (b3 == -1)
702 throw new EOFException();
703 result = (((b0 & 0xff) | (b1 & 0xff) << 8) | ((b2 & 0xff)
704 | (b3 & 0xff) << 8) << 16);
705 }
706 return result;
707 }
708
709 /**
710 * Decode chars from byte buffer using UTF8 encoding. This
711 * operation is performance-critical since a jar file contains a
712 * large number of strings for the name of each file in the
713 * archive. This routine therefore avoids using the expensive
714 * utf8Decoder when decoding is straightforward.
715 *
716 * @param buffer the buffer that contains the encoded character
717 * data
718 * @param pos the index in buffer of the first byte of the encoded
719 * data
720 * @param length the length of the encoded data in number of
721 * bytes.
722 *
723 * @return a String that contains the decoded characters.
724 */
725 private String decodeChars(byte[] buffer, int pos, int length)
726 throws IOException
727 {
728 String result;
729 int i=length - 1;
730 while ((i >= 0) && (buffer[i] <= 0x7f))
731 {
732 i--;
733 }
734 if (i < 0)
735 {
736 result = new String(buffer, 0, pos, length);
737 }
738 else
739 {
740 ByteBuffer bufferBuffer = ByteBuffer.wrap(buffer, pos, length);
741 if (utf8Decoder == null)
742 utf8Decoder = UTF8CHARSET.newDecoder();
743 utf8Decoder.reset();
744 char [] characters = utf8Decoder.decode(bufferBuffer).array();
745 result = String.valueOf(characters);
746 }
747 return result;
748 }
749
750 String readString(int length) throws IOException
751 {
752 if (length > end - (bufferOffset + pos))
753 throw new EOFException();
754
755 String result = null;
756 try
757 {
758 if (buffer.length - pos >= length)
759 {
760 result = decodeChars(buffer, pos, length);
761 pos += length;
762 }
763 else
764 {
765 byte[] b = new byte[length];
766 readFully(b);
767 result = decodeChars(b, 0, length);
768 }
769 }
770 catch (UnsupportedEncodingException uee)
771 {
772 throw new AssertionError(uee);
773 }
774 return result;
775 }
776
777 public void addDummyByte()
778 {
779 dummyByteCount = 1;
780 }
781 }
782 }