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