Home | History | Annotate | Download | only in zip
      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 
     18 package java.util.zip;
     19 
     20 import dalvik.system.CloseGuard;
     21 import java.io.BufferedInputStream;
     22 import java.io.EOFException;
     23 import java.io.DataInputStream;
     24 import java.io.File;
     25 import java.io.IOException;
     26 import java.io.InputStream;
     27 import java.io.RandomAccessFile;
     28 import java.nio.ByteOrder;
     29 import java.util.Enumeration;
     30 import java.util.Iterator;
     31 import java.util.LinkedHashMap;
     32 import libcore.io.BufferIterator;
     33 import libcore.io.HeapBufferIterator;
     34 import libcore.io.Streams;
     35 
     36 /**
     37  * This class provides random read access to a <i>ZIP-archive</i> file.
     38  * <p>
     39  * While {@code ZipInputStream} provides stream based read access to a
     40  * <i>ZIP-archive</i>, this class implements more efficient (file based) access
     41  * and makes use of the <i>central directory</i> within a <i>ZIP-archive</i>.
     42  * <p>
     43  * Use {@code ZipOutputStream} if you want to create an archive.
     44  * <p>
     45  * A temporary ZIP file can be marked for automatic deletion upon closing it.
     46  *
     47  * @see ZipEntry
     48  * @see ZipOutputStream
     49  */
     50 public class ZipFile implements ZipConstants {
     51     /**
     52      * General Purpose Bit Flags, Bit 3.
     53      * If this bit is set, the fields crc-32, compressed
     54      * size and uncompressed size are set to zero in the
     55      * local header.  The correct values are put in the
     56      * data descriptor immediately following the compressed
     57      * data.  (Note: PKZIP version 2.04g for DOS only
     58      * recognizes this bit for method 8 compression, newer
     59      * versions of PKZIP recognize this bit for any
     60      * compression method.)
     61      */
     62     static final int GPBF_DATA_DESCRIPTOR_FLAG = 1 << 3;
     63 
     64     /**
     65      * General Purpose Bit Flags, Bit 11.
     66      * Language encoding flag (EFS).  If this bit is set,
     67      * the filename and comment fields for this file
     68      * must be encoded using UTF-8.
     69      */
     70     static final int GPBF_UTF8_FLAG = 1 << 11;
     71 
     72     /**
     73      * Open ZIP file for read.
     74      */
     75     public static final int OPEN_READ = 1;
     76 
     77     /**
     78      * Delete ZIP file when closed.
     79      */
     80     public static final int OPEN_DELETE = 4;
     81 
     82     private final String fileName;
     83 
     84     private File fileToDeleteOnClose;
     85 
     86     private RandomAccessFile mRaf;
     87 
     88     private final LinkedHashMap<String, ZipEntry> mEntries = new LinkedHashMap<String, ZipEntry>();
     89 
     90     private final CloseGuard guard = CloseGuard.get();
     91 
     92     /**
     93      * Constructs a new {@code ZipFile} with the specified file.
     94      *
     95      * @param file
     96      *            the file to read from.
     97      * @throws ZipException
     98      *             if a ZIP error occurs.
     99      * @throws IOException
    100      *             if an {@code IOException} occurs.
    101      */
    102     public ZipFile(File file) throws ZipException, IOException {
    103         this(file, OPEN_READ);
    104     }
    105 
    106     /**
    107      * Opens a file as <i>ZIP-archive</i>. "mode" must be {@code OPEN_READ} or
    108      * {@code OPEN_DELETE} . The latter sets the "delete on exit" flag through a
    109      * file.
    110      *
    111      * @param file
    112      *            the ZIP file to read.
    113      * @param mode
    114      *            the mode of the file open operation.
    115      * @throws IOException
    116      *             if an {@code IOException} occurs.
    117      */
    118     public ZipFile(File file, int mode) throws IOException {
    119         fileName = file.getPath();
    120         if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) {
    121             throw new IllegalArgumentException();
    122         }
    123 
    124         if ((mode & OPEN_DELETE) != 0) {
    125             fileToDeleteOnClose = file; // file.deleteOnExit();
    126         } else {
    127             fileToDeleteOnClose = null;
    128         }
    129 
    130         mRaf = new RandomAccessFile(fileName, "r");
    131 
    132         readCentralDir();
    133         guard.open("close");
    134     }
    135 
    136     /**
    137      * Opens a ZIP archived file.
    138      *
    139      * @param name
    140      *            the name of the ZIP file.
    141      * @throws IOException
    142      *             if an IOException occurs.
    143      */
    144     public ZipFile(String name) throws IOException {
    145         this(new File(name), OPEN_READ);
    146     }
    147 
    148     @Override protected void finalize() throws IOException {
    149         try {
    150             if (guard != null) {
    151                 guard.warnIfOpen();
    152             }
    153         } finally {
    154             try {
    155                 super.finalize();
    156             } catch (Throwable t) {
    157                 throw new AssertionError(t);
    158             }
    159         }
    160     }
    161 
    162     /**
    163      * Closes this ZIP file. This method is idempotent.
    164      *
    165      * @throws IOException
    166      *             if an IOException occurs.
    167      */
    168     public void close() throws IOException {
    169         guard.close();
    170         RandomAccessFile raf = mRaf;
    171 
    172         if (raf != null) { // Only close initialized instances
    173             synchronized(raf) {
    174                 mRaf = null;
    175                 raf.close();
    176             }
    177             if (fileToDeleteOnClose != null) {
    178                 fileToDeleteOnClose.delete();
    179                 fileToDeleteOnClose = null;
    180             }
    181         }
    182     }
    183 
    184     private void checkNotClosed() {
    185         if (mRaf == null) {
    186             throw new IllegalStateException("Zip file closed");
    187         }
    188     }
    189 
    190     /**
    191      * Returns an enumeration of the entries. The entries are listed in the
    192      * order in which they appear in the ZIP archive.
    193      *
    194      * @return the enumeration of the entries.
    195      * @throws IllegalStateException if this ZIP file has been closed.
    196      */
    197     public Enumeration<? extends ZipEntry> entries() {
    198         checkNotClosed();
    199         final Iterator<ZipEntry> iterator = mEntries.values().iterator();
    200 
    201         return new Enumeration<ZipEntry>() {
    202             public boolean hasMoreElements() {
    203                 checkNotClosed();
    204                 return iterator.hasNext();
    205             }
    206 
    207             public ZipEntry nextElement() {
    208                 checkNotClosed();
    209                 return iterator.next();
    210             }
    211         };
    212     }
    213 
    214     /**
    215      * Gets the ZIP entry with the specified name from this {@code ZipFile}.
    216      *
    217      * @param entryName
    218      *            the name of the entry in the ZIP file.
    219      * @return a {@code ZipEntry} or {@code null} if the entry name does not
    220      *         exist in the ZIP file.
    221      * @throws IllegalStateException if this ZIP file has been closed.
    222      */
    223     public ZipEntry getEntry(String entryName) {
    224         checkNotClosed();
    225         if (entryName == null) {
    226             throw new NullPointerException("entryName == null");
    227         }
    228 
    229         ZipEntry ze = mEntries.get(entryName);
    230         if (ze == null) {
    231             ze = mEntries.get(entryName + "/");
    232         }
    233         return ze;
    234     }
    235 
    236     /**
    237      * Returns an input stream on the data of the specified {@code ZipEntry}.
    238      *
    239      * @param entry
    240      *            the ZipEntry.
    241      * @return an input stream of the data contained in the {@code ZipEntry}.
    242      * @throws IOException
    243      *             if an {@code IOException} occurs.
    244      * @throws IllegalStateException if this ZIP file has been closed.
    245      */
    246     public InputStream getInputStream(ZipEntry entry) throws IOException {
    247         // Make sure this ZipEntry is in this Zip file.  We run it through the name lookup.
    248         entry = getEntry(entry.getName());
    249         if (entry == null) {
    250             return null;
    251         }
    252 
    253         // Create an InputStream at the right part of the file.
    254         RandomAccessFile raf = mRaf;
    255         synchronized (raf) {
    256             // We don't know the entry data's start position. All we have is the
    257             // position of the entry's local header. At position 28 we find the
    258             // length of the extra data. In some cases this length differs from
    259             // the one coming in the central header.
    260             RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);
    261             DataInputStream is = new DataInputStream(rafstrm);
    262             int localExtraLenOrWhatever = Short.reverseBytes(is.readShort());
    263             is.close();
    264 
    265             // Skip the name and this "extra" data or whatever it is:
    266             rafstrm.skip(entry.nameLength + localExtraLenOrWhatever);
    267             rafstrm.mLength = rafstrm.mOffset + entry.compressedSize;
    268             if (entry.compressionMethod == ZipEntry.DEFLATED) {
    269                 int bufSize = Math.max(1024, (int)Math.min(entry.getSize(), 65535L));
    270                 return new ZipInflaterInputStream(rafstrm, new Inflater(true), bufSize, entry);
    271             } else {
    272                 return rafstrm;
    273             }
    274         }
    275     }
    276 
    277     /**
    278      * Gets the file name of this {@code ZipFile}.
    279      *
    280      * @return the file name of this {@code ZipFile}.
    281      */
    282     public String getName() {
    283         return fileName;
    284     }
    285 
    286     /**
    287      * Returns the number of {@code ZipEntries} in this {@code ZipFile}.
    288      *
    289      * @return the number of entries in this file.
    290      * @throws IllegalStateException if this ZIP file has been closed.
    291      */
    292     public int size() {
    293         checkNotClosed();
    294         return mEntries.size();
    295     }
    296 
    297     /**
    298      * Find the central directory and read the contents.
    299      *
    300      * <p>The central directory can be followed by a variable-length comment
    301      * field, so we have to scan through it backwards.  The comment is at
    302      * most 64K, plus we have 18 bytes for the end-of-central-dir stuff
    303      * itself, plus apparently sometimes people throw random junk on the end
    304      * just for the fun of it.
    305      *
    306      * <p>This is all a little wobbly.  If the wrong value ends up in the EOCD
    307      * area, we're hosed. This appears to be the way that everybody handles
    308      * it though, so we're in good company if this fails.
    309      */
    310     private void readCentralDir() throws IOException {
    311         /*
    312          * Scan back, looking for the End Of Central Directory field.  If
    313          * the archive doesn't have a comment, we'll hit it on the first
    314          * try.
    315          *
    316          * No need to synchronize mRaf here -- we only do this when we
    317          * first open the Zip file.
    318          */
    319         long scanOffset = mRaf.length() - ENDHDR;
    320         if (scanOffset < 0) {
    321             throw new ZipException("File too short to be a zip file: " + mRaf.length());
    322         }
    323 
    324         long stopOffset = scanOffset - 65536;
    325         if (stopOffset < 0) {
    326             stopOffset = 0;
    327         }
    328 
    329         final int ENDHEADERMAGIC = 0x06054b50;
    330         while (true) {
    331             mRaf.seek(scanOffset);
    332             if (Integer.reverseBytes(mRaf.readInt()) == ENDHEADERMAGIC) {
    333                 break;
    334             }
    335 
    336             scanOffset--;
    337             if (scanOffset < stopOffset) {
    338                 throw new ZipException("EOCD not found; not a Zip archive?");
    339             }
    340         }
    341 
    342         // Read the End Of Central Directory. We could use ENDHDR instead of the magic number 18,
    343         // but we don't actually need all the header.
    344         byte[] eocd = new byte[18];
    345         mRaf.readFully(eocd);
    346 
    347         // Pull out the information we need.
    348         BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN);
    349         int diskNumber = it.readShort() & 0xffff;
    350         int diskWithCentralDir = it.readShort() & 0xffff;
    351         int numEntries = it.readShort() & 0xffff;
    352         int totalNumEntries = it.readShort() & 0xffff;
    353         it.skip(4); // Ignore centralDirSize.
    354         int centralDirOffset = it.readInt();
    355 
    356         if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {
    357             throw new ZipException("spanned archives not supported");
    358         }
    359 
    360         // Seek to the first CDE and read all entries.
    361         RAFStream rafs = new RAFStream(mRaf, centralDirOffset);
    362         BufferedInputStream bin = new BufferedInputStream(rafs, 4096);
    363         byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.
    364         for (int i = 0; i < numEntries; ++i) {
    365             ZipEntry newEntry = new ZipEntry(hdrBuf, bin);
    366             mEntries.put(newEntry.getName(), newEntry);
    367         }
    368     }
    369 
    370     /**
    371      * Wrap a stream around a RandomAccessFile.  The RandomAccessFile is shared
    372      * among all streams returned by getInputStream(), so we have to synchronize
    373      * access to it.  (We can optimize this by adding buffering here to reduce
    374      * collisions.)
    375      *
    376      * <p>We could support mark/reset, but we don't currently need them.
    377      */
    378     static class RAFStream extends InputStream {
    379 
    380         RandomAccessFile mSharedRaf;
    381         long mOffset;
    382         long mLength;
    383 
    384         public RAFStream(RandomAccessFile raf, long pos) throws IOException {
    385             mSharedRaf = raf;
    386             mOffset = pos;
    387             mLength = raf.length();
    388         }
    389 
    390         @Override public int available() throws IOException {
    391             return (mOffset < mLength ? 1 : 0);
    392         }
    393 
    394         @Override public int read() throws IOException {
    395             return Streams.readSingleByte(this);
    396         }
    397 
    398         @Override public int read(byte[] b, int off, int len) throws IOException {
    399             synchronized (mSharedRaf) {
    400                 mSharedRaf.seek(mOffset);
    401                 if (len > mLength - mOffset) {
    402                     len = (int) (mLength - mOffset);
    403                 }
    404                 int count = mSharedRaf.read(b, off, len);
    405                 if (count > 0) {
    406                     mOffset += count;
    407                     return count;
    408                 } else {
    409                     return -1;
    410                 }
    411             }
    412         }
    413 
    414         @Override
    415         public long skip(long byteCount) throws IOException {
    416             if (byteCount > mLength - mOffset) {
    417                 byteCount = mLength - mOffset;
    418             }
    419             mOffset += byteCount;
    420             return byteCount;
    421         }
    422     }
    423 
    424     static class ZipInflaterInputStream extends InflaterInputStream {
    425 
    426         ZipEntry entry;
    427         long bytesRead = 0;
    428 
    429         public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
    430             super(is, inf, bsize);
    431             this.entry = entry;
    432         }
    433 
    434         @Override
    435         public int read(byte[] buffer, int off, int nbytes) throws IOException {
    436             int i = super.read(buffer, off, nbytes);
    437             if (i != -1) {
    438                 bytesRead += i;
    439             }
    440             return i;
    441         }
    442 
    443         @Override
    444         public int available() throws IOException {
    445             if (closed) {
    446                 // Our superclass will throw an exception, but there's a jtreg test that
    447                 // explicitly checks that the InputStream returned from ZipFile.getInputStream
    448                 // returns 0 even when closed.
    449                 return 0;
    450             }
    451             return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
    452         }
    453     }
    454 }
    455