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