001/* ZipOutputStream.java --
002   Copyright (C) 2001, 2004, 2005, 2006  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package java.util.zip;
040
041import java.io.IOException;
042import java.io.OutputStream;
043import java.io.UnsupportedEncodingException;
044import java.util.Enumeration;
045import java.util.Vector;
046
047/**
048 * This is a FilterOutputStream that writes the files into a zip
049 * archive one after another.  It has a special method to start a new
050 * zip entry.  The zip entries contains information about the file name
051 * size, compressed size, CRC, etc.
052 *
053 * It includes support for STORED and DEFLATED entries.
054 *
055 * This class is not thread safe.
056 *
057 * @author Jochen Hoenicke 
058 */
059public class ZipOutputStream extends DeflaterOutputStream implements ZipConstants
060{
061  private Vector entries = new Vector();
062  private CRC32 crc = new CRC32();
063  private ZipEntry curEntry = null;
064
065  private int curMethod;
066  private int size;
067  private int offset = 0;
068
069  private byte[] zipComment = new byte[0];
070  private int defaultMethod = DEFLATED;
071
072  /**
073   * Our Zip version is hard coded to 1.0 resp. 2.0
074   */
075  private static final int ZIP_STORED_VERSION = 10;
076  private static final int ZIP_DEFLATED_VERSION = 20;
077
078  /**
079   * Compression method.  This method doesn't compress at all.
080   */
081  public static final int STORED = 0;
082  
083  /**
084   * Compression method.  This method uses the Deflater.
085   */
086  public static final int DEFLATED = 8;
087
088  /**
089   * Creates a new Zip output stream, writing a zip archive.
090   * @param out the output stream to which the zip archive is written.
091   */
092  public ZipOutputStream(OutputStream out)
093  {
094    super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
095  }
096
097  /**
098   * Set the zip file comment.
099   * @param comment the comment.
100   * @exception IllegalArgumentException if encoding of comment is
101   * longer than 0xffff bytes.
102   */
103  public void setComment(String comment)
104  {
105    byte[] commentBytes;
106    try
107      {
108        commentBytes = comment.getBytes("UTF-8");
109      }
110    catch (UnsupportedEncodingException uee)
111      {
112        throw new AssertionError(uee);
113      }
114    if (commentBytes.length > 0xffff)
115      throw new IllegalArgumentException("Comment too long.");
116    zipComment = commentBytes;
117  }
118  
119  /**
120   * Sets default compression method.  If the Zip entry specifies
121   * another method its method takes precedence.
122   * @param method the method.
123   * @exception IllegalArgumentException if method is not supported.
124   * @see #STORED
125   * @see #DEFLATED
126   */
127  public void setMethod(int method)
128  {
129    if (method != STORED && method != DEFLATED)
130      throw new IllegalArgumentException("Method not supported.");
131    defaultMethod = method;
132  }
133
134  /**
135   * Sets default compression level.  The new level will be activated
136   * immediately.  
137   * @exception IllegalArgumentException if level is not supported.
138   * @see Deflater
139   */
140  public void setLevel(int level)
141  {
142    def.setLevel(level);
143  }
144  
145  /**
146   * Write an unsigned short in little endian byte order.
147   */
148  private void writeLeShort(int value) throws IOException 
149  {
150    out.write(value & 0xff);
151    out.write((value >> 8) & 0xff);
152  }
153
154  /**
155   * Write an int in little endian byte order.
156   */
157  private void writeLeInt(int value) throws IOException 
158  {
159    writeLeShort(value);
160    writeLeShort(value >> 16);
161  }
162
163  /**
164   * Write a long value as an int.  Some of the zip constants
165   * are declared as longs even though they fit perfectly well
166   * into integers.
167   */
168  private void writeLeInt(long value) throws IOException
169  {
170    writeLeInt((int) value);
171  }
172
173  /**
174   * Starts a new Zip entry. It automatically closes the previous
175   * entry if present.  If the compression method is stored, the entry
176   * must have a valid size and crc, otherwise all elements (except
177   * name) are optional, but must be correct if present.  If the time
178   * is not set in the entry, the current time is used.
179   * @param entry the entry.
180   * @exception IOException if an I/O error occured.
181   * @exception ZipException if stream was finished.
182   */
183  public void putNextEntry(ZipEntry entry) throws IOException
184  {
185    if (entries == null)
186      throw new ZipException("ZipOutputStream was finished");
187
188    int method = entry.getMethod();
189    int flags = 0;
190    if (method == -1)
191      method = defaultMethod;
192
193    if (method == STORED)
194      {
195        if (entry.getCompressedSize() >= 0)
196          {
197            if (entry.getSize() < 0)
198              entry.setSize(entry.getCompressedSize());
199            else if (entry.getSize() != entry.getCompressedSize())
200              throw new ZipException
201                ("Method STORED, but compressed size != size");
202          }
203        else
204          entry.setCompressedSize(entry.getSize());
205
206        if (entry.getSize() < 0)
207          throw new ZipException("Method STORED, but size not set");
208        if (entry.getCrc() < 0)
209          throw new ZipException("Method STORED, but crc not set");
210      }
211    else if (method == DEFLATED)
212      {
213        if (entry.getCompressedSize() < 0
214            || entry.getSize() < 0 || entry.getCrc() < 0)
215          flags |= 8;
216      }
217
218    if (curEntry != null)
219      closeEntry();
220
221    if (entry.getTime() < 0)
222      entry.setTime(System.currentTimeMillis());
223
224    entry.flags = flags;
225    entry.offset = offset;
226    entry.setMethod(method);
227    curMethod = method;
228    /* Write the local file header */
229    writeLeInt(LOCSIG);
230    writeLeShort(method == STORED
231                 ? ZIP_STORED_VERSION : ZIP_DEFLATED_VERSION);
232    writeLeShort(flags);
233    writeLeShort(method);
234    writeLeInt(entry.getDOSTime());
235    if ((flags & 8) == 0)
236      {
237        writeLeInt((int)entry.getCrc());
238        writeLeInt((int)entry.getCompressedSize());
239        writeLeInt((int)entry.getSize());
240      }
241    else
242      {
243        writeLeInt(0);
244        writeLeInt(0);
245        writeLeInt(0);
246      }
247    byte[] name;
248    try
249      {
250        name = entry.getName().getBytes("UTF-8");
251      }
252    catch (UnsupportedEncodingException uee)
253      {
254        throw new AssertionError(uee);
255      }
256    if (name.length > 0xffff)
257      throw new ZipException("Name too long.");
258    byte[] extra = entry.getExtra();
259    if (extra == null)
260      extra = new byte[0];
261    writeLeShort(name.length);
262    writeLeShort(extra.length);
263    out.write(name);
264    out.write(extra);
265
266    offset += LOCHDR + name.length + extra.length;
267
268    /* Activate the entry. */
269
270    curEntry = entry;
271    crc.reset();
272    if (method == DEFLATED)
273      def.reset();
274    size = 0;
275  }
276
277  /**
278   * Closes the current entry.
279   * @exception IOException if an I/O error occured.
280   * @exception ZipException if no entry is active.
281   */
282  public void closeEntry() throws IOException
283  {
284    if (curEntry == null)
285      throw new ZipException("No open entry");
286
287    /* First finish the deflater, if appropriate */
288    if (curMethod == DEFLATED)
289      super.finish();
290
291    int csize = curMethod == DEFLATED ? def.getTotalOut() : size;
292
293    if (curEntry.getSize() < 0)
294      curEntry.setSize(size);
295    else if (curEntry.getSize() != size)
296      throw new ZipException("size was "+size
297                             +", but I expected "+curEntry.getSize());
298
299    if (curEntry.getCompressedSize() < 0)
300      curEntry.setCompressedSize(csize);
301    else if (curEntry.getCompressedSize() != csize)
302      throw new ZipException("compressed size was "+csize
303                             +", but I expected "+curEntry.getSize());
304
305    if (curEntry.getCrc() < 0)
306      curEntry.setCrc(crc.getValue());
307    else if (curEntry.getCrc() != crc.getValue())
308      throw new ZipException("crc was " + Long.toHexString(crc.getValue())
309                             + ", but I expected " 
310                             + Long.toHexString(curEntry.getCrc()));
311
312    offset += csize;
313
314    /* Now write the data descriptor entry if needed. */
315    if (curMethod == DEFLATED && (curEntry.flags & 8) != 0)
316      {
317        writeLeInt(EXTSIG);
318        writeLeInt((int)curEntry.getCrc());
319        writeLeInt((int)curEntry.getCompressedSize());
320        writeLeInt((int)curEntry.getSize());
321        offset += EXTHDR;
322      }
323
324    entries.addElement(curEntry);
325    curEntry = null;
326  }
327
328  /**
329   * Writes the given buffer to the current entry.
330   * @exception IOException if an I/O error occured.
331   * @exception ZipException if no entry is active.
332   */
333  public void write(byte[] b, int off, int len) throws IOException
334  {
335    if (curEntry == null)
336      throw new ZipException("No open entry.");
337
338    switch (curMethod)
339      {
340      case DEFLATED:
341        super.write(b, off, len);
342        break;
343        
344      case STORED:
345        out.write(b, off, len);
346        break;
347      }
348
349    crc.update(b, off, len);
350    size += len;
351  }
352
353  /**
354   * Finishes the stream.  This will write the central directory at the
355   * end of the zip file and flush the stream.
356   * @exception IOException if an I/O error occured.
357   */
358  public void finish() throws IOException
359  {
360    if (entries == null)
361      return;
362    if (curEntry != null)
363      closeEntry();
364
365    int numEntries = 0;
366    int sizeEntries = 0;
367    
368    Enumeration e = entries.elements();
369    while (e.hasMoreElements())
370      {
371        ZipEntry entry = (ZipEntry) e.nextElement();
372        
373        int method = entry.getMethod();
374        writeLeInt(CENSIG);
375        writeLeShort(method == STORED
376                     ? ZIP_STORED_VERSION : ZIP_DEFLATED_VERSION);
377        writeLeShort(method == STORED
378                     ? ZIP_STORED_VERSION : ZIP_DEFLATED_VERSION);
379        writeLeShort(entry.flags);
380        writeLeShort(method);
381        writeLeInt(entry.getDOSTime());
382        writeLeInt((int)entry.getCrc());
383        writeLeInt((int)entry.getCompressedSize());
384        writeLeInt((int)entry.getSize());
385
386        byte[] name;
387        try
388          {
389            name = entry.getName().getBytes("UTF-8");
390          }
391        catch (UnsupportedEncodingException uee)
392          {
393            throw new AssertionError(uee);
394          }
395        if (name.length > 0xffff)
396          throw new ZipException("Name too long.");
397        byte[] extra = entry.getExtra();
398        if (extra == null)
399          extra = new byte[0];
400        String str = entry.getComment();
401        byte[] comment;
402        try
403          {
404            comment = str != null ? str.getBytes("UTF-8") : new byte[0];
405          }
406        catch (UnsupportedEncodingException uee)
407          {
408            throw new AssertionError(uee);
409          }
410        if (comment.length > 0xffff)
411          throw new ZipException("Comment too long.");
412
413        writeLeShort(name.length);
414        writeLeShort(extra.length);
415        writeLeShort(comment.length);
416        writeLeShort(0); /* disk number */
417        writeLeShort(0); /* internal file attr */
418        writeLeInt(0);   /* external file attr */
419        writeLeInt(entry.offset);
420
421        out.write(name);
422        out.write(extra);
423        out.write(comment);
424        numEntries++;
425        sizeEntries += CENHDR + name.length + extra.length + comment.length;
426      }
427
428    writeLeInt(ENDSIG);
429    writeLeShort(0); /* disk number */
430    writeLeShort(0); /* disk with start of central dir */
431    writeLeShort(numEntries);
432    writeLeShort(numEntries);
433    writeLeInt(sizeEntries);
434    writeLeInt(offset);
435    writeLeShort(zipComment.length);
436    out.write(zipComment);
437    out.flush();
438    entries = null;
439  }
440}