ZipFile.java revision a8ed084745590c5e4a0e8559b5821809d60fe242
1/*
2 * Copyright (c) 1995, 2011, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package java.util.zip;
27
28import java.io.Closeable;
29import java.io.InputStream;
30import java.io.IOException;
31import java.io.EOFException;
32import java.io.File;
33import java.nio.charset.Charset;
34import java.nio.charset.StandardCharsets;
35import java.util.ArrayDeque;
36import java.util.Deque;
37import java.util.Enumeration;
38import java.util.HashMap;
39import java.util.Map;
40import java.util.NoSuchElementException;
41import java.util.WeakHashMap;
42import java.security.AccessController;
43import sun.security.action.GetPropertyAction;
44import static java.util.zip.ZipConstants64.*;
45
46/**
47 * This class is used to read entries from a zip file.
48 *
49 * <p> Unless otherwise noted, passing a <tt>null</tt> argument to a constructor
50 * or method in this class will cause a {@link NullPointerException} to be
51 * thrown.
52 *
53 * @author      David Connelly
54 */
55public
56class ZipFile implements ZipConstants, Closeable {
57    private long jzfile;           // address of jzfile data
58    private final String name;     // zip file name
59    private final int total;       // total number of entries
60    private final boolean locsig;  // if zip file starts with LOCSIG (usually true)
61    private volatile boolean closeRequested = false;
62
63    private static final int STORED = ZipEntry.STORED;
64    private static final int DEFLATED = ZipEntry.DEFLATED;
65
66    /**
67     * Mode flag to open a zip file for reading.
68     */
69    public static final int OPEN_READ = 0x1;
70
71    /**
72     * Mode flag to open a zip file and mark it for deletion.  The file will be
73     * deleted some time between the moment that it is opened and the moment
74     * that it is closed, but its contents will remain accessible via the
75     * <tt>ZipFile</tt> object until either the close method is invoked or the
76     * virtual machine exits.
77     */
78    public static final int OPEN_DELETE = 0x4;
79
80    static {
81        /* Zip library is loaded from System.initializeSystemClass */
82        initIDs();
83    }
84
85    private static native void initIDs();
86
87    private static final boolean usemmap;
88
89    static {
90        // Android-changed: always use mmap.
91        usemmap = true;
92    }
93
94    /**
95     * Opens a zip file for reading.
96     *
97     * <p>First, if there is a security manager, its <code>checkRead</code>
98     * method is called with the <code>name</code> argument as its argument
99     * to ensure the read is allowed.
100     *
101     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
102     * decode the entry names and comments.
103     *
104     * @param name the name of the zip file
105     * @throws ZipException if a ZIP format error has occurred
106     * @throws IOException if an I/O error has occurred
107     * @throws SecurityException if a security manager exists and its
108     *         <code>checkRead</code> method doesn't allow read access to the file.
109     *
110     * @see SecurityManager#checkRead(java.lang.String)
111     */
112    public ZipFile(String name) throws IOException {
113        this(new File(name), OPEN_READ);
114    }
115
116    /**
117     * Opens a new <code>ZipFile</code> to read from the specified
118     * <code>File</code> object in the specified mode.  The mode argument
119     * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>.
120     *
121     * <p>First, if there is a security manager, its <code>checkRead</code>
122     * method is called with the <code>name</code> argument as its argument to
123     * ensure the read is allowed.
124     *
125     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
126     * decode the entry names and comments
127     *
128     * @param file the ZIP file to be opened for reading
129     * @param mode the mode in which the file is to be opened
130     * @throws ZipException if a ZIP format error has occurred
131     * @throws IOException if an I/O error has occurred
132     * @throws SecurityException if a security manager exists and
133     *         its <code>checkRead</code> method
134     *         doesn't allow read access to the file,
135     *         or its <code>checkDelete</code> method doesn't allow deleting
136     *         the file when the <tt>OPEN_DELETE</tt> flag is set.
137     * @throws IllegalArgumentException if the <tt>mode</tt> argument is invalid
138     * @see SecurityManager#checkRead(java.lang.String)
139     * @since 1.3
140     */
141    public ZipFile(File file, int mode) throws IOException {
142        this(file, mode, StandardCharsets.UTF_8);
143    }
144
145    /**
146     * Opens a ZIP file for reading given the specified File object.
147     *
148     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used to
149     * decode the entry names and comments.
150     *
151     * @param file the ZIP file to be opened for reading
152     * @throws ZipException if a ZIP format error has occurred
153     * @throws IOException if an I/O error has occurred
154     */
155    public ZipFile(File file) throws ZipException, IOException {
156        this(file, OPEN_READ);
157    }
158
159    private ZipCoder zc;
160
161    /**
162     * Opens a new <code>ZipFile</code> to read from the specified
163     * <code>File</code> object in the specified mode.  The mode argument
164     * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>.
165     *
166     * <p>First, if there is a security manager, its <code>checkRead</code>
167     * method is called with the <code>name</code> argument as its argument to
168     * ensure the read is allowed.
169     *
170     * @param file the ZIP file to be opened for reading
171     * @param mode the mode in which the file is to be opened
172     * @param charset
173     *        the {@linkplain java.nio.charset.Charset charset} to
174     *        be used to decode the ZIP entry name and comment that are not
175     *        encoded by using UTF-8 encoding (indicated by entry's general
176     *        purpose flag).
177     *
178     * @throws ZipException if a ZIP format error has occurred
179     * @throws IOException if an I/O error has occurred
180     *
181     * @throws SecurityException
182     *         if a security manager exists and its <code>checkRead</code>
183     *         method doesn't allow read access to the file,or its
184     *         <code>checkDelete</code> method doesn't allow deleting the
185     *         file when the <tt>OPEN_DELETE</tt> flag is set
186     *
187     * @throws IllegalArgumentException if the <tt>mode</tt> argument is invalid
188     *
189     * @see SecurityManager#checkRead(java.lang.String)
190     *
191     * @since 1.7
192     */
193    public ZipFile(File file, int mode, Charset charset) throws IOException
194    {
195        if (((mode & OPEN_READ) == 0) ||
196            ((mode & ~(OPEN_READ | OPEN_DELETE)) != 0)) {
197            throw new IllegalArgumentException("Illegal mode: 0x"+
198                                               Integer.toHexString(mode));
199        }
200        String name = file.getPath();
201        SecurityManager sm = System.getSecurityManager();
202        if (sm != null) {
203            sm.checkRead(name);
204            if ((mode & OPEN_DELETE) != 0) {
205                sm.checkDelete(name);
206            }
207        }
208        if (charset == null)
209            throw new NullPointerException("charset is null");
210        this.zc = ZipCoder.get(charset);
211        long t0 = System.nanoTime();
212        jzfile = open(name, mode, file.lastModified(), usemmap);
213        this.name = name;
214        this.total = getTotal(jzfile);
215        this.locsig = startsWithLOC(jzfile);
216    }
217
218    /**
219     * Opens a zip file for reading.
220     *
221     * <p>First, if there is a security manager, its <code>checkRead</code>
222     * method is called with the <code>name</code> argument as its argument
223     * to ensure the read is allowed.
224     *
225     * @param name the name of the zip file
226     * @param charset
227     *        the {@linkplain java.nio.charset.Charset charset} to
228     *        be used to decode the ZIP entry name and comment that are not
229     *        encoded by using UTF-8 encoding (indicated by entry's general
230     *        purpose flag).
231     *
232     * @throws ZipException if a ZIP format error has occurred
233     * @throws IOException if an I/O error has occurred
234     * @throws SecurityException
235     *         if a security manager exists and its <code>checkRead</code>
236     *         method doesn't allow read access to the file
237     *
238     * @see SecurityManager#checkRead(java.lang.String)
239     *
240     * @since 1.7
241     */
242    public ZipFile(String name, Charset charset) throws IOException
243    {
244        this(new File(name), OPEN_READ, charset);
245    }
246
247    /**
248     * Opens a ZIP file for reading given the specified File object.
249     * @param file the ZIP file to be opened for reading
250     * @param charset
251     *        The {@linkplain java.nio.charset.Charset charset} to be
252     *        used to decode the ZIP entry name and comment (ignored if
253     *        the <a href="package-summary.html#lang_encoding"> language
254     *        encoding bit</a> of the ZIP entry's general purpose bit
255     *        flag is set).
256     *
257     * @throws ZipException if a ZIP format error has occurred
258     * @throws IOException if an I/O error has occurred
259     *
260     * @since 1.7
261     */
262    public ZipFile(File file, Charset charset) throws IOException
263    {
264        this(file, OPEN_READ, charset);
265    }
266
267    /**
268     * Returns the zip file comment, or null if none.
269     *
270     * @return the comment string for the zip file, or null if none
271     *
272     * @throws IllegalStateException if the zip file has been closed
273     *
274     * Since 1.7
275     */
276    public String getComment() {
277        synchronized (this) {
278            ensureOpen();
279            byte[] bcomm = getCommentBytes(jzfile);
280            if (bcomm == null)
281                return null;
282            return zc.toString(bcomm, bcomm.length);
283        }
284    }
285
286    /**
287     * Returns the zip file entry for the specified name, or null
288     * if not found.
289     *
290     * @param name the name of the entry
291     * @return the zip file entry, or null if not found
292     * @throws IllegalStateException if the zip file has been closed
293     */
294    public ZipEntry getEntry(String name) {
295        if (name == null) {
296            throw new NullPointerException("name");
297        }
298        long jzentry = 0;
299        synchronized (this) {
300            ensureOpen();
301            jzentry = getEntry(jzfile, zc.getBytes(name), true);
302            if (jzentry != 0) {
303                ZipEntry ze = getZipEntry(name, jzentry);
304                freeEntry(jzfile, jzentry);
305                return ze;
306            }
307        }
308        return null;
309    }
310
311    private static native long getEntry(long jzfile, byte[] name,
312                                        boolean addSlash);
313
314    // freeEntry releases the C jzentry struct.
315    private static native void freeEntry(long jzfile, long jzentry);
316
317    // the outstanding inputstreams that need to be closed,
318    // mapped to the inflater objects they use.
319    private final Map<InputStream, Inflater> streams = new WeakHashMap<>();
320
321    /**
322     * Returns an input stream for reading the contents of the specified
323     * zip file entry.
324     *
325     * <p> Closing this ZIP file will, in turn, close all input
326     * streams that have been returned by invocations of this method.
327     *
328     * @param entry the zip file entry
329     * @return the input stream for reading the contents of the specified
330     * zip file entry.
331     * @throws ZipException if a ZIP format error has occurred
332     * @throws IOException if an I/O error has occurred
333     * @throws IllegalStateException if the zip file has been closed
334     */
335    public InputStream getInputStream(ZipEntry entry) throws IOException {
336        if (entry == null) {
337            throw new NullPointerException("entry");
338        }
339        long jzentry = 0;
340        ZipFileInputStream in = null;
341        synchronized (this) {
342            ensureOpen();
343            if (!zc.isUTF8() && (entry.flag & EFS) != 0) {
344                jzentry = getEntry(jzfile, zc.getBytesUTF8(entry.name), false);
345            } else {
346                jzentry = getEntry(jzfile, zc.getBytes(entry.name), false);
347            }
348            if (jzentry == 0) {
349                return null;
350            }
351            in = new ZipFileInputStream(jzentry);
352
353            switch (getEntryMethod(jzentry)) {
354            case STORED:
355                synchronized (streams) {
356                    streams.put(in, null);
357                }
358                return in;
359            case DEFLATED:
360                // MORE: Compute good size for inflater stream:
361                long size = getEntrySize(jzentry) + 2; // Inflater likes a bit of slack
362                if (size > 65536) size = 8192;
363                if (size <= 0) size = 4096;
364                Inflater inf = getInflater();
365                InputStream is =
366                    new ZipFileInflaterInputStream(in, inf, (int)size);
367                synchronized (streams) {
368                    streams.put(is, inf);
369                }
370                return is;
371            default:
372                throw new ZipException("invalid compression method");
373            }
374        }
375    }
376
377    private class ZipFileInflaterInputStream extends InflaterInputStream {
378        private volatile boolean closeRequested = false;
379        private boolean eof = false;
380        private final ZipFileInputStream zfin;
381
382        ZipFileInflaterInputStream(ZipFileInputStream zfin, Inflater inf,
383                int size) {
384            super(zfin, inf, size);
385            this.zfin = zfin;
386        }
387
388        public void close() throws IOException {
389            if (closeRequested)
390                return;
391            closeRequested = true;
392
393            super.close();
394            Inflater inf;
395            synchronized (streams) {
396                inf = streams.remove(this);
397            }
398            if (inf != null) {
399                releaseInflater(inf);
400            }
401        }
402
403        // Override fill() method to provide an extra "dummy" byte
404        // at the end of the input stream. This is required when
405        // using the "nowrap" Inflater option.
406        protected void fill() throws IOException {
407            if (eof) {
408                throw new EOFException("Unexpected end of ZLIB input stream");
409            }
410            len = in.read(buf, 0, buf.length);
411            if (len == -1) {
412                buf[0] = 0;
413                len = 1;
414                eof = true;
415            }
416            inf.setInput(buf, 0, len);
417        }
418
419        public int available() throws IOException {
420            if (closeRequested)
421                return 0;
422            long avail = zfin.size() - inf.getBytesWritten();
423            return (avail > (long) Integer.MAX_VALUE ?
424                    Integer.MAX_VALUE : (int) avail);
425        }
426
427        protected void finalize() throws Throwable {
428            close();
429        }
430    }
431
432    /*
433     * Gets an inflater from the list of available inflaters or allocates
434     * a new one.
435     */
436    private Inflater getInflater() {
437        Inflater inf;
438        synchronized (inflaterCache) {
439            while (null != (inf = inflaterCache.poll())) {
440                if (false == inf.ended()) {
441                    return inf;
442                }
443            }
444        }
445        return new Inflater(true);
446    }
447
448    /*
449     * Releases the specified inflater to the list of available inflaters.
450     */
451    private void releaseInflater(Inflater inf) {
452        if (false == inf.ended()) {
453            inf.reset();
454            synchronized (inflaterCache) {
455                inflaterCache.add(inf);
456            }
457        }
458    }
459
460    // List of available Inflater objects for decompression
461    private Deque<Inflater> inflaterCache = new ArrayDeque<>();
462
463    /**
464     * Returns the path name of the ZIP file.
465     * @return the path name of the ZIP file
466     */
467    public String getName() {
468        return name;
469    }
470
471    /**
472     * Returns an enumeration of the ZIP file entries.
473     * @return an enumeration of the ZIP file entries
474     * @throws IllegalStateException if the zip file has been closed
475     */
476    public Enumeration<? extends ZipEntry> entries() {
477        ensureOpen();
478        return new Enumeration<ZipEntry>() {
479                private int i = 0;
480                public boolean hasMoreElements() {
481                    synchronized (ZipFile.this) {
482                        ensureOpen();
483                        return i < total;
484                    }
485                }
486                public ZipEntry nextElement() throws NoSuchElementException {
487                    synchronized (ZipFile.this) {
488                        ensureOpen();
489                        if (i >= total) {
490                            throw new NoSuchElementException();
491                        }
492                        long jzentry = getNextEntry(jzfile, i++);
493                        if (jzentry == 0) {
494                            String message;
495                            if (closeRequested) {
496                                message = "ZipFile concurrently closed";
497                            } else {
498                                message = getZipMessage(ZipFile.this.jzfile);
499                            }
500                            throw new ZipError("jzentry == 0" +
501                                               ",\n jzfile = " + ZipFile.this.jzfile +
502                                               ",\n total = " + ZipFile.this.total +
503                                               ",\n name = " + ZipFile.this.name +
504                                               ",\n i = " + i +
505                                               ",\n message = " + message
506                                );
507                        }
508                        ZipEntry ze = getZipEntry(null, jzentry);
509                        freeEntry(jzfile, jzentry);
510                        return ze;
511                    }
512                }
513            };
514    }
515
516    private ZipEntry getZipEntry(String name, long jzentry) {
517        ZipEntry e = new ZipEntry();
518        e.flag = getEntryFlag(jzentry);  // get the flag first
519        if (name != null) {
520            e.name = name;
521        } else {
522            byte[] bname = getEntryBytes(jzentry, JZENTRY_NAME);
523            if (!zc.isUTF8() && (e.flag & EFS) != 0) {
524                e.name = zc.toStringUTF8(bname, bname.length);
525            } else {
526                e.name = zc.toString(bname, bname.length);
527            }
528        }
529        e.time = getEntryTime(jzentry);
530        e.crc = getEntryCrc(jzentry);
531        e.size = getEntrySize(jzentry);
532        e. csize = getEntryCSize(jzentry);
533        e.method = getEntryMethod(jzentry);
534        e.extra = getEntryBytes(jzentry, JZENTRY_EXTRA);
535        byte[] bcomm = getEntryBytes(jzentry, JZENTRY_COMMENT);
536        if (bcomm == null) {
537            e.comment = null;
538        } else {
539            if (!zc.isUTF8() && (e.flag & EFS) != 0) {
540                e.comment = zc.toStringUTF8(bcomm, bcomm.length);
541            } else {
542                e.comment = zc.toString(bcomm, bcomm.length);
543            }
544        }
545        return e;
546    }
547
548    private static native long getNextEntry(long jzfile, int i);
549
550    /**
551     * Returns the number of entries in the ZIP file.
552     * @return the number of entries in the ZIP file
553     * @throws IllegalStateException if the zip file has been closed
554     */
555    public int size() {
556        ensureOpen();
557        return total;
558    }
559
560    /**
561     * Closes the ZIP file.
562     * <p> Closing this ZIP file will close all of the input streams
563     * previously returned by invocations of the {@link #getInputStream
564     * getInputStream} method.
565     *
566     * @throws IOException if an I/O error has occurred
567     */
568    public void close() throws IOException {
569        if (closeRequested)
570            return;
571        closeRequested = true;
572
573        synchronized (this) {
574            // Close streams, release their inflaters
575            synchronized (streams) {
576                if (false == streams.isEmpty()) {
577                    Map<InputStream, Inflater> copy = new HashMap<>(streams);
578                    streams.clear();
579                    for (Map.Entry<InputStream, Inflater> e : copy.entrySet()) {
580                        e.getKey().close();
581                        Inflater inf = e.getValue();
582                        if (inf != null) {
583                            inf.end();
584                        }
585                    }
586                }
587            }
588
589            // Release cached inflaters
590            Inflater inf;
591            synchronized (inflaterCache) {
592                while (null != (inf = inflaterCache.poll())) {
593                    inf.end();
594                }
595            }
596
597            if (jzfile != 0) {
598                // Close the zip file
599                long zf = this.jzfile;
600                jzfile = 0;
601
602                close(zf);
603            }
604        }
605    }
606
607    /**
608     * Ensures that the system resources held by this ZipFile object are
609     * released when there are no more references to it.
610     *
611     * <p>
612     * Since the time when GC would invoke this method is undetermined,
613     * it is strongly recommended that applications invoke the <code>close</code>
614     * method as soon they have finished accessing this <code>ZipFile</code>.
615     * This will prevent holding up system resources for an undetermined
616     * length of time.
617     *
618     * @throws IOException if an I/O error has occurred
619     * @see    java.util.zip.ZipFile#close()
620     */
621    protected void finalize() throws IOException {
622        close();
623    }
624
625    private static native void close(long jzfile);
626
627    private void ensureOpen() {
628        if (closeRequested) {
629            throw new IllegalStateException("zip file closed");
630        }
631
632        if (jzfile == 0) {
633            throw new IllegalStateException("The object is not initialized.");
634        }
635    }
636
637    private void ensureOpenOrZipException() throws IOException {
638        if (closeRequested) {
639            throw new ZipException("ZipFile closed");
640        }
641    }
642
643    /*
644     * Inner class implementing the input stream used to read a
645     * (possibly compressed) zip file entry.
646     */
647   private class ZipFileInputStream extends InputStream {
648        private volatile boolean closeRequested = false;
649        protected long jzentry; // address of jzentry data
650        private   long pos;     // current position within entry data
651        protected long rem;     // number of remaining bytes within entry
652        protected long size;    // uncompressed size of this entry
653
654        ZipFileInputStream(long jzentry) {
655            pos = 0;
656            rem = getEntryCSize(jzentry);
657            size = getEntrySize(jzentry);
658            this.jzentry = jzentry;
659        }
660
661        public int read(byte b[], int off, int len) throws IOException {
662            if (rem == 0) {
663                return -1;
664            }
665            if (len <= 0) {
666                return 0;
667            }
668            if (len > rem) {
669                len = (int) rem;
670            }
671            synchronized (ZipFile.this) {
672                ensureOpenOrZipException();
673
674                len = ZipFile.read(ZipFile.this.jzfile, jzentry, pos, b,
675                                   off, len);
676            }
677            if (len > 0) {
678                pos += len;
679                rem -= len;
680            }
681            if (rem == 0) {
682                close();
683            }
684            return len;
685        }
686
687        public int read() throws IOException {
688            byte[] b = new byte[1];
689            if (read(b, 0, 1) == 1) {
690                return b[0] & 0xff;
691            } else {
692                return -1;
693            }
694        }
695
696        public long skip(long n) {
697            if (n > rem)
698                n = rem;
699            pos += n;
700            rem -= n;
701            if (rem == 0) {
702                close();
703            }
704            return n;
705        }
706
707        public int available() {
708            return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem;
709        }
710
711        public long size() {
712            return size;
713        }
714
715        public void close() {
716            if (closeRequested)
717                return;
718            closeRequested = true;
719
720            rem = 0;
721            synchronized (ZipFile.this) {
722                if (jzentry != 0 && ZipFile.this.jzfile != 0) {
723                    freeEntry(ZipFile.this.jzfile, jzentry);
724                    jzentry = 0;
725                }
726            }
727            synchronized (streams) {
728                streams.remove(this);
729            }
730        }
731
732        protected void finalize() {
733            close();
734        }
735    }
736
737    /**
738     * Returns {@code true} if, and only if, the zip file begins with {@code
739     * LOCSIG}.
740     */
741    private boolean startsWithLocHeader() {
742        return locsig;
743    }
744
745    private static native long open(String name, int mode, long lastModified,
746                                    boolean usemmap) throws IOException;
747    private static native int getTotal(long jzfile);
748    private static native boolean startsWithLOC(long jzfile);
749    private static native int read(long jzfile, long jzentry,
750                                   long pos, byte[] b, int off, int len);
751
752    // access to the native zentry object
753    private static native long getEntryTime(long jzentry);
754    private static native long getEntryCrc(long jzentry);
755    private static native long getEntryCSize(long jzentry);
756    private static native long getEntrySize(long jzentry);
757    private static native int getEntryMethod(long jzentry);
758    private static native int getEntryFlag(long jzentry);
759    private static native byte[] getCommentBytes(long jzfile);
760
761    private static final int JZENTRY_NAME = 0;
762    private static final int JZENTRY_EXTRA = 1;
763    private static final int JZENTRY_COMMENT = 2;
764    private static native byte[] getEntryBytes(long jzentry, int type);
765
766    private static native String getZipMessage(long jzfile);
767}
768