1   /*
2    * %W% %E%
3    *
4    * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
5    * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6    */
7   
8   package java.util.zip;
9   
10  import java.io.OutputStream;
11  import java.io.IOException;
12  import java.util.Vector;
13  import java.util.HashSet;
14  
15  /**
16   * This class implements an output stream filter for writing files in the
17   * ZIP file format. Includes support for both compressed and uncompressed
18   * entries.
19   *
20   * @author  David Connelly
21   * @version %I%, %G%
22   */
23  public
24  class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
25  
26      private static class XEntry {
27      public final ZipEntry entry;
28      public final long offset;
29      public final int flag;
30      public XEntry(ZipEntry entry, long offset) {
31          this.entry = entry;
32          this.offset = offset;
33          this.flag = (entry.method == DEFLATED &&
34               (entry.size  == -1 ||
35                entry.csize == -1 ||
36                entry.crc   == -1))
37          // store size, compressed size, and crc-32 in data descriptor
38          // immediately following the compressed entry data
39          ? 8
40          // store size, compressed size, and crc-32 in LOC header
41          : 0;
42      }
43      }
44  
45      private XEntry current;
46      private Vector<XEntry> xentries = new Vector<XEntry>();
47      private HashSet<String> names = new HashSet<String>();
48      private CRC32 crc = new CRC32();
49      private long written = 0;
50      private long locoff = 0;
51      private String comment;
52      private int method = DEFLATED;
53      private boolean finished;
54  
55      private boolean closed = false;
56  
57      private static int version(ZipEntry e) throws ZipException {
58      switch (e.method) {
59      case DEFLATED: return 20;
60      case STORED:   return 10;
61      default: throw new ZipException("unsupported compression method");
62      }
63      }
64  
65      /**
66       * Checks to make sure that this stream has not been closed.
67       */
68      private void ensureOpen() throws IOException {
69      if (closed) {
70          throw new IOException("Stream closed");
71          }
72      }
73      /**
74       * Compression method for uncompressed (STORED) entries.
75       */
76      public static final int STORED = ZipEntry.STORED;
77  
78      /**
79       * Compression method for compressed (DEFLATED) entries.
80       */
81      public static final int DEFLATED = ZipEntry.DEFLATED;
82  
83      /**
84       * Creates a new ZIP output stream.
85       * @param out the actual output stream
86       */
87      public ZipOutputStream(OutputStream out) {
88      super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
89          usesDefaultDeflater = true;
90      }
91  
92      /**
93       * Sets the ZIP file comment.
94       * @param comment the comment string
95       * @exception IllegalArgumentException if the length of the specified
96       *        ZIP file comment is greater than 0xFFFF bytes
97       */
98      public void setComment(String comment) {
99          if (comment != null && comment.length() > 0xffff/3
100                                            && getUTF8Length(comment) > 0xffff) {
101         throw new IllegalArgumentException("ZIP file comment too long.");
102     }
103     this.comment = comment;
104     }
105 
106     /**
107      * Sets the default compression method for subsequent entries. This
108      * default will be used whenever the compression method is not specified
109      * for an individual ZIP file entry, and is initially set to DEFLATED.
110      * @param method the default compression method
111      * @exception IllegalArgumentException if the specified compression method
112      *        is invalid
113      */
114     public void setMethod(int method) {
115     if (method != DEFLATED && method != STORED) {
116         throw new IllegalArgumentException("invalid compression method");
117     }
118     this.method = method;
119     }
120 
121     /**
122      * Sets the compression level for subsequent entries which are DEFLATED.
123      * The default setting is DEFAULT_COMPRESSION.
124      * @param level the compression level (0-9)
125      * @exception IllegalArgumentException if the compression level is invalid
126      */
127     public void setLevel(int level) {
128     def.setLevel(level);
129     }
130 
131     /**
132      * Begins writing a new ZIP file entry and positions the stream to the
133      * start of the entry data. Closes the current entry if still active.
134      * The default compression method will be used if no compression method
135      * was specified for the entry, and the current time will be used if
136      * the entry has no set modification time.
137      * @param e the ZIP entry to be written
138      * @exception ZipException if a ZIP format error has occurred
139      * @exception IOException if an I/O error has occurred
140      */
141     public void putNextEntry(ZipEntry e) throws IOException {
142     ensureOpen();
143     if (current != null) {
144         closeEntry();   // close previous entry
145     }
146     if (e.time == -1) {
147         e.setTime(System.currentTimeMillis());
148     }
149     if (e.method == -1) {
150         e.method = method;  // use default method
151     }
152     switch (e.method) {
153     case DEFLATED:
154         break;
155     case STORED:
156         // compressed size, uncompressed size, and crc-32 must all be
157         // set for entries using STORED compression method
158         if (e.size == -1) {
159         e.size = e.csize;
160         } else if (e.csize == -1) {
161         e.csize = e.size;
162         } else if (e.size != e.csize) {
163         throw new ZipException(
164             "STORED entry where compressed != uncompressed size");
165         }
166         if (e.size == -1 || e.crc == -1) {
167         throw new ZipException(
168             "STORED entry missing size, compressed size, or crc-32");
169         }
170         break;
171     default:
172         throw new ZipException("unsupported compression method");
173     }
174     if (! names.add(e.name)) {
175         throw new ZipException("duplicate entry: " + e.name);
176     }
177     current = new XEntry(e, written);
178     xentries.add(current);
179         writeLOC(current);
180     }
181 
182     /**
183      * Closes the current ZIP entry and positions the stream for writing
184      * the next entry.
185      * @exception ZipException if a ZIP format error has occurred
186      * @exception IOException if an I/O error has occurred
187      */
188     public void closeEntry() throws IOException {
189     ensureOpen();
190     if (current != null) {
191         ZipEntry e = current.entry;
192         switch (e.method) {
193         case DEFLATED:
194         def.finish();
195         while (!def.finished()) {
196             deflate();
197         }
198         if ((current.flag & 8) == 0) {
199             // verify size, compressed size, and crc-32 settings
200             if (e.size != def.getBytesRead()) {
201             throw new ZipException(
202                 "invalid entry size (expected " + e.size +
203                 " but got " + def.getBytesRead() + " bytes)");
204             }
205             if (e.csize != def.getBytesWritten()) {
206             throw new ZipException(
207                 "invalid entry compressed size (expected " +
208                 e.csize + " but got " + def.getBytesWritten() + " bytes)");
209             }
210             if (e.crc != crc.getValue()) {
211             throw new ZipException(
212                 "invalid entry CRC-32 (expected 0x" +
213                 Long.toHexString(e.crc) + " but got 0x" +
214                 Long.toHexString(crc.getValue()) + ")");
215             }
216         } else {
217             e.size  = def.getBytesRead();
218             e.csize = def.getBytesWritten();
219             e.crc = crc.getValue();
220             writeEXT(e);
221         }
222         def.reset();
223         written += e.csize;
224         break;
225         case STORED:
226         // we already know that both e.size and e.csize are the same
227         if (e.size != written - locoff) {
228             throw new ZipException(
229             "invalid entry size (expected " + e.size +
230             " but got " + (written - locoff) + " bytes)");
231         }
232         if (e.crc != crc.getValue()) {
233             throw new ZipException(
234              "invalid entry crc-32 (expected 0x" +
235              Long.toHexString(e.crc) + " but got 0x" +
236              Long.toHexString(crc.getValue()) + ")");
237         }
238         break;
239         default:
240         throw new ZipException("invalid compression method");
241         }
242         crc.reset();
243         current = null;
244     }
245     }
246 
247     /**
248      * Writes an array of bytes to the current ZIP entry data. This method
249      * will block until all the bytes are written.
250      * @param b the data to be written
251      * @param off the start offset in the data
252      * @param len the number of bytes that are written
253      * @exception ZipException if a ZIP file error has occurred
254      * @exception IOException if an I/O error has occurred
255      */
256     public synchronized void write(byte[] b, int off, int len)
257     throws IOException
258     {
259     ensureOpen();
260         if (off < 0 || len < 0 || off > b.length - len) {
261         throw new IndexOutOfBoundsException();
262     } else if (len == 0) {
263         return;
264     }
265 
266     if (current == null) {
267         throw new ZipException("no current ZIP entry");
268     }
269     ZipEntry entry = current.entry;
270     switch (entry.method) {
271     case DEFLATED:
272         super.write(b, off, len);
273         break;
274     case STORED:
275         written += len;
276         if (written - locoff > entry.size) {
277         throw new ZipException(
278             "attempt to write past end of STORED entry");
279         }
280         out.write(b, off, len);
281         break;
282     default:
283         throw new ZipException("invalid compression method");
284     }
285     crc.update(b, off, len);
286     }
287 
288     /**
289      * Finishes writing the contents of the ZIP output stream without closing
290      * the underlying stream. Use this method when applying multiple filters
291      * in succession to the same output stream.
292      * @exception ZipException if a ZIP file error has occurred
293      * @exception IOException if an I/O exception has occurred
294      */
295     public void finish() throws IOException {
296     ensureOpen();
297     if (finished) {
298         return;
299     }
300     if (current != null) {
301         closeEntry();
302     }
303     if (xentries.size() < 1) {
304         throw new ZipException("ZIP file must have at least one entry");
305     }
306     // write central directory
307     long off = written;
308     for (XEntry xentry : xentries)
309         writeCEN(xentry);
310     writeEND(off, written - off);
311     finished = true;
312     }
313 
314     /**
315      * Closes the ZIP output stream as well as the stream being filtered.
316      * @exception ZipException if a ZIP file error has occurred
317      * @exception IOException if an I/O error has occurred
318      */
319     public void close() throws IOException {
320         if (!closed) {
321             super.close();
322             closed = true;
323         }
324     }
325 
326     /*
327      * Writes local file (LOC) header for specified entry.
328      */
329     private void writeLOC(XEntry xentry) throws IOException {
330     ZipEntry e = xentry.entry;
331     int flag = xentry.flag;
332     writeInt(LOCSIG);       // LOC header signature
333     writeShort(version(e));     // version needed to extract
334     writeShort(flag);           // general purpose bit flag
335     writeShort(e.method);       // compression method
336     writeInt(e.time);           // last modification time
337     if ((flag & 8) == 8) {
338         // store size, uncompressed size, and crc-32 in data descriptor
339         // immediately following compressed entry data
340         writeInt(0);
341         writeInt(0);
342         writeInt(0);
343     } else {
344         writeInt(e.crc);        // crc-32
345         writeInt(e.csize);      // compressed size
346         writeInt(e.size);       // uncompressed size
347     }
348     byte[] nameBytes = getUTF8Bytes(e.name);
349     writeShort(nameBytes.length);
350     writeShort(e.extra != null ? e.extra.length : 0);
351     writeBytes(nameBytes, 0, nameBytes.length);
352     if (e.extra != null) {
353         writeBytes(e.extra, 0, e.extra.length);
354     }
355     locoff = written;
356     }
357 
358     /*
359      * Writes extra data descriptor (EXT) for specified entry.
360      */
361     private void writeEXT(ZipEntry e) throws IOException {
362     writeInt(EXTSIG);       // EXT header signature
363     writeInt(e.crc);        // crc-32
364     writeInt(e.csize);      // compressed size
365     writeInt(e.size);       // uncompressed size
366     }
367 
368     /*
369      * Write central directory (CEN) header for specified entry.
370      * REMIND: add support for file attributes
371      */
372     private void writeCEN(XEntry xentry) throws IOException {
373     ZipEntry e  = xentry.entry;
374     int flag = xentry.flag;
375     int version = version(e);
376     writeInt(CENSIG);       // CEN header signature
377     writeShort(version);        // version made by
378     writeShort(version);        // version needed to extract
379     writeShort(flag);       // general purpose bit flag
380     writeShort(e.method);       // compression method
381     writeInt(e.time);       // last modification time
382     writeInt(e.crc);        // crc-32
383     writeInt(e.csize);      // compressed size
384     writeInt(e.size);       // uncompressed size
385     byte[] nameBytes = getUTF8Bytes(e.name);
386     writeShort(nameBytes.length);
387     writeShort(e.extra != null ? e.extra.length : 0);
388     byte[] commentBytes;
389     if (e.comment != null) {
390         commentBytes = getUTF8Bytes(e.comment);
391         writeShort(commentBytes.length);
392     } else {
393         commentBytes = null;
394         writeShort(0);
395     }
396     writeShort(0);          // starting disk number
397     writeShort(0);          // internal file attributes (unused)
398     writeInt(0);            // external file attributes (unused)
399     writeInt(xentry.offset);    // relative offset of local header
400     writeBytes(nameBytes, 0, nameBytes.length);
401     if (e.extra != null) {
402         writeBytes(e.extra, 0, e.extra.length);
403     }
404     if (commentBytes != null) {
405         writeBytes(commentBytes, 0, commentBytes.length);
406     }
407     }
408 
409     /*
410      * Writes end of central directory (END) header.
411      */
412     private void writeEND(long off, long len) throws IOException {
413     int count = xentries.size();
414     writeInt(ENDSIG);       // END record signature
415     writeShort(0);          // number of this disk
416     writeShort(0);          // central directory start disk
417     writeShort(count);      // number of directory entries on disk
418     writeShort(count);      // total number of directory entries
419     writeInt(len);          // length of central directory
420     writeInt(off);          // offset of central directory
421     if (comment != null) {      // zip file comment
422         byte[] b = getUTF8Bytes(comment);
423         writeShort(b.length);
424         writeBytes(b, 0, b.length);
425     } else {
426         writeShort(0);
427     }
428     }
429 
430     /*
431      * Writes a 16-bit short to the output stream in little-endian byte order.
432      */
433     private void writeShort(int v) throws IOException {
434     OutputStream out = this.out;
435     out.write((v >>> 0) & 0xff);
436     out.write((v >>> 8) & 0xff);
437     written += 2;
438     }
439 
440     /*
441      * Writes a 32-bit int to the output stream in little-endian byte order.
442      */
443     private void writeInt(long v) throws IOException {
444     OutputStream out = this.out;
445     out.write((int)((v >>>  0) & 0xff));
446     out.write((int)((v >>>  8) & 0xff));
447     out.write((int)((v >>> 16) & 0xff));
448     out.write((int)((v >>> 24) & 0xff));
449     written += 4;
450     }
451 
452     /*
453      * Writes an array of bytes to the output stream.
454      */
455     private void writeBytes(byte[] b, int off, int len) throws IOException {
456     super.out.write(b, off, len);
457     written += len;
458     }
459 
460     /*
461      * Returns the length of String's UTF8 encoding.
462      */
463     static int getUTF8Length(String s) {
464         int count = 0;
465         for (int i = 0; i < s.length(); i++) {
466             char ch = s.charAt(i);
467             if (ch <= 0x7f) {
468                 count++;
469             } else if (ch <= 0x7ff) {
470                 count += 2;
471             } else {
472                 count += 3;
473             }
474         }
475         return count;
476     }
477 
478     /*
479      * Returns an array of bytes representing the UTF8 encoding
480      * of the specified String.
481      */
482     private static byte[] getUTF8Bytes(String s) {
483     char[] c = s.toCharArray();
484     int len = c.length;
485     // Count the number of encoded bytes...
486     int count = 0;
487     for (int i = 0; i < len; i++) {
488         int ch = c[i];
489         if (ch <= 0x7f) {
490         count++;
491         } else if (ch <= 0x7ff) {
492         count += 2;
493         } else {
494         count += 3;
495         }
496     }
497     // Now return the encoded bytes...
498     byte[] b = new byte[count];
499     int off = 0;
500     for (int i = 0; i < len; i++) {
501         int ch = c[i];
502         if (ch <= 0x7f) {
503         b[off++] = (byte)ch;
504         } else if (ch <= 0x7ff) {
505         b[off++] = (byte)((ch >> 6) | 0xc0);
506         b[off++] = (byte)((ch & 0x3f) | 0x80);
507         } else {
508         b[off++] = (byte)((ch >> 12) | 0xe0);
509         b[off++] = (byte)(((ch >> 6) & 0x3f) | 0x80);
510         b[off++] = (byte)((ch & 0x3f) | 0x80);
511         }
512     }
513     return b;
514     }
515 }
516