ZipOutputStream.java revision d4f87eb09746acbeaf5321c000306ac994f32836
1/*
2 * Copyright (c) 1996, 2010, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package java.util.zip;
27
28import java.io.OutputStream;
29import java.io.IOException;
30import java.nio.charset.Charset;
31import java.nio.charset.StandardCharsets;
32import java.util.Vector;
33import java.util.HashSet;
34import static java.util.zip.ZipConstants64.*;
35
36/**
37 * This class implements an output stream filter for writing files in the
38 * ZIP file format. Includes support for both compressed and uncompressed
39 * entries.
40 *
41 * @author      David Connelly
42 */
43public
44class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
45
46    private static class XEntry {
47        public final ZipEntry entry;
48        public final long offset;
49        public XEntry(ZipEntry entry, long offset) {
50            this.entry = entry;
51            this.offset = offset;
52        }
53    }
54
55    private XEntry current;
56    private Vector<XEntry> xentries = new Vector<>();
57    private HashSet<String> names = new HashSet<>();
58    private CRC32 crc = new CRC32();
59    private long written = 0;
60    private long locoff = 0;
61    private byte[] comment;
62    private int method = DEFLATED;
63    private boolean finished;
64
65    private boolean closed = false;
66
67    private final ZipCoder zc;
68
69    private static int version(ZipEntry e) throws ZipException {
70        switch (e.method) {
71        case DEFLATED: return 20;
72        case STORED:   return 10;
73        default: throw new ZipException("unsupported compression method");
74        }
75    }
76
77    /**
78     * Checks to make sure that this stream has not been closed.
79     */
80    private void ensureOpen() throws IOException {
81        if (closed) {
82            throw new IOException("Stream closed");
83        }
84    }
85    /**
86     * Compression method for uncompressed (STORED) entries.
87     */
88    public static final int STORED = ZipEntry.STORED;
89
90    /**
91     * Compression method for compressed (DEFLATED) entries.
92     */
93    public static final int DEFLATED = ZipEntry.DEFLATED;
94
95    /**
96     * Creates a new ZIP output stream.
97     *
98     * <p>The UTF-8 {@link java.nio.charset.Charset charset} is used
99     * to encode the entry names and comments.
100     *
101     * @param out the actual output stream
102     */
103    public ZipOutputStream(OutputStream out) {
104        this(out, StandardCharsets.UTF_8);
105    }
106
107    /**
108     * Creates a new ZIP output stream.
109     *
110     * @param out the actual output stream
111     *
112     * @param charset the {@linkplain java.nio.charset.Charset charset}
113     *                to be used to encode the entry names and comments
114     *
115     * @since 1.7
116     */
117    public ZipOutputStream(OutputStream out, Charset charset) {
118        super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
119        if (charset == null)
120            throw new NullPointerException("charset is null");
121        this.zc = ZipCoder.get(charset);
122        usesDefaultDeflater = true;
123    }
124
125    /**
126     * Sets the ZIP file comment.
127     * @param comment the comment string
128     * @exception IllegalArgumentException if the length of the specified
129     *            ZIP file comment is greater than 0xFFFF bytes
130     */
131    public void setComment(String comment) {
132        if (comment != null) {
133            this.comment = zc.getBytes(comment);
134            if (this.comment.length > 0xffff)
135                throw new IllegalArgumentException("ZIP file comment too long.");
136        }
137    }
138
139    /**
140     * Sets the default compression method for subsequent entries. This
141     * default will be used whenever the compression method is not specified
142     * for an individual ZIP file entry, and is initially set to DEFLATED.
143     * @param method the default compression method
144     * @exception IllegalArgumentException if the specified compression method
145     *            is invalid
146     */
147    public void setMethod(int method) {
148        if (method != DEFLATED && method != STORED) {
149            throw new IllegalArgumentException("invalid compression method");
150        }
151        this.method = method;
152    }
153
154    /**
155     * Sets the compression level for subsequent entries which are DEFLATED.
156     * The default setting is DEFAULT_COMPRESSION.
157     * @param level the compression level (0-9)
158     * @exception IllegalArgumentException if the compression level is invalid
159     */
160    public void setLevel(int level) {
161        def.setLevel(level);
162    }
163
164    /**
165     * Begins writing a new ZIP file entry and positions the stream to the
166     * start of the entry data. Closes the current entry if still active.
167     * The default compression method will be used if no compression method
168     * was specified for the entry, and the current time will be used if
169     * the entry has no set modification time.
170     * @param e the ZIP entry to be written
171     * @exception ZipException if a ZIP format error has occurred
172     * @exception IOException if an I/O error has occurred
173     */
174    public void putNextEntry(ZipEntry e) throws IOException {
175        ensureOpen();
176        if (current != null) {
177            closeEntry();       // close previous entry
178        }
179        if (e.time == -1) {
180            e.setTime(System.currentTimeMillis());
181        }
182        if (e.method == -1) {
183            e.method = method;  // use default method
184        }
185        // store size, compressed size, and crc-32 in LOC header
186        e.flag = 0;
187        switch (e.method) {
188        case DEFLATED:
189            // store size, compressed size, and crc-32 in data descriptor
190            // immediately following the compressed entry data
191            if (e.size  == -1 || e.csize == -1 || e.crc   == -1)
192                e.flag = 8;
193
194            break;
195        case STORED:
196            // compressed size, uncompressed size, and crc-32 must all be
197            // set for entries using STORED compression method
198            if (e.size == -1) {
199                e.size = e.csize;
200            } else if (e.csize == -1) {
201                e.csize = e.size;
202            } else if (e.size != e.csize) {
203                throw new ZipException(
204                    "STORED entry where compressed != uncompressed size");
205            }
206            if (e.size == -1 || e.crc == -1) {
207                throw new ZipException(
208                    "STORED entry missing size, compressed size, or crc-32");
209            }
210            break;
211        default:
212            throw new ZipException("unsupported compression method");
213        }
214        if (! names.add(e.name)) {
215            throw new ZipException("duplicate entry: " + e.name);
216        }
217        if (zc.isUTF8())
218            e.flag |= EFS;
219        current = new XEntry(e, written);
220        xentries.add(current);
221        writeLOC(current);
222    }
223
224    /**
225     * Closes the current ZIP entry and positions the stream for writing
226     * the next entry.
227     * @exception ZipException if a ZIP format error has occurred
228     * @exception IOException if an I/O error has occurred
229     */
230    public void closeEntry() throws IOException {
231        ensureOpen();
232        if (current != null) {
233            ZipEntry e = current.entry;
234            switch (e.method) {
235            case DEFLATED:
236                def.finish();
237                while (!def.finished()) {
238                    deflate();
239                }
240                if ((e.flag & 8) == 0) {
241                    // verify size, compressed size, and crc-32 settings
242                    if (e.size != def.getBytesRead()) {
243                        throw new ZipException(
244                            "invalid entry size (expected " + e.size +
245                            " but got " + def.getBytesRead() + " bytes)");
246                    }
247                    if (e.csize != def.getBytesWritten()) {
248                        throw new ZipException(
249                            "invalid entry compressed size (expected " +
250                            e.csize + " but got " + def.getBytesWritten() + " bytes)");
251                    }
252                    if (e.crc != crc.getValue()) {
253                        throw new ZipException(
254                            "invalid entry CRC-32 (expected 0x" +
255                            Long.toHexString(e.crc) + " but got 0x" +
256                            Long.toHexString(crc.getValue()) + ")");
257                    }
258                } else {
259                    e.size  = def.getBytesRead();
260                    e.csize = def.getBytesWritten();
261                    e.crc = crc.getValue();
262                    writeEXT(e);
263                }
264                def.reset();
265                written += e.csize;
266                break;
267            case STORED:
268                // we already know that both e.size and e.csize are the same
269                if (e.size != written - locoff) {
270                    throw new ZipException(
271                        "invalid entry size (expected " + e.size +
272                        " but got " + (written - locoff) + " bytes)");
273                }
274                if (e.crc != crc.getValue()) {
275                    throw new ZipException(
276                         "invalid entry crc-32 (expected 0x" +
277                         Long.toHexString(e.crc) + " but got 0x" +
278                         Long.toHexString(crc.getValue()) + ")");
279                }
280                break;
281            default:
282                throw new ZipException("invalid compression method");
283            }
284            crc.reset();
285            current = null;
286        }
287    }
288
289    /**
290     * Writes an array of bytes to the current ZIP entry data. This method
291     * will block until all the bytes are written.
292     * @param b the data to be written
293     * @param off the start offset in the data
294     * @param len the number of bytes that are written
295     * @exception ZipException if a ZIP file error has occurred
296     * @exception IOException if an I/O error has occurred
297     */
298    public synchronized void write(byte[] b, int off, int len)
299        throws IOException
300    {
301        ensureOpen();
302        if (off < 0 || len < 0 || off > b.length - len) {
303            throw new IndexOutOfBoundsException();
304        } else if (len == 0) {
305            return;
306        }
307
308        if (current == null) {
309            throw new ZipException("no current ZIP entry");
310        }
311        ZipEntry entry = current.entry;
312        switch (entry.method) {
313        case DEFLATED:
314            super.write(b, off, len);
315            break;
316        case STORED:
317            written += len;
318            if (written - locoff > entry.size) {
319                throw new ZipException(
320                    "attempt to write past end of STORED entry");
321            }
322            out.write(b, off, len);
323            break;
324        default:
325            throw new ZipException("invalid compression method");
326        }
327        crc.update(b, off, len);
328    }
329
330    /**
331     * Finishes writing the contents of the ZIP output stream without closing
332     * the underlying stream. Use this method when applying multiple filters
333     * in succession to the same output stream.
334     * @exception ZipException if a ZIP file error has occurred
335     * @exception IOException if an I/O exception has occurred
336     */
337    public void finish() throws IOException {
338        ensureOpen();
339        if (finished) {
340            return;
341        }
342        if (xentries.isEmpty()) {
343            throw new ZipException("No entries");
344        }
345        if (current != null) {
346            closeEntry();
347        }
348
349        // write central directory
350        long off = written;
351        for (XEntry xentry : xentries)
352            writeCEN(xentry);
353        writeEND(off, written - off);
354        finished = true;
355    }
356
357    /**
358     * Closes the ZIP output stream as well as the stream being filtered.
359     * @exception ZipException if a ZIP file error has occurred
360     * @exception IOException if an I/O error has occurred
361     */
362    public void close() throws IOException {
363        if (!closed) {
364            super.close();
365            closed = true;
366        }
367    }
368
369    /*
370     * Writes local file (LOC) header for specified entry.
371     */
372    private void writeLOC(XEntry xentry) throws IOException {
373        ZipEntry e = xentry.entry;
374        int flag = e.flag;
375        int elen = (e.extra != null) ? e.extra.length : 0;
376        boolean hasZip64 = false;
377
378        writeInt(LOCSIG);               // LOC header signature
379
380        if ((flag & 8) == 8) {
381            writeShort(version(e));     // version needed to extract
382            writeShort(flag);           // general purpose bit flag
383            writeShort(e.method);       // compression method
384            writeInt(e.time);           // last modification time
385
386            // store size, uncompressed size, and crc-32 in data descriptor
387            // immediately following compressed entry data
388            writeInt(0);
389            writeInt(0);
390            writeInt(0);
391        } else {
392            if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) {
393                hasZip64 = true;
394                writeShort(45);         // ver 4.5 for zip64
395            } else {
396                writeShort(version(e)); // version needed to extract
397            }
398            writeShort(flag);           // general purpose bit flag
399            writeShort(e.method);       // compression method
400            writeInt(e.time);           // last modification time
401            writeInt(e.crc);            // crc-32
402            if (hasZip64) {
403                writeInt(ZIP64_MAGICVAL);
404                writeInt(ZIP64_MAGICVAL);
405                elen += 20;        //headid(2) + size(2) + size(8) + csize(8)
406            } else {
407                writeInt(e.csize);  // compressed size
408                writeInt(e.size);   // uncompressed size
409            }
410        }
411        byte[] nameBytes = zc.getBytes(e.name);
412        writeShort(nameBytes.length);
413        writeShort(elen);
414        writeBytes(nameBytes, 0, nameBytes.length);
415        if (hasZip64) {
416            writeShort(ZIP64_EXTID);
417            writeShort(16);
418            writeLong(e.size);
419            writeLong(e.csize);
420        }
421        if (e.extra != null) {
422            writeBytes(e.extra, 0, e.extra.length);
423        }
424        locoff = written;
425    }
426
427    /*
428     * Writes extra data descriptor (EXT) for specified entry.
429     */
430    private void writeEXT(ZipEntry e) throws IOException {
431        writeInt(EXTSIG);           // EXT header signature
432        writeInt(e.crc);            // crc-32
433        if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) {
434            writeLong(e.csize);
435            writeLong(e.size);
436        } else {
437            writeInt(e.csize);          // compressed size
438            writeInt(e.size);           // uncompressed size
439        }
440    }
441
442    /*
443     * Write central directory (CEN) header for specified entry.
444     * REMIND: add support for file attributes
445     */
446    private void writeCEN(XEntry xentry) throws IOException {
447        ZipEntry e  = xentry.entry;
448        int flag = e.flag;
449        int version = version(e);
450
451        long csize = e.csize;
452        long size = e.size;
453        long offset = xentry.offset;
454        int e64len = 0;
455        boolean hasZip64 = false;
456        if (e.csize >= ZIP64_MAGICVAL) {
457            csize = ZIP64_MAGICVAL;
458            e64len += 8;              // csize(8)
459            hasZip64 = true;
460        }
461        if (e.size >= ZIP64_MAGICVAL) {
462            size = ZIP64_MAGICVAL;    // size(8)
463            e64len += 8;
464            hasZip64 = true;
465        }
466        if (xentry.offset >= ZIP64_MAGICVAL) {
467            offset = ZIP64_MAGICVAL;
468            e64len += 8;              // offset(8)
469            hasZip64 = true;
470        }
471        writeInt(CENSIG);           // CEN header signature
472        if (hasZip64) {
473            writeShort(45);         // ver 4.5 for zip64
474            writeShort(45);
475        } else {
476            writeShort(version);    // version made by
477            writeShort(version);    // version needed to extract
478        }
479        writeShort(flag);           // general purpose bit flag
480        writeShort(e.method);       // compression method
481        writeInt(e.time);           // last modification time
482        writeInt(e.crc);            // crc-32
483        writeInt(csize);            // compressed size
484        writeInt(size);             // uncompressed size
485        byte[] nameBytes = zc.getBytes(e.name);
486        writeShort(nameBytes.length);
487        if (hasZip64) {
488            // + headid(2) + datasize(2)
489            writeShort(e64len + 4 + (e.extra != null ? e.extra.length : 0));
490        } else {
491            writeShort(e.extra != null ? e.extra.length : 0);
492        }
493        byte[] commentBytes;
494        if (e.comment != null) {
495            commentBytes = zc.getBytes(e.comment);
496            writeShort(Math.min(commentBytes.length, 0xffff));
497        } else {
498            commentBytes = null;
499            writeShort(0);
500        }
501        writeShort(0);              // starting disk number
502        writeShort(0);              // internal file attributes (unused)
503        writeInt(0);                // external file attributes (unused)
504        writeInt(offset);           // relative offset of local header
505        writeBytes(nameBytes, 0, nameBytes.length);
506        if (hasZip64) {
507            writeShort(ZIP64_EXTID);// Zip64 extra
508            writeShort(e64len);
509            if (size == ZIP64_MAGICVAL)
510                writeLong(e.size);
511            if (csize == ZIP64_MAGICVAL)
512                writeLong(e.csize);
513            if (offset == ZIP64_MAGICVAL)
514                writeLong(xentry.offset);
515        }
516        if (e.extra != null) {
517            writeBytes(e.extra, 0, e.extra.length);
518        }
519        if (commentBytes != null) {
520            writeBytes(commentBytes, 0, Math.min(commentBytes.length, 0xffff));
521        }
522    }
523
524    /*
525     * Writes end of central directory (END) header.
526     */
527    private void writeEND(long off, long len) throws IOException {
528        boolean hasZip64 = false;
529        long xlen = len;
530        long xoff = off;
531        if (xlen >= ZIP64_MAGICVAL) {
532            xlen = ZIP64_MAGICVAL;
533            hasZip64 = true;
534        }
535        if (xoff >= ZIP64_MAGICVAL) {
536            xoff = ZIP64_MAGICVAL;
537            hasZip64 = true;
538        }
539        int count = xentries.size();
540        if (count >= ZIP64_MAGICCOUNT) {
541            count = ZIP64_MAGICCOUNT;
542            hasZip64 = true;
543        }
544        if (hasZip64) {
545            long off64 = written;
546            //zip64 end of central directory record
547            writeInt(ZIP64_ENDSIG);        // zip64 END record signature
548            writeLong(ZIP64_ENDHDR - 12);  // size of zip64 end
549            writeShort(45);                // version made by
550            writeShort(45);                // version needed to extract
551            writeInt(0);                   // number of this disk
552            writeInt(0);                   // central directory start disk
553            writeLong(xentries.size());    // number of directory entires on disk
554            writeLong(xentries.size());    // number of directory entires
555            writeLong(len);                // length of central directory
556            writeLong(off);                // offset of central directory
557
558            //zip64 end of central directory locator
559            writeInt(ZIP64_LOCSIG);        // zip64 END locator signature
560            writeInt(0);                   // zip64 END start disk
561            writeLong(off64);              // offset of zip64 END
562            writeInt(1);                   // total number of disks (?)
563        }
564        writeInt(ENDSIG);                 // END record signature
565        writeShort(0);                    // number of this disk
566        writeShort(0);                    // central directory start disk
567        writeShort(count);                // number of directory entries on disk
568        writeShort(count);                // total number of directory entries
569        writeInt(xlen);                   // length of central directory
570        writeInt(xoff);                   // offset of central directory
571        if (comment != null) {            // zip file comment
572            writeShort(comment.length);
573            writeBytes(comment, 0, comment.length);
574        } else {
575            writeShort(0);
576        }
577    }
578
579    /*
580     * Writes a 16-bit short to the output stream in little-endian byte order.
581     */
582    private void writeShort(int v) throws IOException {
583        OutputStream out = this.out;
584        out.write((v >>> 0) & 0xff);
585        out.write((v >>> 8) & 0xff);
586        written += 2;
587    }
588
589    /*
590     * Writes a 32-bit int to the output stream in little-endian byte order.
591     */
592    private void writeInt(long v) throws IOException {
593        OutputStream out = this.out;
594        out.write((int)((v >>>  0) & 0xff));
595        out.write((int)((v >>>  8) & 0xff));
596        out.write((int)((v >>> 16) & 0xff));
597        out.write((int)((v >>> 24) & 0xff));
598        written += 4;
599    }
600
601    /*
602     * Writes a 64-bit int to the output stream in little-endian byte order.
603     */
604    private void writeLong(long v) throws IOException {
605        OutputStream out = this.out;
606        out.write((int)((v >>>  0) & 0xff));
607        out.write((int)((v >>>  8) & 0xff));
608        out.write((int)((v >>> 16) & 0xff));
609        out.write((int)((v >>> 24) & 0xff));
610        out.write((int)((v >>> 32) & 0xff));
611        out.write((int)((v >>> 40) & 0xff));
612        out.write((int)((v >>> 48) & 0xff));
613        out.write((int)((v >>> 56) & 0xff));
614        written += 8;
615    }
616
617    /*
618     * Writes an array of bytes to the output stream.
619     */
620    private void writeBytes(byte[] b, int off, int len) throws IOException {
621        super.out.write(b, off, len);
622        written += len;
623    }
624}
625