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