001 /*
002 * Copyright 1996-2006 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025
026 package java.util.zip;
027
028 import java.io.OutputStream;
029 import java.io.IOException;
030 import java.util.Vector;
031 import java.util.HashSet;
032
033 /**
034 * This class implements an output stream filter for writing files in the
035 * ZIP file format. Includes support for both compressed and uncompressed
036 * entries.
037 *
038 * @author David Connelly
039 * @version 1.41, 05/05/07
040 */
041 public class ZipOutputStream extends DeflaterOutputStream implements
042 ZipConstants {
043
044 private static class XEntry {
045 public final ZipEntry entry;
046 public final long offset;
047 public final int flag;
048
049 public XEntry(ZipEntry entry, long offset) {
050 this .entry = entry;
051 this .offset = offset;
052 this .flag = (entry.method == DEFLATED && (entry.size == -1
053 || entry.csize == -1 || entry.crc == -1))
054 // store size, compressed size, and crc-32 in data descriptor
055 // immediately following the compressed entry data
056 ? 8
057 // store size, compressed size, and crc-32 in LOC header
058 : 0;
059 }
060 }
061
062 private XEntry current;
063 private Vector<XEntry> xentries = new Vector<XEntry>();
064 private HashSet<String> names = new HashSet<String>();
065 private CRC32 crc = new CRC32();
066 private long written = 0;
067 private long locoff = 0;
068 private String comment;
069 private int method = DEFLATED;
070 private boolean finished;
071
072 private boolean closed = false;
073
074 private static int version(ZipEntry e) throws ZipException {
075 switch (e.method) {
076 case DEFLATED:
077 return 20;
078 case STORED:
079 return 10;
080 default:
081 throw new ZipException("unsupported compression method");
082 }
083 }
084
085 /**
086 * Checks to make sure that this stream has not been closed.
087 */
088 private void ensureOpen() throws IOException {
089 if (closed) {
090 throw new IOException("Stream closed");
091 }
092 }
093
094 /**
095 * Compression method for uncompressed (STORED) entries.
096 */
097 public static final int STORED = ZipEntry.STORED;
098
099 /**
100 * Compression method for compressed (DEFLATED) entries.
101 */
102 public static final int DEFLATED = ZipEntry.DEFLATED;
103
104 /**
105 * Creates a new ZIP output stream.
106 * @param out the actual output stream
107 */
108 public ZipOutputStream(OutputStream out) {
109 super (out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
110 usesDefaultDeflater = true;
111 }
112
113 /**
114 * Sets the ZIP file comment.
115 * @param comment the comment string
116 * @exception IllegalArgumentException if the length of the specified
117 * ZIP file comment is greater than 0xFFFF bytes
118 */
119 public void setComment(String comment) {
120 if (comment != null && comment.length() > 0xffff / 3
121 && getUTF8Length(comment) > 0xffff) {
122 throw new IllegalArgumentException(
123 "ZIP file comment too long.");
124 }
125 this .comment = comment;
126 }
127
128 /**
129 * Sets the default compression method for subsequent entries. This
130 * default will be used whenever the compression method is not specified
131 * for an individual ZIP file entry, and is initially set to DEFLATED.
132 * @param method the default compression method
133 * @exception IllegalArgumentException if the specified compression method
134 * is invalid
135 */
136 public void setMethod(int method) {
137 if (method != DEFLATED && method != STORED) {
138 throw new IllegalArgumentException(
139 "invalid compression method");
140 }
141 this .method = method;
142 }
143
144 /**
145 * Sets the compression level for subsequent entries which are DEFLATED.
146 * The default setting is DEFAULT_COMPRESSION.
147 * @param level the compression level (0-9)
148 * @exception IllegalArgumentException if the compression level is invalid
149 */
150 public void setLevel(int level) {
151 def.setLevel(level);
152 }
153
154 /**
155 * Begins writing a new ZIP file entry and positions the stream to the
156 * start of the entry data. Closes the current entry if still active.
157 * The default compression method will be used if no compression method
158 * was specified for the entry, and the current time will be used if
159 * the entry has no set modification time.
160 * @param e the ZIP entry to be written
161 * @exception ZipException if a ZIP format error has occurred
162 * @exception IOException if an I/O error has occurred
163 */
164 public void putNextEntry(ZipEntry e) throws IOException {
165 ensureOpen();
166 if (current != null) {
167 closeEntry(); // close previous entry
168 }
169 if (e.time == -1) {
170 e.setTime(System.currentTimeMillis());
171 }
172 if (e.method == -1) {
173 e.method = method; // use default method
174 }
175 switch (e.method) {
176 case DEFLATED:
177 break;
178 case STORED:
179 // compressed size, uncompressed size, and crc-32 must all be
180 // set for entries using STORED compression method
181 if (e.size == -1) {
182 e.size = e.csize;
183 } else if (e.csize == -1) {
184 e.csize = e.size;
185 } else if (e.size != e.csize) {
186 throw new ZipException(
187 "STORED entry where compressed != uncompressed size");
188 }
189 if (e.size == -1 || e.crc == -1) {
190 throw new ZipException(
191 "STORED entry missing size, compressed size, or crc-32");
192 }
193 break;
194 default:
195 throw new ZipException("unsupported compression method");
196 }
197 if (!names.add(e.name)) {
198 throw new ZipException("duplicate entry: " + e.name);
199 }
200 current = new XEntry(e, written);
201 xentries.add(current);
202 writeLOC(current);
203 }
204
205 /**
206 * Closes the current ZIP entry and positions the stream for writing
207 * the next entry.
208 * @exception ZipException if a ZIP format error has occurred
209 * @exception IOException if an I/O error has occurred
210 */
211 public void closeEntry() throws IOException {
212 ensureOpen();
213 if (current != null) {
214 ZipEntry e = current.entry;
215 switch (e.method) {
216 case DEFLATED:
217 def.finish();
218 while (!def.finished()) {
219 deflate();
220 }
221 if ((current.flag & 8) == 0) {
222 // verify size, compressed size, and crc-32 settings
223 if (e.size != def.getBytesRead()) {
224 throw new ZipException(
225 "invalid entry size (expected "
226 + e.size + " but got "
227 + def.getBytesRead()
228 + " bytes)");
229 }
230 if (e.csize != def.getBytesWritten()) {
231 throw new ZipException(
232 "invalid entry compressed size (expected "
233 + e.csize + " but got "
234 + def.getBytesWritten()
235 + " bytes)");
236 }
237 if (e.crc != crc.getValue()) {
238 throw new ZipException(
239 "invalid entry CRC-32 (expected 0x"
240 + Long.toHexString(e.crc)
241 + " but got 0x"
242 + Long.toHexString(crc
243 .getValue()) + ")");
244 }
245 } else {
246 e.size = def.getBytesRead();
247 e.csize = def.getBytesWritten();
248 e.crc = crc.getValue();
249 writeEXT(e);
250 }
251 def.reset();
252 written += e.csize;
253 break;
254 case STORED:
255 // we already know that both e.size and e.csize are the same
256 if (e.size != written - locoff) {
257 throw new ZipException(
258 "invalid entry size (expected " + e.size
259 + " but got " + (written - locoff)
260 + " bytes)");
261 }
262 if (e.crc != crc.getValue()) {
263 throw new ZipException(
264 "invalid entry crc-32 (expected 0x"
265 + Long.toHexString(e.crc)
266 + " but got 0x"
267 + Long.toHexString(crc.getValue())
268 + ")");
269 }
270 break;
271 default:
272 throw new ZipException("invalid compression method");
273 }
274 crc.reset();
275 current = null;
276 }
277 }
278
279 /**
280 * Writes an array of bytes to the current ZIP entry data. This method
281 * will block until all the bytes are written.
282 * @param b the data to be written
283 * @param off the start offset in the data
284 * @param len the number of bytes that are written
285 * @exception ZipException if a ZIP file error has occurred
286 * @exception IOException if an I/O error has occurred
287 */
288 public synchronized void write(byte[] b, int off, int len)
289 throws IOException {
290 ensureOpen();
291 if (off < 0 || len < 0 || off > b.length - len) {
292 throw new IndexOutOfBoundsException();
293 } else if (len == 0) {
294 return;
295 }
296
297 if (current == null) {
298 throw new ZipException("no current ZIP entry");
299 }
300 ZipEntry entry = current.entry;
301 switch (entry.method) {
302 case DEFLATED:
303 super .write(b, off, len);
304 break;
305 case STORED:
306 written += len;
307 if (written - locoff > entry.size) {
308 throw new ZipException(
309 "attempt to write past end of STORED entry");
310 }
311 out.write(b, off, len);
312 break;
313 default:
314 throw new ZipException("invalid compression method");
315 }
316 crc.update(b, off, len);
317 }
318
319 /**
320 * Finishes writing the contents of the ZIP output stream without closing
321 * the underlying stream. Use this method when applying multiple filters
322 * in succession to the same output stream.
323 * @exception ZipException if a ZIP file error has occurred
324 * @exception IOException if an I/O exception has occurred
325 */
326 public void finish() throws IOException {
327 ensureOpen();
328 if (finished) {
329 return;
330 }
331 if (current != null) {
332 closeEntry();
333 }
334 if (xentries.size() < 1) {
335 throw new ZipException(
336 "ZIP file must have at least one entry");
337 }
338 // write central directory
339 long off = written;
340 for (XEntry xentry : xentries)
341 writeCEN(xentry);
342 writeEND(off, written - off);
343 finished = true;
344 }
345
346 /**
347 * Closes the ZIP output stream as well as the stream being filtered.
348 * @exception ZipException if a ZIP file error has occurred
349 * @exception IOException if an I/O error has occurred
350 */
351 public void close() throws IOException {
352 if (!closed) {
353 super .close();
354 closed = true;
355 }
356 }
357
358 /*
359 * Writes local file (LOC) header for specified entry.
360 */
361 private void writeLOC(XEntry xentry) throws IOException {
362 ZipEntry e = xentry.entry;
363 int flag = xentry.flag;
364 writeInt(LOCSIG); // LOC header signature
365 writeShort(version(e)); // version needed to extract
366 writeShort(flag); // general purpose bit flag
367 writeShort(e.method); // compression method
368 writeInt(e.time); // last modification time
369 if ((flag & 8) == 8) {
370 // store size, uncompressed size, and crc-32 in data descriptor
371 // immediately following compressed entry data
372 writeInt(0);
373 writeInt(0);
374 writeInt(0);
375 } else {
376 writeInt(e.crc); // crc-32
377 writeInt(e.csize); // compressed size
378 writeInt(e.size); // uncompressed size
379 }
380 byte[] nameBytes = getUTF8Bytes(e.name);
381 writeShort(nameBytes.length);
382 writeShort(e.extra != null ? e.extra.length : 0);
383 writeBytes(nameBytes, 0, nameBytes.length);
384 if (e.extra != null) {
385 writeBytes(e.extra, 0, e.extra.length);
386 }
387 locoff = written;
388 }
389
390 /*
391 * Writes extra data descriptor (EXT) for specified entry.
392 */
393 private void writeEXT(ZipEntry e) throws IOException {
394 writeInt(EXTSIG); // EXT header signature
395 writeInt(e.crc); // crc-32
396 writeInt(e.csize); // compressed size
397 writeInt(e.size); // uncompressed size
398 }
399
400 /*
401 * Write central directory (CEN) header for specified entry.
402 * REMIND: add support for file attributes
403 */
404 private void writeCEN(XEntry xentry) throws IOException {
405 ZipEntry e = xentry.entry;
406 int flag = xentry.flag;
407 int version = version(e);
408 writeInt(CENSIG); // CEN header signature
409 writeShort(version); // version made by
410 writeShort(version); // version needed to extract
411 writeShort(flag); // general purpose bit flag
412 writeShort(e.method); // compression method
413 writeInt(e.time); // last modification time
414 writeInt(e.crc); // crc-32
415 writeInt(e.csize); // compressed size
416 writeInt(e.size); // uncompressed size
417 byte[] nameBytes = getUTF8Bytes(e.name);
418 writeShort(nameBytes.length);
419 writeShort(e.extra != null ? e.extra.length : 0);
420 byte[] commentBytes;
421 if (e.comment != null) {
422 commentBytes = getUTF8Bytes(e.comment);
423 writeShort(commentBytes.length);
424 } else {
425 commentBytes = null;
426 writeShort(0);
427 }
428 writeShort(0); // starting disk number
429 writeShort(0); // internal file attributes (unused)
430 writeInt(0); // external file attributes (unused)
431 writeInt(xentry.offset); // relative offset of local header
432 writeBytes(nameBytes, 0, nameBytes.length);
433 if (e.extra != null) {
434 writeBytes(e.extra, 0, e.extra.length);
435 }
436 if (commentBytes != null) {
437 writeBytes(commentBytes, 0, commentBytes.length);
438 }
439 }
440
441 /*
442 * Writes end of central directory (END) header.
443 */
444 private void writeEND(long off, long len) throws IOException {
445 int count = xentries.size();
446 writeInt(ENDSIG); // END record signature
447 writeShort(0); // number of this disk
448 writeShort(0); // central directory start disk
449 writeShort(count); // number of directory entries on disk
450 writeShort(count); // total number of directory entries
451 writeInt(len); // length of central directory
452 writeInt(off); // offset of central directory
453 if (comment != null) { // zip file comment
454 byte[] b = getUTF8Bytes(comment);
455 writeShort(b.length);
456 writeBytes(b, 0, b.length);
457 } else {
458 writeShort(0);
459 }
460 }
461
462 /*
463 * Writes a 16-bit short to the output stream in little-endian byte order.
464 */
465 private void writeShort(int v) throws IOException {
466 OutputStream out = this .out;
467 out.write((v >>> 0) & 0xff);
468 out.write((v >>> 8) & 0xff);
469 written += 2;
470 }
471
472 /*
473 * Writes a 32-bit int to the output stream in little-endian byte order.
474 */
475 private void writeInt(long v) throws IOException {
476 OutputStream out = this .out;
477 out.write((int) ((v >>> 0) & 0xff));
478 out.write((int) ((v >>> 8) & 0xff));
479 out.write((int) ((v >>> 16) & 0xff));
480 out.write((int) ((v >>> 24) & 0xff));
481 written += 4;
482 }
483
484 /*
485 * Writes an array of bytes to the output stream.
486 */
487 private void writeBytes(byte[] b, int off, int len)
488 throws IOException {
489 super .out.write(b, off, len);
490 written += len;
491 }
492
493 /*
494 * Returns the length of String's UTF8 encoding.
495 */
496 static int getUTF8Length(String s) {
497 int count = 0;
498 for (int i = 0; i < s.length(); i++) {
499 char ch = s.charAt(i);
500 if (ch <= 0x7f) {
501 count++;
502 } else if (ch <= 0x7ff) {
503 count += 2;
504 } else {
505 count += 3;
506 }
507 }
508 return count;
509 }
510
511 /*
512 * Returns an array of bytes representing the UTF8 encoding
513 * of the specified String.
514 */
515 private static byte[] getUTF8Bytes(String s) {
516 char[] c = s.toCharArray();
517 int len = c.length;
518 // Count the number of encoded bytes...
519 int count = 0;
520 for (int i = 0; i < len; i++) {
521 int ch = c[i];
522 if (ch <= 0x7f) {
523 count++;
524 } else if (ch <= 0x7ff) {
525 count += 2;
526 } else {
527 count += 3;
528 }
529 }
530 // Now return the encoded bytes...
531 byte[] b = new byte[count];
532 int off = 0;
533 for (int i = 0; i < len; i++) {
534 int ch = c[i];
535 if (ch <= 0x7f) {
536 b[off++] = (byte) ch;
537 } else if (ch <= 0x7ff) {
538 b[off++] = (byte) ((ch >> 6) | 0xc0);
539 b[off++] = (byte) ((ch & 0x3f) | 0x80);
540 } else {
541 b[off++] = (byte) ((ch >> 12) | 0xe0);
542 b[off++] = (byte) (((ch >> 6) & 0x3f) | 0x80);
543 b[off++] = (byte) ((ch & 0x3f) | 0x80);
544 }
545 }
546 return b;
547 }
548 }
|