blob: 4348c56834a5efc891a942be237fcfd140463a80 [file] [log] [blame]
/*
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowlegement:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowlegement may appear in the software itself,
* if and wherever such third-party acknowlegements normally appear.
*
* 4. The names "The Jakarta Project", "Ant", and "Apache Software
* Foundation" must not be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache"
* nor may "Apache" appear in their names without prior written
* permission of the Apache Group.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package org.apache.tools.zip;
import java.io.OutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.ZipException;
/**
* Reimplementation of {@link java.util.zip.ZipOutputStream
* java.util.zip.ZipOutputStream} that does handle the extended
* functionality of this package, especially internal/external file
* attributes and extra fields with different layouts for local file
* data and central directory entries.
*
* <p>This implementation will use a Data Descriptor to store size and
* CRC information for DEFLATED entries, this means, you don't need to
* calculate them yourself. Unfortunately this is not possible for
* the STORED method, here setting the CRC and uncompressed size
* information is required before {@link #putNextEntry putNextEntry}
* will be called.</p>
*
* @author <a href="stefan.bodewig@epost.de">Stefan Bodewig</a>
* @version $Revision$
*/
public class ZipOutputStream extends DeflaterOutputStream {
/**
* Current entry.
*
* @since 1.1
*/
private ZipEntry entry;
/**
* The file comment.
*
* @since 1.1
*/
private String comment = "";
/**
* Compression level for next entry.
*
* @since 1.1
*/
private int level = Deflater.DEFAULT_COMPRESSION;
/**
* Has the compression level changed when compared to the last
* entry?
*
* @since 1.5
*/
private boolean hasCompressionLevelChanged = false;
/**
* Default compression method for next entry.
*
* @since 1.1
*/
private int method = DEFLATED;
/**
* List of ZipEntries written so far.
*
* @since 1.1
*/
private Vector entries = new Vector();
/**
* CRC instance to avoid parsing DEFLATED data twice.
*
* @since 1.1
*/
private CRC32 crc = new CRC32();
/**
* Count the bytes written to out.
*
* @since 1.1
*/
private long written = 0;
/**
* Data for current entry started here.
*
* @since 1.1
*/
private long dataStart = 0;
/**
* Start of central directory.
*
* @since 1.1
*/
private ZipLong cdOffset = new ZipLong(0);
/**
* Length of central directory.
*
* @since 1.1
*/
private ZipLong cdLength = new ZipLong(0);
/**
* Helper, a 0 as ZipShort.
*
* @since 1.1
*/
private static final byte[] ZERO = {0, 0};
/**
* Helper, a 0 as ZipLong.
*
* @since 1.1
*/
private static final byte[] LZERO = {0, 0, 0, 0};
/**
* Holds the offsets of the LFH starts for each entry
*
* @since 1.1
*/
private Hashtable offsets = new Hashtable();
/**
* The encoding to use for filenames and the file comment.
*
* <p>For a list of possible values see <a
* href="http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html">http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html</a>.
* Defaults to the platform's default character encoding.</p>
*
* @since 1.3
*/
private String encoding = null;
/**
* Compression method for deflated entries.
*
* @since 1.1
*/
public static final int DEFLATED = ZipEntry.DEFLATED;
/**
* Compression method for deflated entries.
*
* @since 1.1
*/
public static final int STORED = ZipEntry.STORED;
/**
* Creates a new ZIP OutputStream filtering the underlying stream.
*
* @since 1.1
*/
public ZipOutputStream(OutputStream out) {
super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
}
/**
* The encoding to use for filenames and the file comment.
*
* <p>For a list of possible values see <a
* href="http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html">http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html</a>.
* Defaults to the platform's default character encoding.</p>
*
* @since 1.3
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* The encoding to use for filenames and the file comment.
*
* @return null if using the platform's default character encoding.
*
* @since 1.3
*/
public String getEncoding() {
return encoding;
}
/*
* Found out by experiment, that DeflaterOutputStream.close()
* will call finish() - so we don't need to override close
* ourselves.
*/
/**
* Finishs writing the contents and closes this as well as the
* underlying stream.
*
* @since 1.1
*/
public void finish() throws IOException {
closeEntry();
cdOffset = new ZipLong(written);
for (int i = 0; i < entries.size(); i++) {
writeCentralFileHeader((ZipEntry) entries.elementAt(i));
}
cdLength = new ZipLong(written - cdOffset.getValue());
writeCentralDirectoryEnd();
offsets.clear();
entries.removeAllElements();
}
/**
* Writes all necessary data for this entry.
*
* @since 1.1
*/
public void closeEntry() throws IOException {
if (entry == null) {
return;
}
long realCrc = crc.getValue();
crc.reset();
if (entry.getMethod() == DEFLATED) {
def.finish();
while (!def.finished()) {
deflate();
}
entry.setSize(def.getTotalIn());
entry.setComprSize(def.getTotalOut());
entry.setCrc(realCrc);
def.reset();
written += entry.getCompressedSize();
} else {
if (entry.getCrc() != realCrc) {
throw new ZipException("bad CRC checksum for entry "
+ entry.getName() + ": "
+ Long.toHexString(entry.getCrc())
+ " instead of "
+ Long.toHexString(realCrc));
}
if (entry.getSize() != written - dataStart) {
throw new ZipException("bad size for entry "
+ entry.getName() + ": "
+ entry.getSize()
+ " instead of "
+ (written - dataStart));
}
}
writeDataDescriptor(entry);
entry = null;
}
/**
* Begin writing next entry.
*
* @since 1.1
*/
public void putNextEntry(ZipEntry ze) throws IOException {
closeEntry();
entry = ze;
entries.addElement(entry);
if (entry.getMethod() == -1) { // not specified
entry.setMethod(method);
}
if (entry.getTime() == -1) { // not specified
entry.setTime(System.currentTimeMillis());
}
if (entry.getMethod() == STORED) {
if (entry.getSize() == -1) {
throw new ZipException("uncompressed size is required for STORED method");
}
if (entry.getCrc() == -1) {
throw new ZipException("crc checksum is required for STORED method");
}
entry.setComprSize(entry.getSize());
} else if (hasCompressionLevelChanged) {
def.setLevel(level);
hasCompressionLevelChanged = false;
}
writeLocalFileHeader(entry);
}
/**
* Set the file comment.
*
* @since 1.1
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
* Sets the compression level for subsequent entries.
*
* <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
*
* @since 1.1
*/
public void setLevel(int level) {
hasCompressionLevelChanged = (this.level != level);
this.level = level;
}
/**
* Sets the default compression method for subsequent entries.
*
* <p>Default is DEFLATED.</p>
*
* @since 1.1
*/
public void setMethod(int method) {
this.method = method;
}
/**
* Writes bytes to ZIP entry.
*
* <p>Override is necessary to support STORED entries, as well as
* calculationg CRC automatically for DEFLATED entries.</p>
*/
public void write(byte[] b, int offset, int length) throws IOException {
if (entry.getMethod() == DEFLATED) {
super.write(b, offset, length);
} else {
out.write(b, offset, length);
written += length;
}
crc.update(b, offset, length);
}
/*
* Various ZIP constants
*/
/**
* local file header signature
*
* @since 1.1
*/
protected static final ZipLong LFH_SIG = new ZipLong(0X04034B50L);
/**
* data descriptor signature
*
* @since 1.1
*/
protected static final ZipLong DD_SIG = new ZipLong(0X08074B50L);
/**
* central file header signature
*
* @since 1.1
*/
protected static final ZipLong CFH_SIG = new ZipLong(0X02014B50L);
/**
* end of central dir signature
*
* @since 1.1
*/
protected static final ZipLong EOCD_SIG = new ZipLong(0X06054B50L);
/**
* Writes the local file header entry
*
* @since 1.1
*/
protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
offsets.put(ze, new ZipLong(written));
out.write(LFH_SIG.getBytes());
written += 4;
// version needed to extract
// general purpose bit flag
if (ze.getMethod() == DEFLATED) {
// requires version 2 as we are going to store length info
// in the data descriptor
out.write((new ZipShort(20)).getBytes());
// bit3 set to signal, we use a data descriptor
out.write((new ZipShort(8)).getBytes());
} else {
out.write((new ZipShort(10)).getBytes());
out.write(ZERO);
}
written += 4;
// compression method
out.write((new ZipShort(ze.getMethod())).getBytes());
written += 2;
// last mod. time and date
out.write(toDosTime(new Date(ze.getTime())).getBytes());
written += 4;
// CRC
// compressed length
// uncompressed length
if (ze.getMethod() == DEFLATED) {
out.write(LZERO);
out.write(LZERO);
out.write(LZERO);
} else {
out.write((new ZipLong(ze.getCrc())).getBytes());
out.write((new ZipLong(ze.getSize())).getBytes());
out.write((new ZipLong(ze.getSize())).getBytes());
}
written += 12;
// file name length
byte[] name = getBytes(ze.getName());
out.write((new ZipShort(name.length)).getBytes());
written += 2;
// extra field length
byte[] extra = ze.getLocalFileDataExtra();
out.write((new ZipShort(extra.length)).getBytes());
written += 2;
// file name
out.write(name);
written += name.length;
// extra field
out.write(extra);
written += extra.length;
dataStart = written;
}
/**
* Writes the data descriptor entry
*
* @since 1.1
*/
protected void writeDataDescriptor(ZipEntry ze) throws IOException {
if (ze.getMethod() != DEFLATED) {
return;
}
out.write(DD_SIG.getBytes());
out.write((new ZipLong(entry.getCrc())).getBytes());
out.write((new ZipLong(entry.getCompressedSize())).getBytes());
out.write((new ZipLong(entry.getSize())).getBytes());
written += 16;
}
/**
* Writes the central file header entry
*
* @since 1.1
*/
protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
out.write(CFH_SIG.getBytes());
written += 4;
// version made by
out.write((new ZipShort(20)).getBytes());
written += 2;
// version needed to extract
// general purpose bit flag
if (ze.getMethod() == DEFLATED) {
// requires version 2 as we are going to store length info
// in the data descriptor
out.write((new ZipShort(20)).getBytes());
// bit3 set to signal, we use a data descriptor
out.write((new ZipShort(8)).getBytes());
} else {
out.write((new ZipShort(10)).getBytes());
out.write(ZERO);
}
written += 4;
// compression method
out.write((new ZipShort(ze.getMethod())).getBytes());
written += 2;
// last mod. time and date
out.write(toDosTime(new Date(ze.getTime())).getBytes());
written += 4;
// CRC
// compressed length
// uncompressed length
out.write((new ZipLong(ze.getCrc())).getBytes());
out.write((new ZipLong(ze.getCompressedSize())).getBytes());
out.write((new ZipLong(ze.getSize())).getBytes());
written += 12;
// file name length
byte[] name = getBytes(ze.getName());
out.write((new ZipShort(name.length)).getBytes());
written += 2;
// extra field length
byte[] extra = ze.getCentralDirectoryExtra();
out.write((new ZipShort(extra.length)).getBytes());
written += 2;
// file comment length
String comm = ze.getComment();
if (comm == null) {
comm = "";
}
byte[] comment = getBytes(comm);
out.write((new ZipShort(comment.length)).getBytes());
written += 2;
// disk number start
out.write(ZERO);
written += 2;
// internal file attributes
out.write((new ZipShort(ze.getInternalAttributes())).getBytes());
written += 2;
// external file attributes
out.write((new ZipLong(ze.getExternalAttributes())).getBytes());
written += 4;
// relative offset of LFH
out.write(((ZipLong) offsets.get(ze)).getBytes());
written += 4;
// file name
out.write(name);
written += name.length;
// extra field
out.write(extra);
written += extra.length;
// file comment
out.write(comment);
written += comment.length;
}
/**
* Writes the &quot;End of central dir record&quot;
*
* @since 1.1
*/
protected void writeCentralDirectoryEnd() throws IOException {
out.write(EOCD_SIG.getBytes());
// disk numbers
out.write(ZERO);
out.write(ZERO);
// number of entries
byte[] num = (new ZipShort(entries.size())).getBytes();
out.write(num);
out.write(num);
// length and location of CD
out.write(cdLength.getBytes());
out.write(cdOffset.getBytes());
// ZIP file comment
byte[] data = getBytes(comment);
out.write((new ZipShort(data.length)).getBytes());
out.write(data);
}
/**
* Smallest date/time ZIP can handle.
*
* @since 1.1
*/
private static final ZipLong DOS_TIME_MIN = new ZipLong(0x00002100L);
/**
* Convert a Date object to a DOS date/time field.
*
* <p>Stolen from InfoZip's <code>fileio.c</code></p>
*
* @since 1.1
*/
protected static ZipLong toDosTime(Date time) {
int year = time.getYear() + 1900;
int month = time.getMonth() + 1;
if (year < 1980) {
return DOS_TIME_MIN;
}
long value = ((year - 1980) << 25)
| (month << 21)
| (time.getDate() << 16)
| (time.getHours() << 11)
| (time.getMinutes() << 5)
| (time.getSeconds() >> 1);
byte[] result = new byte[4];
result[0] = (byte) ((value & 0xFF));
result[1] = (byte) ((value & 0xFF00) >> 8);
result[2] = (byte) ((value & 0xFF0000) >> 16);
result[3] = (byte) ((value & 0xFF000000l) >> 24);
return new ZipLong(result);
}
/**
* Retrieve the bytes for the given String in the encoding set for
* this Stream.
*
* @since 1.3
*/
protected byte[] getBytes(String name) throws ZipException {
if (encoding == null) {
return name.getBytes();
} else {
try {
return name.getBytes(encoding);
} catch (UnsupportedEncodingException uee) {
throw new ZipException(uee.getMessage());
}
}
}
}