blob: 65394268297bec06604b818b07b4303db7dfe18d [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1996-2006 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
26package java.util.zip;
27
28import java.io.OutputStream;
29import java.io.IOException;
30import java.util.Vector;
31import java.util.HashSet;
32
33/**
34 * This class implements an output stream filter for writing files in the
35 * ZIP file format. Includes support for both compressed and uncompressed
36 * entries.
37 *
38 * @author David Connelly
39 */
40public
41class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
42
43 private static class XEntry {
44 public final ZipEntry entry;
45 public final long offset;
46 public final int flag;
47 public XEntry(ZipEntry entry, long offset) {
48 this.entry = entry;
49 this.offset = offset;
50 this.flag = (entry.method == DEFLATED &&
51 (entry.size == -1 ||
52 entry.csize == -1 ||
53 entry.crc == -1))
54 // store size, compressed size, and crc-32 in data descriptor
55 // immediately following the compressed entry data
56 ? 8
57 // store size, compressed size, and crc-32 in LOC header
58 : 0;
59 }
60 }
61
62 private XEntry current;
63 private Vector<XEntry> xentries = new Vector<XEntry>();
64 private HashSet<String> names = new HashSet<String>();
65 private CRC32 crc = new CRC32();
66 private long written = 0;
67 private long locoff = 0;
68 private String comment;
69 private int method = DEFLATED;
70 private boolean finished;
71
72 private boolean closed = false;
73
74 private static int version(ZipEntry e) throws ZipException {
75 switch (e.method) {
76 case DEFLATED: return 20;
77 case STORED: return 10;
78 default: throw new ZipException("unsupported compression method");
79 }
80 }
81
82 /**
83 * Checks to make sure that this stream has not been closed.
84 */
85 private void ensureOpen() throws IOException {
86 if (closed) {
87 throw new IOException("Stream closed");
88 }
89 }
90 /**
91 * Compression method for uncompressed (STORED) entries.
92 */
93 public static final int STORED = ZipEntry.STORED;
94
95 /**
96 * Compression method for compressed (DEFLATED) entries.
97 */
98 public static final int DEFLATED = ZipEntry.DEFLATED;
99
100 /**
101 * Creates a new ZIP output stream.
102 * @param out the actual output stream
103 */
104 public ZipOutputStream(OutputStream out) {
105 super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
106 usesDefaultDeflater = true;
107 }
108
109 /**
110 * Sets the ZIP file comment.
111 * @param comment the comment string
112 * @exception IllegalArgumentException if the length of the specified
113 * ZIP file comment is greater than 0xFFFF bytes
114 */
115 public void setComment(String comment) {
116 if (comment != null && comment.length() > 0xffff/3
117 && getUTF8Length(comment) > 0xffff) {
118 throw new IllegalArgumentException("ZIP file comment too long.");
119 }
120 this.comment = comment;
121 }
122
123 /**
124 * Sets the default compression method for subsequent entries. This
125 * default will be used whenever the compression method is not specified
126 * for an individual ZIP file entry, and is initially set to DEFLATED.
127 * @param method the default compression method
128 * @exception IllegalArgumentException if the specified compression method
129 * is invalid
130 */
131 public void setMethod(int method) {
132 if (method != DEFLATED && method != STORED) {
133 throw new IllegalArgumentException("invalid compression method");
134 }
135 this.method = method;
136 }
137
138 /**
139 * Sets the compression level for subsequent entries which are DEFLATED.
140 * The default setting is DEFAULT_COMPRESSION.
141 * @param level the compression level (0-9)
142 * @exception IllegalArgumentException if the compression level is invalid
143 */
144 public void setLevel(int level) {
145 def.setLevel(level);
146 }
147
148 /**
149 * Begins writing a new ZIP file entry and positions the stream to the
150 * start of the entry data. Closes the current entry if still active.
151 * The default compression method will be used if no compression method
152 * was specified for the entry, and the current time will be used if
153 * the entry has no set modification time.
154 * @param e the ZIP entry to be written
155 * @exception ZipException if a ZIP format error has occurred
156 * @exception IOException if an I/O error has occurred
157 */
158 public void putNextEntry(ZipEntry e) throws IOException {
159 ensureOpen();
160 if (current != null) {
161 closeEntry(); // close previous entry
162 }
163 if (e.time == -1) {
164 e.setTime(System.currentTimeMillis());
165 }
166 if (e.method == -1) {
167 e.method = method; // use default method
168 }
169 switch (e.method) {
170 case DEFLATED:
171 break;
172 case STORED:
173 // compressed size, uncompressed size, and crc-32 must all be
174 // set for entries using STORED compression method
175 if (e.size == -1) {
176 e.size = e.csize;
177 } else if (e.csize == -1) {
178 e.csize = e.size;
179 } else if (e.size != e.csize) {
180 throw new ZipException(
181 "STORED entry where compressed != uncompressed size");
182 }
183 if (e.size == -1 || e.crc == -1) {
184 throw new ZipException(
185 "STORED entry missing size, compressed size, or crc-32");
186 }
187 break;
188 default:
189 throw new ZipException("unsupported compression method");
190 }
191 if (! names.add(e.name)) {
192 throw new ZipException("duplicate entry: " + e.name);
193 }
194 current = new XEntry(e, written);
195 xentries.add(current);
196 writeLOC(current);
197 }
198
199 /**
200 * Closes the current ZIP entry and positions the stream for writing
201 * the next entry.
202 * @exception ZipException if a ZIP format error has occurred
203 * @exception IOException if an I/O error has occurred
204 */
205 public void closeEntry() throws IOException {
206 ensureOpen();
207 if (current != null) {
208 ZipEntry e = current.entry;
209 switch (e.method) {
210 case DEFLATED:
211 def.finish();
212 while (!def.finished()) {
213 deflate();
214 }
215 if ((current.flag & 8) == 0) {
216 // verify size, compressed size, and crc-32 settings
217 if (e.size != def.getBytesRead()) {
218 throw new ZipException(
219 "invalid entry size (expected " + e.size +
220 " but got " + def.getBytesRead() + " bytes)");
221 }
222 if (e.csize != def.getBytesWritten()) {
223 throw new ZipException(
224 "invalid entry compressed size (expected " +
225 e.csize + " but got " + def.getBytesWritten() + " bytes)");
226 }
227 if (e.crc != crc.getValue()) {
228 throw new ZipException(
229 "invalid entry CRC-32 (expected 0x" +
230 Long.toHexString(e.crc) + " but got 0x" +
231 Long.toHexString(crc.getValue()) + ")");
232 }
233 } else {
234 e.size = def.getBytesRead();
235 e.csize = def.getBytesWritten();
236 e.crc = crc.getValue();
237 writeEXT(e);
238 }
239 def.reset();
240 written += e.csize;
241 break;
242 case STORED:
243 // we already know that both e.size and e.csize are the same
244 if (e.size != written - locoff) {
245 throw new ZipException(
246 "invalid entry size (expected " + e.size +
247 " but got " + (written - locoff) + " bytes)");
248 }
249 if (e.crc != crc.getValue()) {
250 throw new ZipException(
251 "invalid entry crc-32 (expected 0x" +
252 Long.toHexString(e.crc) + " but got 0x" +
253 Long.toHexString(crc.getValue()) + ")");
254 }
255 break;
256 default:
257 throw new ZipException("invalid compression method");
258 }
259 crc.reset();
260 current = null;
261 }
262 }
263
264 /**
265 * Writes an array of bytes to the current ZIP entry data. This method
266 * will block until all the bytes are written.
267 * @param b the data to be written
268 * @param off the start offset in the data
269 * @param len the number of bytes that are written
270 * @exception ZipException if a ZIP file error has occurred
271 * @exception IOException if an I/O error has occurred
272 */
273 public synchronized void write(byte[] b, int off, int len)
274 throws IOException
275 {
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 (current == null) {
284 throw new ZipException("no current ZIP entry");
285 }
286 ZipEntry entry = current.entry;
287 switch (entry.method) {
288 case DEFLATED:
289 super.write(b, off, len);
290 break;
291 case STORED:
292 written += len;
293 if (written - locoff > entry.size) {
294 throw new ZipException(
295 "attempt to write past end of STORED entry");
296 }
297 out.write(b, off, len);
298 break;
299 default:
300 throw new ZipException("invalid compression method");
301 }
302 crc.update(b, off, len);
303 }
304
305 /**
306 * Finishes writing the contents of the ZIP output stream without closing
307 * the underlying stream. Use this method when applying multiple filters
308 * in succession to the same output stream.
309 * @exception ZipException if a ZIP file error has occurred
310 * @exception IOException if an I/O exception has occurred
311 */
312 public void finish() throws IOException {
313 ensureOpen();
314 if (finished) {
315 return;
316 }
317 if (current != null) {
318 closeEntry();
319 }
320 if (xentries.size() < 1) {
321 throw new ZipException("ZIP file must have at least one entry");
322 }
323 // write central directory
324 long off = written;
325 for (XEntry xentry : xentries)
326 writeCEN(xentry);
327 writeEND(off, written - off);
328 finished = true;
329 }
330
331 /**
332 * Closes the ZIP output stream as well as the stream being filtered.
333 * @exception ZipException if a ZIP file error has occurred
334 * @exception IOException if an I/O error has occurred
335 */
336 public void close() throws IOException {
337 if (!closed) {
338 super.close();
339 closed = true;
340 }
341 }
342
343 /*
344 * Writes local file (LOC) header for specified entry.
345 */
346 private void writeLOC(XEntry xentry) throws IOException {
347 ZipEntry e = xentry.entry;
348 int flag = xentry.flag;
349 writeInt(LOCSIG); // LOC header signature
350 writeShort(version(e)); // version needed to extract
351 writeShort(flag); // general purpose bit flag
352 writeShort(e.method); // compression method
353 writeInt(e.time); // last modification time
354 if ((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 * REMIND: add support for file attributes
388 */
389 private void writeCEN(XEntry xentry) throws IOException {
390 ZipEntry e = xentry.entry;
391 int flag = xentry.flag;
392 int version = version(e);
393 writeInt(CENSIG); // CEN header signature
394 writeShort(version); // version made by
395 writeShort(version); // version needed to extract
396 writeShort(flag); // general purpose bit flag
397 writeShort(e.method); // compression method
398 writeInt(e.time); // last modification time
399 writeInt(e.crc); // crc-32
400 writeInt(e.csize); // compressed size
401 writeInt(e.size); // uncompressed size
402 byte[] nameBytes = getUTF8Bytes(e.name);
403 writeShort(nameBytes.length);
404 writeShort(e.extra != null ? e.extra.length : 0);
405 byte[] commentBytes;
406 if (e.comment != null) {
407 commentBytes = getUTF8Bytes(e.comment);
408 writeShort(commentBytes.length);
409 } else {
410 commentBytes = null;
411 writeShort(0);
412 }
413 writeShort(0); // starting disk number
414 writeShort(0); // internal file attributes (unused)
415 writeInt(0); // external file attributes (unused)
416 writeInt(xentry.offset); // relative offset of local header
417 writeBytes(nameBytes, 0, nameBytes.length);
418 if (e.extra != null) {
419 writeBytes(e.extra, 0, e.extra.length);
420 }
421 if (commentBytes != null) {
422 writeBytes(commentBytes, 0, commentBytes.length);
423 }
424 }
425
426 /*
427 * Writes end of central directory (END) header.
428 */
429 private void writeEND(long off, long len) throws IOException {
430 int count = xentries.size();
431 writeInt(ENDSIG); // END record signature
432 writeShort(0); // number of this disk
433 writeShort(0); // central directory start disk
434 writeShort(count); // number of directory entries on disk
435 writeShort(count); // total number of directory entries
436 writeInt(len); // length of central directory
437 writeInt(off); // offset of central directory
438 if (comment != null) { // zip file comment
439 byte[] b = getUTF8Bytes(comment);
440 writeShort(b.length);
441 writeBytes(b, 0, b.length);
442 } else {
443 writeShort(0);
444 }
445 }
446
447 /*
448 * Writes a 16-bit short to the output stream in little-endian byte order.
449 */
450 private void writeShort(int v) throws IOException {
451 OutputStream out = this.out;
452 out.write((v >>> 0) & 0xff);
453 out.write((v >>> 8) & 0xff);
454 written += 2;
455 }
456
457 /*
458 * Writes a 32-bit int to the output stream in little-endian byte order.
459 */
460 private void writeInt(long v) throws IOException {
461 OutputStream out = this.out;
462 out.write((int)((v >>> 0) & 0xff));
463 out.write((int)((v >>> 8) & 0xff));
464 out.write((int)((v >>> 16) & 0xff));
465 out.write((int)((v >>> 24) & 0xff));
466 written += 4;
467 }
468
469 /*
470 * Writes an array of bytes to the output stream.
471 */
472 private void writeBytes(byte[] b, int off, int len) throws IOException {
473 super.out.write(b, off, len);
474 written += len;
475 }
476
477 /*
478 * Returns the length of String's UTF8 encoding.
479 */
480 static int getUTF8Length(String s) {
481 int count = 0;
482 for (int i = 0; i < s.length(); i++) {
483 char ch = s.charAt(i);
484 if (ch <= 0x7f) {
485 count++;
486 } else if (ch <= 0x7ff) {
487 count += 2;
488 } else {
489 count += 3;
490 }
491 }
492 return count;
493 }
494
495 /*
496 * Returns an array of bytes representing the UTF8 encoding
497 * of the specified String.
498 */
499 private static byte[] getUTF8Bytes(String s) {
500 char[] c = s.toCharArray();
501 int len = c.length;
502 // Count the number of encoded bytes...
503 int count = 0;
504 for (int i = 0; i < len; i++) {
505 int ch = c[i];
506 if (ch <= 0x7f) {
507 count++;
508 } else if (ch <= 0x7ff) {
509 count += 2;
510 } else {
511 count += 3;
512 }
513 }
514 // Now return the encoded bytes...
515 byte[] b = new byte[count];
516 int off = 0;
517 for (int i = 0; i < len; i++) {
518 int ch = c[i];
519 if (ch <= 0x7f) {
520 b[off++] = (byte)ch;
521 } else if (ch <= 0x7ff) {
522 b[off++] = (byte)((ch >> 6) | 0xc0);
523 b[off++] = (byte)((ch & 0x3f) | 0x80);
524 } else {
525 b[off++] = (byte)((ch >> 12) | 0xe0);
526 b[off++] = (byte)(((ch >> 6) & 0x3f) | 0x80);
527 b[off++] = (byte)((ch & 0x3f) | 0x80);
528 }
529 }
530 return b;
531 }
532}