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