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