ZipOutputStream.java revision d4f87eb09746acbeaf5321c000306ac994f32836
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 26package java.util.zip; 27 28import java.io.OutputStream; 29import java.io.IOException; 30import java.nio.charset.Charset; 31import java.nio.charset.StandardCharsets; 32import java.util.Vector; 33import java.util.HashSet; 34import 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 */ 43public 44class 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 (xentries.isEmpty()) { 343 throw new ZipException("No entries"); 344 } 345 if (current != null) { 346 closeEntry(); 347 } 348 349 // write central directory 350 long off = written; 351 for (XEntry xentry : xentries) 352 writeCEN(xentry); 353 writeEND(off, written - off); 354 finished = true; 355 } 356 357 /** 358 * Closes the ZIP output stream as well as the stream being filtered. 359 * @exception ZipException if a ZIP file error has occurred 360 * @exception IOException if an I/O error has occurred 361 */ 362 public void close() throws IOException { 363 if (!closed) { 364 super.close(); 365 closed = true; 366 } 367 } 368 369 /* 370 * Writes local file (LOC) header for specified entry. 371 */ 372 private void writeLOC(XEntry xentry) throws IOException { 373 ZipEntry e = xentry.entry; 374 int flag = e.flag; 375 int elen = (e.extra != null) ? e.extra.length : 0; 376 boolean hasZip64 = false; 377 378 writeInt(LOCSIG); // LOC header signature 379 380 if ((flag & 8) == 8) { 381 writeShort(version(e)); // version needed to extract 382 writeShort(flag); // general purpose bit flag 383 writeShort(e.method); // compression method 384 writeInt(e.time); // last modification time 385 386 // store size, uncompressed size, and crc-32 in data descriptor 387 // immediately following compressed entry data 388 writeInt(0); 389 writeInt(0); 390 writeInt(0); 391 } else { 392 if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) { 393 hasZip64 = true; 394 writeShort(45); // ver 4.5 for zip64 395 } else { 396 writeShort(version(e)); // version needed to extract 397 } 398 writeShort(flag); // general purpose bit flag 399 writeShort(e.method); // compression method 400 writeInt(e.time); // last modification time 401 writeInt(e.crc); // crc-32 402 if (hasZip64) { 403 writeInt(ZIP64_MAGICVAL); 404 writeInt(ZIP64_MAGICVAL); 405 elen += 20; //headid(2) + size(2) + size(8) + csize(8) 406 } else { 407 writeInt(e.csize); // compressed size 408 writeInt(e.size); // uncompressed size 409 } 410 } 411 byte[] nameBytes = zc.getBytes(e.name); 412 writeShort(nameBytes.length); 413 writeShort(elen); 414 writeBytes(nameBytes, 0, nameBytes.length); 415 if (hasZip64) { 416 writeShort(ZIP64_EXTID); 417 writeShort(16); 418 writeLong(e.size); 419 writeLong(e.csize); 420 } 421 if (e.extra != null) { 422 writeBytes(e.extra, 0, e.extra.length); 423 } 424 locoff = written; 425 } 426 427 /* 428 * Writes extra data descriptor (EXT) for specified entry. 429 */ 430 private void writeEXT(ZipEntry e) throws IOException { 431 writeInt(EXTSIG); // EXT header signature 432 writeInt(e.crc); // crc-32 433 if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) { 434 writeLong(e.csize); 435 writeLong(e.size); 436 } else { 437 writeInt(e.csize); // compressed size 438 writeInt(e.size); // uncompressed size 439 } 440 } 441 442 /* 443 * Write central directory (CEN) header for specified entry. 444 * REMIND: add support for file attributes 445 */ 446 private void writeCEN(XEntry xentry) throws IOException { 447 ZipEntry e = xentry.entry; 448 int flag = e.flag; 449 int version = version(e); 450 451 long csize = e.csize; 452 long size = e.size; 453 long offset = xentry.offset; 454 int e64len = 0; 455 boolean hasZip64 = false; 456 if (e.csize >= ZIP64_MAGICVAL) { 457 csize = ZIP64_MAGICVAL; 458 e64len += 8; // csize(8) 459 hasZip64 = true; 460 } 461 if (e.size >= ZIP64_MAGICVAL) { 462 size = ZIP64_MAGICVAL; // size(8) 463 e64len += 8; 464 hasZip64 = true; 465 } 466 if (xentry.offset >= ZIP64_MAGICVAL) { 467 offset = ZIP64_MAGICVAL; 468 e64len += 8; // offset(8) 469 hasZip64 = true; 470 } 471 writeInt(CENSIG); // CEN header signature 472 if (hasZip64) { 473 writeShort(45); // ver 4.5 for zip64 474 writeShort(45); 475 } else { 476 writeShort(version); // version made by 477 writeShort(version); // version needed to extract 478 } 479 writeShort(flag); // general purpose bit flag 480 writeShort(e.method); // compression method 481 writeInt(e.time); // last modification time 482 writeInt(e.crc); // crc-32 483 writeInt(csize); // compressed size 484 writeInt(size); // uncompressed size 485 byte[] nameBytes = zc.getBytes(e.name); 486 writeShort(nameBytes.length); 487 if (hasZip64) { 488 // + headid(2) + datasize(2) 489 writeShort(e64len + 4 + (e.extra != null ? e.extra.length : 0)); 490 } else { 491 writeShort(e.extra != null ? e.extra.length : 0); 492 } 493 byte[] commentBytes; 494 if (e.comment != null) { 495 commentBytes = zc.getBytes(e.comment); 496 writeShort(Math.min(commentBytes.length, 0xffff)); 497 } else { 498 commentBytes = null; 499 writeShort(0); 500 } 501 writeShort(0); // starting disk number 502 writeShort(0); // internal file attributes (unused) 503 writeInt(0); // external file attributes (unused) 504 writeInt(offset); // relative offset of local header 505 writeBytes(nameBytes, 0, nameBytes.length); 506 if (hasZip64) { 507 writeShort(ZIP64_EXTID);// Zip64 extra 508 writeShort(e64len); 509 if (size == ZIP64_MAGICVAL) 510 writeLong(e.size); 511 if (csize == ZIP64_MAGICVAL) 512 writeLong(e.csize); 513 if (offset == ZIP64_MAGICVAL) 514 writeLong(xentry.offset); 515 } 516 if (e.extra != null) { 517 writeBytes(e.extra, 0, e.extra.length); 518 } 519 if (commentBytes != null) { 520 writeBytes(commentBytes, 0, Math.min(commentBytes.length, 0xffff)); 521 } 522 } 523 524 /* 525 * Writes end of central directory (END) header. 526 */ 527 private void writeEND(long off, long len) throws IOException { 528 boolean hasZip64 = false; 529 long xlen = len; 530 long xoff = off; 531 if (xlen >= ZIP64_MAGICVAL) { 532 xlen = ZIP64_MAGICVAL; 533 hasZip64 = true; 534 } 535 if (xoff >= ZIP64_MAGICVAL) { 536 xoff = ZIP64_MAGICVAL; 537 hasZip64 = true; 538 } 539 int count = xentries.size(); 540 if (count >= ZIP64_MAGICCOUNT) { 541 count = ZIP64_MAGICCOUNT; 542 hasZip64 = true; 543 } 544 if (hasZip64) { 545 long off64 = written; 546 //zip64 end of central directory record 547 writeInt(ZIP64_ENDSIG); // zip64 END record signature 548 writeLong(ZIP64_ENDHDR - 12); // size of zip64 end 549 writeShort(45); // version made by 550 writeShort(45); // version needed to extract 551 writeInt(0); // number of this disk 552 writeInt(0); // central directory start disk 553 writeLong(xentries.size()); // number of directory entires on disk 554 writeLong(xentries.size()); // number of directory entires 555 writeLong(len); // length of central directory 556 writeLong(off); // offset of central directory 557 558 //zip64 end of central directory locator 559 writeInt(ZIP64_LOCSIG); // zip64 END locator signature 560 writeInt(0); // zip64 END start disk 561 writeLong(off64); // offset of zip64 END 562 writeInt(1); // total number of disks (?) 563 } 564 writeInt(ENDSIG); // END record signature 565 writeShort(0); // number of this disk 566 writeShort(0); // central directory start disk 567 writeShort(count); // number of directory entries on disk 568 writeShort(count); // total number of directory entries 569 writeInt(xlen); // length of central directory 570 writeInt(xoff); // offset of central directory 571 if (comment != null) { // zip file comment 572 writeShort(comment.length); 573 writeBytes(comment, 0, comment.length); 574 } else { 575 writeShort(0); 576 } 577 } 578 579 /* 580 * Writes a 16-bit short to the output stream in little-endian byte order. 581 */ 582 private void writeShort(int v) throws IOException { 583 OutputStream out = this.out; 584 out.write((v >>> 0) & 0xff); 585 out.write((v >>> 8) & 0xff); 586 written += 2; 587 } 588 589 /* 590 * Writes a 32-bit int to the output stream in little-endian byte order. 591 */ 592 private void writeInt(long v) throws IOException { 593 OutputStream out = this.out; 594 out.write((int)((v >>> 0) & 0xff)); 595 out.write((int)((v >>> 8) & 0xff)); 596 out.write((int)((v >>> 16) & 0xff)); 597 out.write((int)((v >>> 24) & 0xff)); 598 written += 4; 599 } 600 601 /* 602 * Writes a 64-bit int to the output stream in little-endian byte order. 603 */ 604 private void writeLong(long v) throws IOException { 605 OutputStream out = this.out; 606 out.write((int)((v >>> 0) & 0xff)); 607 out.write((int)((v >>> 8) & 0xff)); 608 out.write((int)((v >>> 16) & 0xff)); 609 out.write((int)((v >>> 24) & 0xff)); 610 out.write((int)((v >>> 32) & 0xff)); 611 out.write((int)((v >>> 40) & 0xff)); 612 out.write((int)((v >>> 48) & 0xff)); 613 out.write((int)((v >>> 56) & 0xff)); 614 written += 8; 615 } 616 617 /* 618 * Writes an array of bytes to the output stream. 619 */ 620 private void writeBytes(byte[] b, int off, int len) throws IOException { 621 super.out.write(b, off, len); 622 written += len; 623 } 624} 625