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 }