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.ByteArrayOutputStream;
     21 import java.io.IOException;
     22 import java.io.OutputStream;
     23 import java.nio.charset.StandardCharsets;
     24 import java.util.Arrays;
     25 import java.util.HashSet;
     26 import libcore.util.EmptyArray;
     27 
     28 /**
     29  * Used to write (compress) data into zip files.
     30  *
     31  * <p>{@code ZipOutputStream} is used to write {@link ZipEntry}s to the underlying
     32  * stream. Output from {@code ZipOutputStream} can be read using {@link ZipFile}
     33  * or {@link ZipInputStream}.
     34  *
     35  * <p>While {@code DeflaterOutputStream} can write compressed zip file
     36  * entries, this extension can write uncompressed entries as well.
     37  * Use {@link ZipEntry#setMethod} or {@link #setMethod} with the {@link ZipEntry#STORED} flag.
     38  *
     39  * <h3>Example</h3>
     40  * <p>Using {@code ZipOutputStream} is a little more complicated than {@link GZIPOutputStream}
     41  * because zip files are containers that can contain multiple files. This code creates a zip
     42  * file containing several files, similar to the {@code zip(1)} utility.
     43  * <pre>
     44  * OutputStream os = ...
     45  * ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os));
     46  * try {
     47  *     for (int i = 0; i < fileCount; ++i) {
     48  *         String filename = ...
     49  *         byte[] bytes = ...
     50  *         ZipEntry entry = new ZipEntry(filename);
     51  *         zos.putNextEntry(entry);
     52  *         zos.write(bytes);
     53  *         zos.closeEntry();
     54  *     }
     55  * } finally {
     56  *     zos.close();
     57  * }
     58  * </pre>
     59  */
     60 public class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
     61 
     62     /**
     63      * Indicates deflated entries.
     64      */
     65     public static final int DEFLATED = 8;
     66 
     67     /**
     68      * Indicates uncompressed entries.
     69      */
     70     public static final int STORED = 0;
     71 
     72     private static final int ZIP_VERSION_2_0 = 20; // Zip specification version 2.0.
     73 
     74     private byte[] commentBytes = EmptyArray.BYTE;
     75 
     76     private final HashSet<String> entries = new HashSet<String>();
     77 
     78     private int defaultCompressionMethod = DEFLATED;
     79 
     80     private int compressionLevel = Deflater.DEFAULT_COMPRESSION;
     81 
     82     private ByteArrayOutputStream cDir = new ByteArrayOutputStream();
     83 
     84     private ZipEntry currentEntry;
     85 
     86     private final CRC32 crc = new CRC32();
     87 
     88     private int offset = 0, curOffset = 0, nameLength;
     89 
     90     private byte[] nameBytes;
     91 
     92     /**
     93      * Constructs a new {@code ZipOutputStream} that writes a zip file
     94      * to the given {@code OutputStream}.
     95      */
     96     public ZipOutputStream(OutputStream os) {
     97         super(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
     98     }
     99 
    100     /**
    101      * Closes the current {@code ZipEntry}, if any, and the underlying output
    102      * stream. If the stream is already closed this method does nothing.
    103      *
    104      * @throws IOException
    105      *             If an error occurs closing the stream.
    106      */
    107     @Override
    108     public void close() throws IOException {
    109         // don't call super.close() because that calls finish() conditionally
    110         if (out != null) {
    111             finish();
    112             def.end();
    113             out.close();
    114             out = null;
    115         }
    116     }
    117 
    118     /**
    119      * Closes the current {@code ZipEntry}. Any entry terminal data is written
    120      * to the underlying stream.
    121      *
    122      * @throws IOException
    123      *             If an error occurs closing the entry.
    124      */
    125     public void closeEntry() throws IOException {
    126         checkOpen();
    127         if (currentEntry == null) {
    128             return;
    129         }
    130         if (currentEntry.getMethod() == DEFLATED) {
    131             super.finish();
    132         }
    133 
    134         // Verify values for STORED types
    135         if (currentEntry.getMethod() == STORED) {
    136             if (crc.getValue() != currentEntry.crc) {
    137                 throw new ZipException("CRC mismatch");
    138             }
    139             if (currentEntry.size != crc.tbytes) {
    140                 throw new ZipException("Size mismatch");
    141             }
    142         }
    143         curOffset = LOCHDR;
    144 
    145         // Write the DataDescriptor
    146         if (currentEntry.getMethod() != STORED) {
    147             curOffset += EXTHDR;
    148             writeLong(out, EXTSIG);
    149             writeLong(out, currentEntry.crc = crc.getValue());
    150             writeLong(out, currentEntry.compressedSize = def.getTotalOut());
    151             writeLong(out, currentEntry.size = def.getTotalIn());
    152         }
    153         // Update the CentralDirectory
    154         // http://www.pkware.com/documents/casestudies/APPNOTE.TXT
    155         int flags = currentEntry.getMethod() == STORED ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG;
    156         // Since gingerbread, we always set the UTF-8 flag on individual files.
    157         // Some tools insist that the central directory also have the UTF-8 flag.
    158         // http://code.google.com/p/android/issues/detail?id=20214
    159         flags |= ZipFile.GPBF_UTF8_FLAG;
    160         writeLong(cDir, CENSIG);
    161         writeShort(cDir, ZIP_VERSION_2_0); // Version this file was made by.
    162         writeShort(cDir, ZIP_VERSION_2_0); // Minimum version needed to extract.
    163         writeShort(cDir, flags);
    164         writeShort(cDir, currentEntry.getMethod());
    165         writeShort(cDir, currentEntry.time);
    166         writeShort(cDir, currentEntry.modDate);
    167         writeLong(cDir, crc.getValue());
    168         if (currentEntry.getMethod() == DEFLATED) {
    169             curOffset += writeLong(cDir, def.getTotalOut());
    170             writeLong(cDir, def.getTotalIn());
    171         } else {
    172             curOffset += writeLong(cDir, crc.tbytes);
    173             writeLong(cDir, crc.tbytes);
    174         }
    175         curOffset += writeShort(cDir, nameLength);
    176         if (currentEntry.extra != null) {
    177             curOffset += writeShort(cDir, currentEntry.extra.length);
    178         } else {
    179             writeShort(cDir, 0);
    180         }
    181 
    182         String comment = currentEntry.getComment();
    183         byte[] commentBytes = EmptyArray.BYTE;
    184         if (comment != null) {
    185             commentBytes = comment.getBytes(StandardCharsets.UTF_8);
    186         }
    187         writeShort(cDir, commentBytes.length); // Comment length.
    188         writeShort(cDir, 0); // Disk Start
    189         writeShort(cDir, 0); // Internal File Attributes
    190         writeLong(cDir, 0); // External File Attributes
    191         writeLong(cDir, offset);
    192         cDir.write(nameBytes);
    193         nameBytes = null;
    194         if (currentEntry.extra != null) {
    195             cDir.write(currentEntry.extra);
    196         }
    197         offset += curOffset;
    198         if (commentBytes.length > 0) {
    199             cDir.write(commentBytes);
    200         }
    201         currentEntry = null;
    202         crc.reset();
    203         def.reset();
    204         done = false;
    205     }
    206 
    207     /**
    208      * Indicates that all entries have been written to the stream. Any terminal
    209      * information is written to the underlying stream.
    210      *
    211      * @throws IOException
    212      *             if an error occurs while terminating the stream.
    213      */
    214     @Override
    215     public void finish() throws IOException {
    216         // TODO: is there a bug here? why not checkOpen?
    217         if (out == null) {
    218             throw new IOException("Stream is closed");
    219         }
    220         if (cDir == null) {
    221             return;
    222         }
    223         if (entries.isEmpty()) {
    224             throw new ZipException("No entries");
    225         }
    226         if (currentEntry != null) {
    227             closeEntry();
    228         }
    229         int cdirSize = cDir.size();
    230         // Write Central Dir End
    231         writeLong(cDir, ENDSIG);
    232         writeShort(cDir, 0); // Disk Number
    233         writeShort(cDir, 0); // Start Disk
    234         writeShort(cDir, entries.size()); // Number of entries
    235         writeShort(cDir, entries.size()); // Number of entries
    236         writeLong(cDir, cdirSize); // Size of central dir
    237         writeLong(cDir, offset); // Offset of central dir
    238         writeShort(cDir, commentBytes.length);
    239         if (commentBytes.length > 0) {
    240             cDir.write(commentBytes);
    241         }
    242         // Write the central directory.
    243         cDir.writeTo(out);
    244         cDir = null;
    245     }
    246 
    247     /**
    248      * Writes entry information to the underlying stream. Data associated with
    249      * the entry can then be written using {@code write()}. After data is
    250      * written {@code closeEntry()} must be called to complete the writing of
    251      * the entry to the underlying stream.
    252      *
    253      * @param ze
    254      *            the {@code ZipEntry} to store.
    255      * @throws IOException
    256      *             If an error occurs storing the entry.
    257      * @see #write
    258      */
    259     public void putNextEntry(ZipEntry ze) throws IOException {
    260         if (currentEntry != null) {
    261             closeEntry();
    262         }
    263 
    264         // Did this ZipEntry specify a method, or should we use the default?
    265         int method = ze.getMethod();
    266         if (method == -1) {
    267             method = defaultCompressionMethod;
    268         }
    269 
    270         // If the method is STORED, check that the ZipEntry was configured appropriately.
    271         if (method == STORED) {
    272             if (ze.getCompressedSize() == -1) {
    273                 ze.setCompressedSize(ze.getSize());
    274             } else if (ze.getSize() == -1) {
    275                 ze.setSize(ze.getCompressedSize());
    276             }
    277             if (ze.getCrc() == -1) {
    278                 throw new ZipException("STORED entry missing CRC");
    279             }
    280             if (ze.getSize() == -1) {
    281                 throw new ZipException("STORED entry missing size");
    282             }
    283             if (ze.size != ze.compressedSize) {
    284                 throw new ZipException("STORED entry size/compressed size mismatch");
    285             }
    286         }
    287 
    288         checkOpen();
    289 
    290         if (entries.contains(ze.name)) {
    291             throw new ZipException("Entry already exists: " + ze.name);
    292         }
    293         if (entries.size() == 64*1024-1) {
    294             // TODO: support Zip64.
    295             throw new ZipException("Too many entries for the zip file format's 16-bit entry count");
    296         }
    297         nameBytes = ze.name.getBytes(StandardCharsets.UTF_8);
    298         nameLength = nameBytes.length;
    299         if (nameLength > 0xffff) {
    300             throw new IllegalArgumentException("Name too long: " + nameLength + " UTF-8 bytes");
    301         }
    302 
    303         def.setLevel(compressionLevel);
    304         ze.setMethod(method);
    305 
    306         currentEntry = ze;
    307         entries.add(currentEntry.name);
    308 
    309         // Local file header.
    310         // http://www.pkware.com/documents/casestudies/APPNOTE.TXT
    311         int flags = (method == STORED) ? 0 : ZipFile.GPBF_DATA_DESCRIPTOR_FLAG;
    312         // Java always outputs UTF-8 filenames. (Before Java 7, the RI didn't set this flag and used
    313         // modified UTF-8. From Java 7, it sets this flag and uses normal UTF-8.)
    314         flags |= ZipFile.GPBF_UTF8_FLAG;
    315         writeLong(out, LOCSIG); // Entry header
    316         writeShort(out, ZIP_VERSION_2_0); // Minimum version needed to extract.
    317         writeShort(out, flags);
    318         writeShort(out, method);
    319         if (currentEntry.getTime() == -1) {
    320             currentEntry.setTime(System.currentTimeMillis());
    321         }
    322         writeShort(out, currentEntry.time);
    323         writeShort(out, currentEntry.modDate);
    324 
    325         if (method == STORED) {
    326             writeLong(out, currentEntry.crc);
    327             writeLong(out, currentEntry.size);
    328             writeLong(out, currentEntry.size);
    329         } else {
    330             writeLong(out, 0);
    331             writeLong(out, 0);
    332             writeLong(out, 0);
    333         }
    334         writeShort(out, nameLength);
    335         if (currentEntry.extra != null) {
    336             writeShort(out, currentEntry.extra.length);
    337         } else {
    338             writeShort(out, 0);
    339         }
    340         out.write(nameBytes);
    341         if (currentEntry.extra != null) {
    342             out.write(currentEntry.extra);
    343         }
    344     }
    345 
    346     /**
    347      * Sets the comment associated with the file being written. See {@link ZipFile#getComment}.
    348      * @throws IllegalArgumentException if the comment is >= 64 Ki UTF-8 bytes.
    349      */
    350     public void setComment(String comment) {
    351         if (comment == null) {
    352             this.commentBytes = null;
    353             return;
    354         }
    355 
    356         byte[] newCommentBytes = comment.getBytes(StandardCharsets.UTF_8);
    357         if (newCommentBytes.length > 0xffff) {
    358             throw new IllegalArgumentException("Comment too long: " + newCommentBytes.length + " bytes");
    359         }
    360         this.commentBytes = newCommentBytes;
    361     }
    362 
    363     /**
    364      * Sets the <a href="Deflater.html#compression_level">compression level</a> to be used
    365      * for writing entry data.
    366      */
    367     public void setLevel(int level) {
    368         if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) {
    369             throw new IllegalArgumentException("Bad level: " + level);
    370         }
    371         compressionLevel = level;
    372     }
    373 
    374     /**
    375      * Sets the default compression method to be used when a {@code ZipEntry} doesn't
    376      * explicitly specify a method. See {@link ZipEntry#setMethod} for more details.
    377      */
    378     public void setMethod(int method) {
    379         if (method != STORED && method != DEFLATED) {
    380             throw new IllegalArgumentException("Bad method: " + method);
    381         }
    382         defaultCompressionMethod = method;
    383     }
    384 
    385     private long writeLong(OutputStream os, long i) throws IOException {
    386         // Write out the long value as an unsigned int
    387         os.write((int) (i & 0xFF));
    388         os.write((int) (i >> 8) & 0xFF);
    389         os.write((int) (i >> 16) & 0xFF);
    390         os.write((int) (i >> 24) & 0xFF);
    391         return i;
    392     }
    393 
    394     private int writeShort(OutputStream os, int i) throws IOException {
    395         os.write(i & 0xFF);
    396         os.write((i >> 8) & 0xFF);
    397         return i;
    398     }
    399 
    400     /**
    401      * Writes data for the current entry to the underlying stream.
    402      *
    403      * @exception IOException
    404      *                If an error occurs writing to the stream
    405      */
    406     @Override
    407     public void write(byte[] buffer, int offset, int byteCount) throws IOException {
    408         Arrays.checkOffsetAndCount(buffer.length, offset, byteCount);
    409         if (currentEntry == null) {
    410             throw new ZipException("No active entry");
    411         }
    412 
    413         if (currentEntry.getMethod() == STORED) {
    414             out.write(buffer, offset, byteCount);
    415         } else {
    416             super.write(buffer, offset, byteCount);
    417         }
    418         crc.update(buffer, offset, byteCount);
    419     }
    420 
    421     private void checkOpen() throws IOException {
    422         if (cDir == null) {
    423             throw new IOException("Stream is closed");
    424         }
    425     }
    426 }
    427