1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package java.util.zip;
19
20import dalvik.system.CloseGuard;
21import java.io.BufferedInputStream;
22import java.io.Closeable;
23import java.io.DataInputStream;
24import java.io.File;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.RandomAccessFile;
28import java.nio.ByteOrder;
29import java.nio.charset.StandardCharsets;
30import java.util.Enumeration;
31import java.util.Iterator;
32import java.util.LinkedHashMap;
33import libcore.io.BufferIterator;
34import libcore.io.HeapBufferIterator;
35import libcore.io.Streams;
36
37/**
38 * This class provides random read access to a zip file. You pay more to read
39 * the zip file's central directory up front (from the constructor), but if you're using
40 * {@link #getEntry} to look up multiple files by name, you get the benefit of this index.
41 *
42 * <p>If you only want to iterate through all the files (using {@link #entries()}, you should
43 * consider {@link ZipInputStream}, which provides stream-like read access to a zip file and
44 * has a lower up-front cost because you don't pay to build an in-memory index.
45 *
46 * <p>If you want to create a zip file, use {@link ZipOutputStream}. There is no API for updating
47 * an existing zip file.
48 */
49public class ZipFile implements Closeable, ZipConstants {
50    /**
51     * General Purpose Bit Flags, Bit 0.
52     * If set, indicates that the file is encrypted.
53     */
54    static final int GPBF_ENCRYPTED_FLAG = 1 << 0;
55
56    /**
57     * General Purpose Bit Flags, Bit 3.
58     * If this bit is set, the fields crc-32, compressed
59     * size and uncompressed size are set to zero in the
60     * local header.  The correct values are put in the
61     * data descriptor immediately following the compressed
62     * data.  (Note: PKZIP version 2.04g for DOS only
63     * recognizes this bit for method 8 compression, newer
64     * versions of PKZIP recognize this bit for any
65     * compression method.)
66     */
67    static final int GPBF_DATA_DESCRIPTOR_FLAG = 1 << 3;
68
69    /**
70     * General Purpose Bit Flags, Bit 11.
71     * Language encoding flag (EFS).  If this bit is set,
72     * the filename and comment fields for this file
73     * must be encoded using UTF-8.
74     */
75    static final int GPBF_UTF8_FLAG = 1 << 11;
76
77    /**
78     * Supported General Purpose Bit Flags Mask.
79     * Bit mask of bits not supported.
80     * Note: The only bit that we will enforce at this time
81     * is the encrypted bit. Although other bits are not supported,
82     * we must not enforce them as this could break some legitimate
83     * use cases (See http://b/8617715).
84     */
85    static final int GPBF_UNSUPPORTED_MASK = GPBF_ENCRYPTED_FLAG;
86
87    /**
88     * Open zip file for reading.
89     */
90    public static final int OPEN_READ = 1;
91
92    /**
93     * Delete zip file when closed.
94     */
95    public static final int OPEN_DELETE = 4;
96
97    private final String filename;
98
99    private File fileToDeleteOnClose;
100
101    private RandomAccessFile raf;
102
103    private final LinkedHashMap<String, ZipEntry> entries = new LinkedHashMap<String, ZipEntry>();
104
105    private String comment;
106
107    private final CloseGuard guard = CloseGuard.get();
108
109    /**
110     * Constructs a new {@code ZipFile} allowing read access to the contents of the given file.
111     * @throws ZipException if a zip error occurs.
112     * @throws IOException if an {@code IOException} occurs.
113     */
114    public ZipFile(File file) throws ZipException, IOException {
115        this(file, OPEN_READ);
116    }
117
118    /**
119     * Constructs a new {@code ZipFile} allowing read access to the contents of the given file.
120     * @throws IOException if an IOException occurs.
121     */
122    public ZipFile(String name) throws IOException {
123        this(new File(name), OPEN_READ);
124    }
125
126    /**
127     * Constructs a new {@code ZipFile} allowing access to the given file.
128     * The {@code mode} must be either {@code OPEN_READ} or {@code OPEN_READ|OPEN_DELETE}.
129     *
130     * <p>If the {@code OPEN_DELETE} flag is supplied, the file will be deleted at or before the
131     * time that the {@code ZipFile} is closed (the contents will remain accessible until
132     * this {@code ZipFile} is closed); it also calls {@code File.deleteOnExit}.
133     *
134     * @throws IOException if an {@code IOException} occurs.
135     */
136    public ZipFile(File file, int mode) throws IOException {
137        filename = file.getPath();
138        if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) {
139            throw new IllegalArgumentException("Bad mode: " + mode);
140        }
141
142        if ((mode & OPEN_DELETE) != 0) {
143            fileToDeleteOnClose = file;
144            fileToDeleteOnClose.deleteOnExit();
145        } else {
146            fileToDeleteOnClose = null;
147        }
148
149        raf = new RandomAccessFile(filename, "r");
150
151        readCentralDir();
152        guard.open("close");
153    }
154
155    @Override protected void finalize() throws IOException {
156        try {
157            if (guard != null) {
158                guard.warnIfOpen();
159            }
160        } finally {
161            try {
162                super.finalize();
163            } catch (Throwable t) {
164                throw new AssertionError(t);
165            }
166        }
167    }
168
169    /**
170     * Closes this zip file. This method is idempotent. This method may cause I/O if the
171     * zip file needs to be deleted.
172     *
173     * @throws IOException
174     *             if an IOException occurs.
175     */
176    public void close() throws IOException {
177        guard.close();
178
179        RandomAccessFile localRaf = raf;
180        if (localRaf != null) { // Only close initialized instances
181            synchronized (localRaf) {
182                raf = null;
183                localRaf.close();
184            }
185            if (fileToDeleteOnClose != null) {
186                fileToDeleteOnClose.delete();
187                fileToDeleteOnClose = null;
188            }
189        }
190    }
191
192    private void checkNotClosed() {
193        if (raf == null) {
194            throw new IllegalStateException("Zip file closed");
195        }
196    }
197
198    /**
199     * Returns an enumeration of the entries. The entries are listed in the
200     * order in which they appear in the zip file.
201     *
202     * <p>If you only need to iterate over the entries in a zip file, and don't
203     * need random-access entry lookup by name, you should probably use {@link ZipInputStream}
204     * instead, to avoid paying to construct the in-memory index.
205     *
206     * @throws IllegalStateException if this zip file has been closed.
207     */
208    public Enumeration<? extends ZipEntry> entries() {
209        checkNotClosed();
210        final Iterator<ZipEntry> iterator = entries.values().iterator();
211
212        return new Enumeration<ZipEntry>() {
213            public boolean hasMoreElements() {
214                checkNotClosed();
215                return iterator.hasNext();
216            }
217
218            public ZipEntry nextElement() {
219                checkNotClosed();
220                return iterator.next();
221            }
222        };
223    }
224
225    /**
226     * Returns this file's comment, or null if it doesn't have one.
227     * See {@link ZipOutputStream#setComment}.
228     *
229     * @throws IllegalStateException if this zip file has been closed.
230     * @since 1.7
231     */
232    public String getComment() {
233        checkNotClosed();
234        return comment;
235    }
236
237    /**
238     * Returns the zip entry with the given name, or null if there is no such entry.
239     *
240     * @throws IllegalStateException if this zip file has been closed.
241     */
242    public ZipEntry getEntry(String entryName) {
243        checkNotClosed();
244        if (entryName == null) {
245            throw new NullPointerException("entryName == null");
246        }
247
248        ZipEntry ze = entries.get(entryName);
249        if (ze == null) {
250            ze = entries.get(entryName + "/");
251        }
252        return ze;
253    }
254
255    /**
256     * Returns an input stream on the data of the specified {@code ZipEntry}.
257     *
258     * @param entry
259     *            the ZipEntry.
260     * @return an input stream of the data contained in the {@code ZipEntry}.
261     * @throws IOException
262     *             if an {@code IOException} occurs.
263     * @throws IllegalStateException if this zip file has been closed.
264     */
265    public InputStream getInputStream(ZipEntry entry) throws IOException {
266        // Make sure this ZipEntry is in this Zip file.  We run it through the name lookup.
267        entry = getEntry(entry.getName());
268        if (entry == null) {
269            return null;
270        }
271
272        // Create an InputStream at the right part of the file.
273        RandomAccessFile localRaf = raf;
274        synchronized (localRaf) {
275            // We don't know the entry data's start position. All we have is the
276            // position of the entry's local header.
277            // http://www.pkware.com/documents/casestudies/APPNOTE.TXT
278            RAFStream rafStream = new RAFStream(localRaf, entry.localHeaderRelOffset);
279            DataInputStream is = new DataInputStream(rafStream);
280
281            final int localMagic = Integer.reverseBytes(is.readInt());
282            if (localMagic != LOCSIG) {
283                throwZipException("Local File Header", localMagic);
284            }
285
286            is.skipBytes(2);
287
288            // At position 6 we find the General Purpose Bit Flag.
289            int gpbf = Short.reverseBytes(is.readShort()) & 0xffff;
290            if ((gpbf & ZipFile.GPBF_UNSUPPORTED_MASK) != 0) {
291                throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf);
292            }
293
294            // Offset 26 has the file name length, and offset 28 has the extra field length.
295            // These lengths can differ from the ones in the central header.
296            is.skipBytes(18);
297            int fileNameLength = Short.reverseBytes(is.readShort()) & 0xffff;
298            int extraFieldLength = Short.reverseBytes(is.readShort()) & 0xffff;
299            is.close();
300
301            // Skip the variable-size file name and extra field data.
302            rafStream.skip(fileNameLength + extraFieldLength);
303
304            if (entry.compressionMethod == ZipEntry.STORED) {
305                rafStream.endOffset = rafStream.offset + entry.size;
306                return rafStream;
307            } else {
308                rafStream.endOffset = rafStream.offset + entry.compressedSize;
309                int bufSize = Math.max(1024, (int) Math.min(entry.getSize(), 65535L));
310                return new ZipInflaterInputStream(rafStream, new Inflater(true), bufSize, entry);
311            }
312        }
313    }
314
315    /**
316     * Gets the file name of this {@code ZipFile}.
317     *
318     * @return the file name of this {@code ZipFile}.
319     */
320    public String getName() {
321        return filename;
322    }
323
324    /**
325     * Returns the number of {@code ZipEntries} in this {@code ZipFile}.
326     *
327     * @return the number of entries in this file.
328     * @throws IllegalStateException if this zip file has been closed.
329     */
330    public int size() {
331        checkNotClosed();
332        return entries.size();
333    }
334
335    /**
336     * Find the central directory and read the contents.
337     *
338     * <p>The central directory can be followed by a variable-length comment
339     * field, so we have to scan through it backwards.  The comment is at
340     * most 64K, plus we have 18 bytes for the end-of-central-dir stuff
341     * itself, plus apparently sometimes people throw random junk on the end
342     * just for the fun of it.
343     *
344     * <p>This is all a little wobbly.  If the wrong value ends up in the EOCD
345     * area, we're hosed. This appears to be the way that everybody handles
346     * it though, so we're in good company if this fails.
347     */
348    private void readCentralDir() throws IOException {
349        // Scan back, looking for the End Of Central Directory field. If the zip file doesn't
350        // have an overall comment (unrelated to any per-entry comments), we'll hit the EOCD
351        // on the first try.
352        // No need to synchronize raf here -- we only do this when we first open the zip file.
353        long scanOffset = raf.length() - ENDHDR;
354        if (scanOffset < 0) {
355            throw new ZipException("File too short to be a zip file: " + raf.length());
356        }
357
358        raf.seek(0);
359        final int headerMagic = Integer.reverseBytes(raf.readInt());
360        if (headerMagic != LOCSIG) {
361            throw new ZipException("Not a zip archive");
362        }
363
364        long stopOffset = scanOffset - 65536;
365        if (stopOffset < 0) {
366            stopOffset = 0;
367        }
368
369        while (true) {
370            raf.seek(scanOffset);
371            if (Integer.reverseBytes(raf.readInt()) == ENDSIG) {
372                break;
373            }
374
375            scanOffset--;
376            if (scanOffset < stopOffset) {
377                throw new ZipException("End Of Central Directory signature not found");
378            }
379        }
380
381        // Read the End Of Central Directory. ENDHDR includes the signature bytes,
382        // which we've already read.
383        byte[] eocd = new byte[ENDHDR - 4];
384        raf.readFully(eocd);
385
386        // Pull out the information we need.
387        BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN);
388        int diskNumber = it.readShort() & 0xffff;
389        int diskWithCentralDir = it.readShort() & 0xffff;
390        int numEntries = it.readShort() & 0xffff;
391        int totalNumEntries = it.readShort() & 0xffff;
392        it.skip(4); // Ignore centralDirSize.
393        long centralDirOffset = ((long) it.readInt()) & 0xffffffffL;
394        int commentLength = it.readShort() & 0xffff;
395
396        if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {
397            throw new ZipException("Spanned archives not supported");
398        }
399
400        if (commentLength > 0) {
401            byte[] commentBytes = new byte[commentLength];
402            raf.readFully(commentBytes);
403            comment = new String(commentBytes, 0, commentBytes.length, StandardCharsets.UTF_8);
404        }
405
406        // Seek to the first CDE and read all entries.
407        // We have to do this now (from the constructor) rather than lazily because the
408        // public API doesn't allow us to throw IOException except from the constructor
409        // or from getInputStream.
410        RAFStream rafStream = new RAFStream(raf, centralDirOffset);
411        BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096);
412        byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.
413        for (int i = 0; i < numEntries; ++i) {
414            ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream);
415            if (newEntry.localHeaderRelOffset >= centralDirOffset) {
416                throw new ZipException("Local file header offset is after central directory");
417            }
418            String entryName = newEntry.getName();
419            if (entries.put(entryName, newEntry) != null) {
420                throw new ZipException("Duplicate entry name: " + entryName);
421            }
422        }
423    }
424
425    static void throwZipException(String msg, int magic) throws ZipException {
426        final String hexString = IntegralToString.intToHexString(magic, true, 8);
427        throw new ZipException(msg + " signature not found; was " + hexString);
428    }
429
430    /**
431     * Wrap a stream around a RandomAccessFile.  The RandomAccessFile is shared
432     * among all streams returned by getInputStream(), so we have to synchronize
433     * access to it.  (We can optimize this by adding buffering here to reduce
434     * collisions.)
435     *
436     * <p>We could support mark/reset, but we don't currently need them.
437     */
438    static class RAFStream extends InputStream {
439        private final RandomAccessFile sharedRaf;
440        private long endOffset;
441        private long offset;
442
443        public RAFStream(RandomAccessFile raf, long initialOffset) throws IOException {
444            sharedRaf = raf;
445            offset = initialOffset;
446            endOffset = raf.length();
447        }
448
449        @Override public int available() throws IOException {
450            return (offset < endOffset ? 1 : 0);
451        }
452
453        @Override public int read() throws IOException {
454            return Streams.readSingleByte(this);
455        }
456
457        @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
458            synchronized (sharedRaf) {
459                final long length = endOffset - offset;
460                if (byteCount > length) {
461                    byteCount = (int) length;
462                }
463                sharedRaf.seek(offset);
464                int count = sharedRaf.read(buffer, byteOffset, byteCount);
465                if (count > 0) {
466                    offset += count;
467                    return count;
468                } else {
469                    return -1;
470                }
471            }
472        }
473
474        @Override public long skip(long byteCount) throws IOException {
475            if (byteCount > endOffset - offset) {
476                byteCount = endOffset - offset;
477            }
478            offset += byteCount;
479            return byteCount;
480        }
481
482        public int fill(Inflater inflater, int nativeEndBufSize) throws IOException {
483            synchronized (sharedRaf) {
484                int len = Math.min((int) (endOffset - offset), nativeEndBufSize);
485                int cnt = inflater.setFileInput(sharedRaf.getFD(), offset, nativeEndBufSize);
486                // setFileInput read from the file, so we need to get the OS and RAFStream back
487                // in sync...
488                skip(cnt);
489                return len;
490            }
491        }
492    }
493
494    static class ZipInflaterInputStream extends InflaterInputStream {
495        private final ZipEntry entry;
496        private long bytesRead = 0;
497
498        public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
499            super(is, inf, bsize);
500            this.entry = entry;
501        }
502
503        @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
504            final int i;
505            try {
506                i = super.read(buffer, byteOffset, byteCount);
507            } catch (IOException e) {
508                throw new IOException("Error reading data for " + entry.getName() + " near offset "
509                        + bytesRead, e);
510            }
511            if (i == -1) {
512                if (entry.size != bytesRead) {
513                    throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
514                            + entry.size);
515                }
516            } else {
517                bytesRead += i;
518            }
519            return i;
520        }
521
522        @Override public int available() throws IOException {
523            if (closed) {
524                // Our superclass will throw an exception, but there's a jtreg test that
525                // explicitly checks that the InputStream returned from ZipFile.getInputStream
526                // returns 0 even when closed.
527                return 0;
528            }
529            return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
530        }
531    }
532}
533