2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
19 package org.apache.tools.zip;
22 import java.io.FileOutputStream;
23 import java.io.FilterOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.io.RandomAccessFile;
27 import java.io.UnsupportedEncodingException;
28 import java.util.Date;
29 import java.util.Hashtable;
30 import java.util.Vector;
31 import java.util.zip.CRC32;
32 import java.util.zip.Deflater;
33 import java.util.zip.ZipException;
36 * Reimplementation of {@link java.util.zip.ZipOutputStream
37 * java.util.zip.ZipOutputStream} that does handle the extended
38 * functionality of this package, especially internal/external file
39 * attributes and extra fields with different layouts for local file
40 * data and central directory entries.
42 * <p>This class will try to use {@link java.io.RandomAccessFile
43 * RandomAccessFile} when you know that the output is going to go to a
46 * <p>If RandomAccessFile cannot be used, this implementation will use
47 * a Data Descriptor to store size and CRC information for {@link
48 * #DEFLATED DEFLATED} entries, this means, you don't need to
49 * calculate them yourself. Unfortunately this is not possible for
50 * the {@link #STORED STORED} method, here setting the CRC and
51 * uncompressed size information is required before {@link
52 * #putNextEntry putNextEntry} can be called.</p>
55 public class ZipOutputStream extends FilterOutputStream {
58 * Compression method for deflated entries.
62 public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
65 * Default compression level for deflated entries.
69 public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
72 * Compression method for stored entries.
76 public static final int STORED = java.util.zip.ZipEntry.STORED;
83 private ZipEntry entry;
90 private String comment = "";
93 * Compression level for next entry.
97 private int level = DEFAULT_COMPRESSION;
100 * Has the compression level changed when compared to the last
105 private boolean hasCompressionLevelChanged = false;
108 * Default compression method for next entry.
112 private int method = java.util.zip.ZipEntry.DEFLATED;
115 * List of ZipEntries written so far.
119 private Vector entries = new Vector();
122 * CRC instance to avoid parsing DEFLATED data twice.
126 private CRC32 crc = new CRC32();
129 * Count the bytes written to out.
133 private long written = 0;
136 * Data for local header data
140 private long dataStart = 0;
143 * Offset for CRC entry in the local file header data for the
144 * current entry starts here.
148 private long localDataStart = 0;
151 * Start of central directory.
155 private long cdOffset = 0;
158 * Length of central directory.
162 private long cdLength = 0;
165 * Helper, a 0 as ZipShort.
169 private static final byte[] ZERO = {0, 0};
172 * Helper, a 0 as ZipLong.
176 private static final byte[] LZERO = {0, 0, 0, 0};
179 * Holds the offsets of the LFH starts for each entry.
183 private Hashtable offsets = new Hashtable();
186 * The encoding to use for filenames and the file comment.
188 * <p>For a list of possible values see <a
189 * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
190 * Defaults to the platform's default character encoding.</p>
194 private String encoding = null;
196 // CheckStyle:VisibilityModifier OFF - bc
199 * This Deflater object is used for output.
201 * <p>This attribute is only protected to provide a level of API
202 * backwards compatibility. This class used to extend {@link
203 * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
208 protected Deflater def = new Deflater(level, true);
211 * This buffer servers as a Deflater.
213 * <p>This attribute is only protected to provide a level of API
214 * backwards compatibility. This class used to extend {@link
215 * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
220 protected byte[] buf = new byte[512];
222 // CheckStyle:VisibilityModifier ON
225 * Optional random access output.
229 private RandomAccessFile raf = null;
232 * Creates a new ZIP OutputStream filtering the underlying stream.
233 * @param out the outputstream to zip
236 public ZipOutputStream(OutputStream out) {
241 * Creates a new ZIP OutputStream writing to a File. Will use
242 * random access if possible.
243 * @param file the file to zip to
245 * @throws IOException on error
247 public ZipOutputStream(File file) throws IOException {
251 raf = new RandomAccessFile(file, "rw");
253 } catch (IOException e) {
257 } catch (IOException inner) {
262 out = new FileOutputStream(file);
267 * This method indicates whether this archive is writing to a seekable stream (i.e., to a random
270 * <p>For seekable streams, you don't need to calculate the CRC or
271 * uncompressed size for {@link #STORED} entries before
272 * invoking {@link #putNextEntry}.
273 * @return true if seekable
276 public boolean isSeekable() {
281 * The encoding to use for filenames and the file comment.
283 * <p>For a list of possible values see <a
284 * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
285 * Defaults to the platform's default character encoding.</p>
286 * @param encoding the encoding value
289 public void setEncoding(String encoding) {
290 this.encoding = encoding;
294 * The encoding to use for filenames and the file comment.
296 * @return null if using the platform's default character encoding.
300 public String getEncoding() {
305 * Finishs writing the contents and closes this as well as the
309 * @throws IOException on error
311 public void finish() throws IOException {
314 for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) {
315 writeCentralFileHeader((ZipEntry) entries.elementAt(i));
317 cdLength = written - cdOffset;
318 writeCentralDirectoryEnd();
320 entries.removeAllElements();
324 * Writes all necessary data for this entry.
327 * @throws IOException on error
329 public void closeEntry() throws IOException {
334 long realCrc = crc.getValue();
337 if (entry.getMethod() == DEFLATED) {
339 while (!def.finished()) {
343 entry.setSize(adjustToLong(def.getTotalIn()));
344 entry.setCompressedSize(adjustToLong(def.getTotalOut()));
345 entry.setCrc(realCrc);
349 written += entry.getCompressedSize();
350 } else if (raf == null) {
351 if (entry.getCrc() != realCrc) {
352 throw new ZipException("bad CRC checksum for entry "
353 + entry.getName() + ": "
354 + Long.toHexString(entry.getCrc())
356 + Long.toHexString(realCrc));
359 if (entry.getSize() != written - dataStart) {
360 throw new ZipException("bad size for entry "
361 + entry.getName() + ": "
364 + (written - dataStart));
366 } else { /* method is STORED and we used RandomAccessFile */
367 long size = written - dataStart;
370 entry.setCompressedSize(size);
371 entry.setCrc(realCrc);
374 // If random access output, write the local file header containing
375 // the correct CRC and compressed/uncompressed sizes
377 long save = raf.getFilePointer();
379 raf.seek(localDataStart);
380 writeOut(ZipLong.getBytes(entry.getCrc()));
381 writeOut(ZipLong.getBytes(entry.getCompressedSize()));
382 writeOut(ZipLong.getBytes(entry.getSize()));
386 writeDataDescriptor(entry);
391 * Begin writing next entry.
392 * @param ze the entry to write
394 * @throws IOException on error
396 public void putNextEntry(ZipEntry ze) throws IOException {
400 entries.addElement(entry);
402 if (entry.getMethod() == -1) { // not specified
403 entry.setMethod(method);
406 if (entry.getTime() == -1) { // not specified
407 entry.setTime(System.currentTimeMillis());
410 // Size/CRC not required if RandomAccessFile is used
411 if (entry.getMethod() == STORED && raf == null) {
412 if (entry.getSize() == -1) {
413 throw new ZipException("uncompressed size is required for"
414 + " STORED method when not writing to a"
417 if (entry.getCrc() == -1) {
418 throw new ZipException("crc checksum is required for STORED"
419 + " method when not writing to a file");
421 entry.setCompressedSize(entry.getSize());
424 if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
426 hasCompressionLevelChanged = false;
428 writeLocalFileHeader(entry);
432 * Set the file comment.
433 * @param comment the comment
436 public void setComment(String comment) {
437 this.comment = comment;
441 * Sets the compression level for subsequent entries.
443 * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
444 * @param level the compression level.
445 * @throws IllegalArgumentException if an invalid compression level is specified.
448 public void setLevel(int level) {
449 if (level < Deflater.DEFAULT_COMPRESSION
450 || level > Deflater.BEST_COMPRESSION) {
451 throw new IllegalArgumentException(
452 "Invalid compression level: " + level);
454 hasCompressionLevelChanged = (this.level != level);
459 * Sets the default compression method for subsequent entries.
461 * <p>Default is DEFLATED.</p>
462 * @param method an <code>int</code> from java.util.zip.ZipEntry
465 public void setMethod(int method) {
466 this.method = method;
470 * Writes bytes to ZIP entry.
471 * @param b the byte array to write
472 * @param offset the start position to write from
473 * @param length the number of bytes to write
474 * @throws IOException on error
476 public void write(byte[] b, int offset, int length) throws IOException {
477 if (entry.getMethod() == DEFLATED) {
479 if (!def.finished()) {
480 def.setInput(b, offset, length);
481 while (!def.needsInput()) {
487 writeOut(b, offset, length);
490 crc.update(b, offset, length);
494 * Writes a single byte to ZIP entry.
496 * <p>Delegates to the three arg method.</p>
497 * @param b the byte to write
499 * @throws IOException on error
501 public void write(int b) throws IOException {
502 byte[] buff = new byte[1];
503 buff[0] = (byte) (b & 0xff);
508 * Closes this output stream and releases any system resources
509 * associated with the stream.
511 * @exception IOException if an I/O error occurs.
514 public void close() throws IOException {
526 * Flushes this output stream and forces any buffered output bytes
527 * to be written out to the stream.
529 * @exception IOException if an I/O error occurs.
532 public void flush() throws IOException {
539 * Various ZIP constants
542 * local file header signature
546 protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);
548 * data descriptor signature
552 protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
554 * central file header signature
558 protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);
560 * end of central dir signature
564 protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
567 * Writes next block of compressed data to the output stream.
568 * @throws IOException on error
572 protected final void deflate() throws IOException {
573 int len = def.deflate(buf, 0, buf.length);
575 writeOut(buf, 0, len);
580 * Writes the local file header entry
581 * @param ze the entry to write
582 * @throws IOException on error
586 protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
587 offsets.put(ze, ZipLong.getBytes(written));
592 //store method in local variable to prevent multiple method calls
593 final int zipMethod = ze.getMethod();
595 // version needed to extract
596 // general purpose bit flag
597 if (zipMethod == DEFLATED && raf == null) {
598 // requires version 2 as we are going to store length info
599 // in the data descriptor
600 writeOut(ZipShort.getBytes(20));
602 // bit3 set to signal, we use a data descriptor
603 writeOut(ZipShort.getBytes(8));
605 writeOut(ZipShort.getBytes(10));
610 // compression method
611 writeOut(ZipShort.getBytes(zipMethod));
614 // last mod. time and date
615 writeOut(toDosTime(ze.getTime()));
620 // uncompressed length
621 localDataStart = written;
622 if (zipMethod == DEFLATED || raf != null) {
627 writeOut(ZipLong.getBytes(ze.getCrc()));
628 writeOut(ZipLong.getBytes(ze.getSize()));
629 writeOut(ZipLong.getBytes(ze.getSize()));
634 byte[] name = getBytes(ze.getName());
635 writeOut(ZipShort.getBytes(name.length));
638 // extra field length
639 byte[] extra = ze.getLocalFileDataExtra();
640 writeOut(ZipShort.getBytes(extra.length));
645 written += name.length;
649 written += extra.length;
655 * Writes the data descriptor entry.
656 * @param ze the entry to write
657 * @throws IOException on error
661 protected void writeDataDescriptor(ZipEntry ze) throws IOException {
662 if (ze.getMethod() != DEFLATED || raf != null) {
666 writeOut(ZipLong.getBytes(entry.getCrc()));
667 writeOut(ZipLong.getBytes(entry.getCompressedSize()));
668 writeOut(ZipLong.getBytes(entry.getSize()));
673 * Writes the central file header entry.
674 * @param ze the entry to write
675 * @throws IOException on error
679 protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
684 writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
687 // version needed to extract
688 // general purpose bit flag
689 if (ze.getMethod() == DEFLATED && raf == null) {
690 // requires version 2 as we are going to store length info
691 // in the data descriptor
692 writeOut(ZipShort.getBytes(20));
694 // bit3 set to signal, we use a data descriptor
695 writeOut(ZipShort.getBytes(8));
697 writeOut(ZipShort.getBytes(10));
702 // compression method
703 writeOut(ZipShort.getBytes(ze.getMethod()));
706 // last mod. time and date
707 writeOut(toDosTime(ze.getTime()));
712 // uncompressed length
713 writeOut(ZipLong.getBytes(ze.getCrc()));
714 writeOut(ZipLong.getBytes(ze.getCompressedSize()));
715 writeOut(ZipLong.getBytes(ze.getSize()));
719 byte[] name = getBytes(ze.getName());
720 writeOut(ZipShort.getBytes(name.length));
723 // extra field length
724 byte[] extra = ze.getCentralDirectoryExtra();
725 writeOut(ZipShort.getBytes(extra.length));
728 // file comment length
729 String comm = ze.getComment();
733 byte[] commentB = getBytes(comm);
734 writeOut(ZipShort.getBytes(commentB.length));
741 // internal file attributes
742 writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
745 // external file attributes
746 writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
749 // relative offset of LFH
750 writeOut((byte[]) offsets.get(ze));
755 written += name.length;
759 written += extra.length;
763 written += commentB.length;
767 * Writes the "End of central dir record".
768 * @throws IOException on error
772 protected void writeCentralDirectoryEnd() throws IOException {
780 byte[] num = ZipShort.getBytes(entries.size());
784 // length and location of CD
785 writeOut(ZipLong.getBytes(cdLength));
786 writeOut(ZipLong.getBytes(cdOffset));
789 byte[] data = getBytes(comment);
790 writeOut(ZipShort.getBytes(data.length));
795 * Smallest date/time ZIP can handle.
799 private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);
802 * Convert a Date object to a DOS date/time field.
803 * @param time the <code>Date</code> to convert
804 * @return the date as a <code>ZipLong</code>
807 protected static ZipLong toDosTime(Date time) {
808 return new ZipLong(toDosTime(time.getTime()));
812 * Convert a Date object to a DOS date/time field.
814 * <p>Stolen from InfoZip's <code>fileio.c</code></p>
815 * @param t number of milliseconds since the epoch
816 * @return the date as a byte array
819 protected static byte[] toDosTime(long t) {
820 Date time = new Date(t);
821 int year = time.getYear() + 1900;
825 int month = time.getMonth() + 1;
826 long value = ((year - 1980) << 25)
828 | (time.getDate() << 16)
829 | (time.getHours() << 11)
830 | (time.getMinutes() << 5)
831 | (time.getSeconds() >> 1);
832 return ZipLong.getBytes(value);
836 * Retrieve the bytes for the given String in the encoding set for
838 * @param name the string to get bytes from
839 * @return the bytes as a byte array
840 * @throws ZipException on error
844 protected byte[] getBytes(String name) throws ZipException {
845 if (encoding == null) {
846 return name.getBytes();
849 return name.getBytes(encoding);
850 } catch (UnsupportedEncodingException uee) {
851 throw new ZipException(uee.getMessage());
857 * Write bytes to output or random access file.
858 * @param data the byte array to write
859 * @throws IOException on error
863 protected final void writeOut(byte[] data) throws IOException {
864 writeOut(data, 0, data.length);
868 * Write bytes to output or random access file.
869 * @param data the byte array to write
870 * @param offset the start position to write from
871 * @param length the number of bytes to write
872 * @throws IOException on error
876 protected final void writeOut(byte[] data, int offset, int length)
879 raf.write(data, offset, length);
881 out.write(data, offset, length);
886 * Assumes a negative integer really is a positive integer that
887 * has wrapped around and re-creates the original value.
888 * @param i the value to treat as unsigned int.
889 * @return the unsigned int as a long.
892 protected static long adjustToLong(int i) {
894 return 2 * ((long) Integer.MAX_VALUE) + 2 + i;