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