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 java.io.IOException;
     21 import java.io.InputStream;
     22 import java.io.PushbackInputStream;
     23 import java.nio.ByteOrder;
     24 import java.nio.charset.ModifiedUtf8;
     25 import java.util.jar.Attributes;
     26 import java.util.jar.JarEntry;
     27 import java.util.Arrays;
     28 import libcore.io.Memory;
     29 import libcore.io.Streams;
     30 
     31 /**
     32  * This class provides an implementation of {@code FilterInputStream} that
     33  * decompresses data from an {@code InputStream} containing a ZIP archive.
     34  *
     35  * <p>A ZIP archive is a collection of (possibly) compressed files.
     36  * When reading from a {@code ZipInputStream}, you retrieve the
     37  * entry's metadata with {@code getNextEntry} before you can read the userdata.
     38  *
     39  * <p>Although {@code InflaterInputStream} can only read compressed ZIP archive
     40  * entries, this class can read non-compressed entries as well.
     41  *
     42  * <p>Use {@code ZipFile} if you can access the archive as a file directly,
     43  * especially if you want random access to entries, rather than needing to
     44  * iterate over all entries.
     45  *
     46  * <h3>Example</h3>
     47  * <p>Using {@code ZipInputStream} is a little more complicated than {@link GZIPInputStream}
     48  * because ZIP archives are containers that can contain multiple files. This code pulls all the
     49  * files out of a ZIP archive, similar to the {@code unzip(1)} utility.
     50  * <pre>
     51  * InputStream is = ...
     52  * ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
     53  * try {
     54  *     ZipEntry ze;
     55  *     while ((ze = zis.getNextEntry()) != null) {
     56  *         ByteArrayOutputStream baos = new ByteArrayOutputStream();
     57  *         byte[] buffer = new byte[1024];
     58  *         int count;
     59  *         while ((count = zis.read(buffer)) != -1) {
     60  *             baos.write(buffer, 0, count);
     61  *         }
     62  *         String filename = ze.getName();
     63  *         byte[] bytes = baos.toByteArray();
     64  *         // do something with 'filename' and 'bytes'...
     65  *     }
     66  * } finally {
     67  *     zis.close();
     68  * }
     69  * </pre>
     70  *
     71  * @see ZipEntry
     72  * @see ZipFile
     73  */
     74 public class ZipInputStream extends InflaterInputStream implements ZipConstants {
     75     private static final int ZIPLocalHeaderVersionNeeded = 20;
     76 
     77     private boolean entriesEnd = false;
     78 
     79     private boolean hasDD = false;
     80 
     81     private int entryIn = 0;
     82 
     83     private int inRead, lastRead = 0;
     84 
     85     private ZipEntry currentEntry;
     86 
     87     private final byte[] hdrBuf = new byte[LOCHDR - LOCVER];
     88 
     89     private final CRC32 crc = new CRC32();
     90 
     91     private byte[] nameBuf = new byte[256];
     92 
     93     private char[] charBuf = new char[256];
     94 
     95     /**
     96      * Constructs a new {@code ZipInputStream} from the specified input stream.
     97      *
     98      * @param stream
     99      *            the input stream to representing a ZIP archive.
    100      */
    101     public ZipInputStream(InputStream stream) {
    102         super(new PushbackInputStream(stream, BUF_SIZE), new Inflater(true));
    103         if (stream == null) {
    104             throw new NullPointerException();
    105         }
    106     }
    107 
    108     /**
    109      * Closes this {@code ZipInputStream}.
    110      *
    111      * @throws IOException
    112      *             if an {@code IOException} occurs.
    113      */
    114     @Override
    115     public void close() throws IOException {
    116         if (!closed) {
    117             closeEntry(); // Close the current entry
    118             super.close();
    119         }
    120     }
    121 
    122     /**
    123      * Closes the current ZIP entry and positions to read the next entry.
    124      *
    125      * @throws IOException
    126      *             if an {@code IOException} occurs.
    127      */
    128     public void closeEntry() throws IOException {
    129         checkClosed();
    130         if (currentEntry == null) {
    131             return;
    132         }
    133         if (currentEntry instanceof java.util.jar.JarEntry) {
    134             Attributes temp = ((JarEntry) currentEntry).getAttributes();
    135             if (temp != null && temp.containsKey("hidden")) {
    136                 return;
    137             }
    138         }
    139 
    140         /*
    141          * The following code is careful to leave the ZipInputStream in a
    142          * consistent state, even when close() results in an exception. It does
    143          * so by:
    144          *  - pushing bytes back into the source stream
    145          *  - reading a data descriptor footer from the source stream
    146          *  - resetting fields that manage the entry being closed
    147          */
    148 
    149         // Ensure all entry bytes are read
    150         Exception failure = null;
    151         try {
    152             Streams.skipAll(this);
    153         } catch (Exception e) {
    154             failure = e;
    155         }
    156 
    157         int inB, out;
    158         if (currentEntry.compressionMethod == ZipEntry.DEFLATED) {
    159             inB = inf.getTotalIn();
    160             out = inf.getTotalOut();
    161         } else {
    162             inB = inRead;
    163             out = inRead;
    164         }
    165         int diff = entryIn - inB;
    166         // Pushback any required bytes
    167         if (diff != 0) {
    168             ((PushbackInputStream) in).unread(buf, len - diff, diff);
    169         }
    170 
    171         try {
    172             readAndVerifyDataDescriptor(inB, out);
    173         } catch (Exception e) {
    174             if (failure == null) { // otherwise we're already going to throw
    175                 failure = e;
    176             }
    177         }
    178 
    179         inf.reset();
    180         lastRead = inRead = entryIn = len = 0;
    181         crc.reset();
    182         currentEntry = null;
    183 
    184         if (failure != null) {
    185             if (failure instanceof IOException) {
    186                 throw (IOException) failure;
    187             } else if (failure instanceof RuntimeException) {
    188                 throw (RuntimeException) failure;
    189             }
    190             AssertionError error = new AssertionError();
    191             error.initCause(failure);
    192             throw error;
    193         }
    194     }
    195 
    196     private void readAndVerifyDataDescriptor(int inB, int out) throws IOException {
    197         if (hasDD) {
    198             Streams.readFully(in, hdrBuf, 0, EXTHDR);
    199             int sig = Memory.peekInt(hdrBuf, 0, ByteOrder.LITTLE_ENDIAN);
    200             if (sig != (int) EXTSIG) {
    201                 throw new ZipException(String.format("unknown format (EXTSIG=%x)", sig));
    202             }
    203             currentEntry.crc = ((long) Memory.peekInt(hdrBuf, EXTCRC, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    204             currentEntry.compressedSize = ((long) Memory.peekInt(hdrBuf, EXTSIZ, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    205             currentEntry.size = ((long) Memory.peekInt(hdrBuf, EXTLEN, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    206         }
    207         if (currentEntry.crc != crc.getValue()) {
    208             throw new ZipException("CRC mismatch");
    209         }
    210         if (currentEntry.compressedSize != inB || currentEntry.size != out) {
    211             throw new ZipException("Size mismatch");
    212         }
    213     }
    214 
    215     /**
    216      * Reads the next entry from this {@code ZipInputStream} or {@code null} if
    217      * no more entries are present.
    218      *
    219      * @return the next {@code ZipEntry} contained in the input stream.
    220      * @throws IOException
    221      *             if an {@code IOException} occurs.
    222      * @see ZipEntry
    223      */
    224     public ZipEntry getNextEntry() throws IOException {
    225         closeEntry();
    226         if (entriesEnd) {
    227             return null;
    228         }
    229 
    230         // Read the signature to see whether there's another local file header.
    231         Streams.readFully(in, hdrBuf, 0, 4);
    232         int hdr = Memory.peekInt(hdrBuf, 0, ByteOrder.LITTLE_ENDIAN);
    233         if (hdr == CENSIG) {
    234             entriesEnd = true;
    235             return null;
    236         }
    237         if (hdr != LOCSIG) {
    238             return null;
    239         }
    240 
    241         // Read the local file header.
    242         Streams.readFully(in, hdrBuf, 0, (LOCHDR - LOCVER));
    243         int version = peekShort(0) & 0xff;
    244         if (version > ZIPLocalHeaderVersionNeeded) {
    245             throw new ZipException("Cannot read local header version " + version);
    246         }
    247         int flags = peekShort(LOCFLG - LOCVER);
    248         hasDD = ((flags & ZipFile.GPBF_DATA_DESCRIPTOR_FLAG) != 0);
    249         int ceLastModifiedTime = peekShort(LOCTIM - LOCVER);
    250         int ceLastModifiedDate = peekShort(LOCTIM - LOCVER + 2);
    251         int ceCompressionMethod = peekShort(LOCHOW - LOCVER);
    252         long ceCrc = 0, ceCompressedSize = 0, ceSize = -1;
    253         if (!hasDD) {
    254             ceCrc = ((long) Memory.peekInt(hdrBuf, LOCCRC - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    255             ceCompressedSize = ((long) Memory.peekInt(hdrBuf, LOCSIZ - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    256             ceSize = ((long) Memory.peekInt(hdrBuf, LOCLEN - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL;
    257         }
    258         int nameLength = peekShort(LOCNAM - LOCVER);
    259         if (nameLength == 0) {
    260             throw new ZipException("Entry is not named");
    261         }
    262         int extraLength = peekShort(LOCEXT - LOCVER);
    263 
    264         if (nameLength > nameBuf.length) {
    265             nameBuf = new byte[nameLength];
    266             // The bytes are modified UTF-8, so the number of chars will always be less than or
    267             // equal to the number of bytes. It's fine if this buffer is too long.
    268             charBuf = new char[nameLength];
    269         }
    270         Streams.readFully(in, nameBuf, 0, nameLength);
    271         currentEntry = createZipEntry(ModifiedUtf8.decode(nameBuf, charBuf, 0, nameLength));
    272         currentEntry.time = ceLastModifiedTime;
    273         currentEntry.modDate = ceLastModifiedDate;
    274         currentEntry.setMethod(ceCompressionMethod);
    275         if (ceSize != -1) {
    276             currentEntry.setCrc(ceCrc);
    277             currentEntry.setSize(ceSize);
    278             currentEntry.setCompressedSize(ceCompressedSize);
    279         }
    280         if (extraLength > 0) {
    281             byte[] extraData = new byte[extraLength];
    282             Streams.readFully(in, extraData, 0, extraLength);
    283             currentEntry.setExtra(extraData);
    284         }
    285         return currentEntry;
    286     }
    287 
    288     private int peekShort(int offset) {
    289         return Memory.peekShort(hdrBuf, offset, ByteOrder.LITTLE_ENDIAN) & 0xffff;
    290     }
    291 
    292     /**
    293      * Reads up to the specified number of uncompressed bytes into the buffer
    294      * starting at the offset.
    295      *
    296      * @return the number of bytes read
    297      */
    298     @Override
    299     public int read(byte[] buffer, int offset, int byteCount) throws IOException {
    300         checkClosed();
    301         Arrays.checkOffsetAndCount(buffer.length, offset, byteCount);
    302 
    303         if (inf.finished() || currentEntry == null) {
    304             return -1;
    305         }
    306 
    307         if (currentEntry.compressionMethod == ZipEntry.STORED) {
    308             int csize = (int) currentEntry.size;
    309             if (inRead >= csize) {
    310                 return -1;
    311             }
    312             if (lastRead >= len) {
    313                 lastRead = 0;
    314                 if ((len = in.read(buf)) == -1) {
    315                     eof = true;
    316                     return -1;
    317                 }
    318                 entryIn += len;
    319             }
    320             int toRead = byteCount > (len - lastRead) ? len - lastRead : byteCount;
    321             if ((csize - inRead) < toRead) {
    322                 toRead = csize - inRead;
    323             }
    324             System.arraycopy(buf, lastRead, buffer, offset, toRead);
    325             lastRead += toRead;
    326             inRead += toRead;
    327             crc.update(buffer, offset, toRead);
    328             return toRead;
    329         }
    330         if (inf.needsInput()) {
    331             fill();
    332             if (len > 0) {
    333                 entryIn += len;
    334             }
    335         }
    336         int read;
    337         try {
    338             read = inf.inflate(buffer, offset, byteCount);
    339         } catch (DataFormatException e) {
    340             throw new ZipException(e.getMessage());
    341         }
    342         if (read == 0 && inf.finished()) {
    343             return -1;
    344         }
    345         crc.update(buffer, offset, read);
    346         return read;
    347     }
    348 
    349     @Override
    350     public int available() throws IOException {
    351         checkClosed();
    352         // The InflaterInputStream contract says we must only return 0 or 1.
    353         return (currentEntry == null || inRead < currentEntry.size) ? 1 : 0;
    354     }
    355 
    356     /**
    357      * creates a {@link ZipEntry } with the given name.
    358      *
    359      * @param name
    360      *            the name of the entry.
    361      * @return the created {@code ZipEntry}.
    362      */
    363     protected ZipEntry createZipEntry(String name) {
    364         return new ZipEntry(name);
    365     }
    366 
    367     private void checkClosed() throws IOException {
    368         if (closed) {
    369             throw new IOException("Stream is closed");
    370         }
    371     }
    372 }
    373