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