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 }