001    /* ZipOutputStream.java --
002       Copyright (C) 2001, 2004, 2005, 2006  Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package java.util.zip;
040    
041    import java.io.IOException;
042    import java.io.OutputStream;
043    import java.io.UnsupportedEncodingException;
044    import java.util.Enumeration;
045    import 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     */
059    public 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    }