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