| /* |
| * 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 "End of central dir record" |
| * |
| * @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()); |
| } |
| } |
| } |
| |
| } |