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 functionality of
38 * this package, especially internal/external file attributes and extra fields
39 * with different layouts for local file data and central directory entries.
42 * This class will try to use {@link java.io.RandomAccessFile RandomAccessFile}
43 * when you know that the output is going to go to a file.
47 * If RandomAccessFile cannot be used, this implementation will use a Data
48 * Descriptor to store size and CRC information for {@link #DEFLATED DEFLATED}
49 * entries, this means, you don't need to calculate them yourself. Unfortunately
50 * this is not possible for the {@link #STORED STORED} method, here setting the
51 * CRC and uncompressed size information is required before
52 * {@link #putNextEntry putNextEntry} can be called.
56 public class ZipOutputStream extends FilterOutputStream {
59 * Compression method for deflated entries.
63 public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
66 * Default compression level for deflated entries.
70 public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
73 * Compression method for stored entries.
77 public static final int STORED = java.util.zip.ZipEntry.STORED;
84 private ZipEntry entry;
91 private String comment = "";
94 * Compression level for next entry.
98 private int level = DEFAULT_COMPRESSION;
101 * Has the compression level changed when compared to the last entry?
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 current entry
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.
189 * For a list of possible values see <a
190 * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html"
191 * >http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
192 * Defaults to the platform's default character encoding.
197 private String encoding = null;
199 // CheckStyle:VisibilityModifier OFF - bc
202 * This Deflater object is used for output.
205 * This attribute is only protected to provide a level of API backwards
206 * compatibility. This class used to extend
207 * {@link java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
213 protected Deflater def = new Deflater(level, true);
216 * This buffer servers as a Deflater.
219 * This attribute is only protected to provide a level of API backwards
220 * compatibility. This class used to extend
221 * {@link java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
227 protected byte[] buf = new byte[512];
229 // CheckStyle:VisibilityModifier ON
232 * Optional random access output.
236 private RandomAccessFile raf = null;
239 * Creates a new ZIP OutputStream filtering the underlying stream.
242 * the outputstream to zip
245 public ZipOutputStream(OutputStream out) {
250 * Creates a new ZIP OutputStream writing to a File. Will use random access if
256 * @throws IOException
259 public ZipOutputStream(File file) throws IOException {
263 raf = new RandomAccessFile(file, "rw");
265 } catch (IOException e) {
269 } catch (IOException inner) {
274 out = new FileOutputStream(file);
279 * This method indicates whether this archive is writing to a seekable stream
280 * (i.e., to a random access file).
283 * For seekable streams, you don't need to calculate the CRC or uncompressed
284 * size for {@link #STORED} entries before invoking {@link #putNextEntry}.
286 * @return true if seekable
289 public boolean isSeekable() {
294 * The encoding to use for filenames and the file comment.
297 * For a list of possible values see <a
298 * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html"
299 * >http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
300 * Defaults to the platform's default character encoding.
307 public void setEncoding(String encoding) {
308 this.encoding = encoding;
312 * The encoding to use for filenames and the file comment.
314 * @return null if using the platform's default character encoding.
318 public String getEncoding() {
323 * Finishs writing the contents and closes this as well as the underlying
327 * @throws IOException
330 public void finish() throws IOException {
333 for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) {
334 writeCentralFileHeader((ZipEntry) entries.elementAt(i));
336 cdLength = written - cdOffset;
337 writeCentralDirectoryEnd();
339 entries.removeAllElements();
343 * Writes all necessary data for this entry.
346 * @throws IOException
349 public void closeEntry() throws IOException {
354 long realCrc = crc.getValue();
357 if (entry.getMethod() == DEFLATED) {
359 while (!def.finished()) {
363 entry.setSize(adjustToLong(def.getTotalIn()));
364 entry.setCompressedSize(adjustToLong(def.getTotalOut()));
365 entry.setCrc(realCrc);
369 written += entry.getCompressedSize();
370 } else if (raf == null) {
371 if (entry.getCrc() != realCrc) {
372 throw new ZipException("bad CRC checksum for entry " + entry.getName()
373 + ": " + Long.toHexString(entry.getCrc()) + " instead of "
374 + Long.toHexString(realCrc));
377 if (entry.getSize() != written - dataStart) {
378 throw new ZipException("bad size for entry " + entry.getName() + ": "
379 + entry.getSize() + " instead of " + (written - dataStart));
381 } else { /* method is STORED and we used RandomAccessFile */
382 long size = written - dataStart;
385 entry.setCompressedSize(size);
386 entry.setCrc(realCrc);
389 // If random access output, write the local file header containing
390 // the correct CRC and compressed/uncompressed sizes
392 long save = raf.getFilePointer();
394 raf.seek(localDataStart);
395 writeOut(ZipLong.getBytes(entry.getCrc()));
396 writeOut(ZipLong.getBytes(entry.getCompressedSize()));
397 writeOut(ZipLong.getBytes(entry.getSize()));
401 writeDataDescriptor(entry);
406 * Begin writing next entry.
411 * @throws IOException
414 public void putNextEntry(ZipEntry ze) throws IOException {
418 entries.addElement(entry);
420 if (entry.getMethod() == -1) { // not specified
421 entry.setMethod(method);
424 if (entry.getTime() == -1) { // not specified
425 entry.setTime(System.currentTimeMillis());
428 // Size/CRC not required if RandomAccessFile is used
429 if (entry.getMethod() == STORED && raf == null) {
430 if (entry.getSize() == -1) {
431 throw new ZipException("uncompressed size is required for"
432 + " STORED method when not writing to a" + " file");
434 if (entry.getCrc() == -1) {
435 throw new ZipException("crc checksum is required for STORED"
436 + " method when not writing to a file");
438 entry.setCompressedSize(entry.getSize());
441 if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
443 hasCompressionLevelChanged = false;
445 writeLocalFileHeader(entry);
449 * Set the file comment.
455 public void setComment(String comment) {
456 this.comment = comment;
460 * Sets the compression level for subsequent entries.
463 * Default is Deflater.DEFAULT_COMPRESSION.
467 * the compression level.
468 * @throws IllegalArgumentException
469 * if an invalid compression level is specified.
472 public void setLevel(int level) {
473 if (level < Deflater.DEFAULT_COMPRESSION
474 || level > Deflater.BEST_COMPRESSION) {
475 throw new IllegalArgumentException("Invalid compression level: " + level);
477 hasCompressionLevelChanged = (this.level != level);
482 * Sets the default compression method for subsequent entries.
485 * Default is DEFLATED.
489 * an <code>int</code> from java.util.zip.ZipEntry
492 public void setMethod(int method) {
493 this.method = method;
497 * Writes bytes to ZIP entry.
500 * the byte array to write
502 * the start position to write from
504 * the number of bytes to write
505 * @throws IOException
508 public void write(byte[] b, int offset, int length) throws IOException {
509 if (entry.getMethod() == DEFLATED) {
511 if (!def.finished()) {
512 def.setInput(b, offset, length);
513 while (!def.needsInput()) {
519 writeOut(b, offset, length);
522 crc.update(b, offset, length);
526 * Writes a single byte to ZIP entry.
529 * Delegates to the three arg method.
535 * @throws IOException
538 public void write(int b) throws IOException {
539 byte[] buff = new byte[1];
540 buff[0] = (byte) (b & 0xff);
545 * Closes this output stream and releases any system resources associated with
548 * @exception IOException
549 * if an I/O error occurs.
552 public void close() throws IOException {
564 * Flushes this output stream and forces any buffered output bytes to be
565 * written out to the stream.
567 * @exception IOException
568 * if an I/O error occurs.
571 public void flush() throws IOException {
578 * Various ZIP constants
581 * local file header signature
585 protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);
588 * data descriptor signature
592 protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
595 * central file header signature
599 protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);
602 * end of central dir signature
606 protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
609 * Writes next block of compressed data to the output stream.
611 * @throws IOException
616 protected final void deflate() throws IOException {
617 int len = def.deflate(buf, 0, buf.length);
619 writeOut(buf, 0, len);
624 * Writes the local file header entry
628 * @throws IOException
633 protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
634 offsets.put(ze, ZipLong.getBytes(written));
639 // store method in local variable to prevent multiple method calls
640 final int zipMethod = ze.getMethod();
642 // version needed to extract
643 // general purpose bit flag
644 if (zipMethod == DEFLATED && raf == null) {
645 // requires version 2 as we are going to store length info
646 // in the data descriptor
647 writeOut(ZipShort.getBytes(20));
649 // bit3 set to signal, we use a data descriptor
650 writeOut(ZipShort.getBytes(8));
652 writeOut(ZipShort.getBytes(10));
657 // compression method
658 writeOut(ZipShort.getBytes(zipMethod));
661 // last mod. time and date
662 writeOut(toDosTime(ze.getTime()));
667 // uncompressed length
668 localDataStart = written;
669 if (zipMethod == DEFLATED || raf != null) {
674 writeOut(ZipLong.getBytes(ze.getCrc()));
675 writeOut(ZipLong.getBytes(ze.getSize()));
676 writeOut(ZipLong.getBytes(ze.getSize()));
681 byte[] name = getBytes(ze.getName());
682 writeOut(ZipShort.getBytes(name.length));
685 // extra field length
686 byte[] extra = ze.getLocalFileDataExtra();
687 writeOut(ZipShort.getBytes(extra.length));
692 written += name.length;
696 written += extra.length;
702 * Writes the data descriptor entry.
706 * @throws IOException
711 protected void writeDataDescriptor(ZipEntry ze) throws IOException {
712 if (ze.getMethod() != DEFLATED || raf != null) {
716 writeOut(ZipLong.getBytes(entry.getCrc()));
717 writeOut(ZipLong.getBytes(entry.getCompressedSize()));
718 writeOut(ZipLong.getBytes(entry.getSize()));
723 * Writes the central file header entry.
727 * @throws IOException
732 protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
737 writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
740 // version needed to extract
741 // general purpose bit flag
742 if (ze.getMethod() == DEFLATED && raf == null) {
743 // requires version 2 as we are going to store length info
744 // in the data descriptor
745 writeOut(ZipShort.getBytes(20));
747 // bit3 set to signal, we use a data descriptor
748 writeOut(ZipShort.getBytes(8));
750 writeOut(ZipShort.getBytes(10));
755 // compression method
756 writeOut(ZipShort.getBytes(ze.getMethod()));
759 // last mod. time and date
760 writeOut(toDosTime(ze.getTime()));
765 // uncompressed length
766 writeOut(ZipLong.getBytes(ze.getCrc()));
767 writeOut(ZipLong.getBytes(ze.getCompressedSize()));
768 writeOut(ZipLong.getBytes(ze.getSize()));
772 byte[] name = getBytes(ze.getName());
773 writeOut(ZipShort.getBytes(name.length));
776 // extra field length
777 byte[] extra = ze.getCentralDirectoryExtra();
778 writeOut(ZipShort.getBytes(extra.length));
781 // file comment length
782 String comm = ze.getComment();
786 byte[] commentB = getBytes(comm);
787 writeOut(ZipShort.getBytes(commentB.length));
794 // internal file attributes
795 writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
798 // external file attributes
799 writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
802 // relative offset of LFH
803 writeOut((byte[]) offsets.get(ze));
808 written += name.length;
812 written += extra.length;
816 written += commentB.length;
820 * Writes the "End of central dir record".
822 * @throws IOException
827 protected void writeCentralDirectoryEnd() throws IOException {
835 byte[] num = ZipShort.getBytes(entries.size());
839 // length and location of CD
840 writeOut(ZipLong.getBytes(cdLength));
841 writeOut(ZipLong.getBytes(cdOffset));
844 byte[] data = getBytes(comment);
845 writeOut(ZipShort.getBytes(data.length));
850 * Smallest date/time ZIP can handle.
854 private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);
857 * Convert a Date object to a DOS date/time field.
860 * the <code>Date</code> to convert
861 * @return the date as a <code>ZipLong</code>
864 protected static ZipLong toDosTime(Date time) {
865 return new ZipLong(toDosTime(time.getTime()));
869 * Convert a Date object to a DOS date/time field.
872 * Stolen from InfoZip's <code>fileio.c</code>
876 * number of milliseconds since the epoch
877 * @return the date as a byte array
880 protected static byte[] toDosTime(long t) {
881 Date time = new Date(t);
882 int year = time.getYear() + 1900;
886 int month = time.getMonth() + 1;
887 long value = ((year - 1980) << 25) | (month << 21) | (time.getDate() << 16)
888 | (time.getHours() << 11) | (time.getMinutes() << 5)
889 | (time.getSeconds() >> 1);
890 return ZipLong.getBytes(value);
894 * Retrieve the bytes for the given String in the encoding set for this
898 * the string to get bytes from
899 * @return the bytes as a byte array
900 * @throws ZipException
905 protected byte[] getBytes(String name) throws ZipException {
906 if (encoding == null) {
907 return name.getBytes();
910 return name.getBytes(encoding);
911 } catch (UnsupportedEncodingException uee) {
912 throw new ZipException(uee.getMessage());
918 * Write bytes to output or random access file.
921 * the byte array to write
922 * @throws IOException
927 protected final void writeOut(byte[] data) throws IOException {
928 writeOut(data, 0, data.length);
932 * Write bytes to output or random access file.
935 * the byte array to write
937 * the start position to write from
939 * the number of bytes to write
940 * @throws IOException
945 protected final void writeOut(byte[] data, int offset, int length)
948 raf.write(data, offset, length);
950 out.write(data, offset, length);
955 * Assumes a negative integer really is a positive integer that has wrapped
956 * around and re-creates the original value.
959 * the value to treat as unsigned int.
960 * @return the unsigned int as a long.
963 protected static long adjustToLong(int i) {
965 return 2 * ((long) Integer.MAX_VALUE) + 2 + i;