/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.archive.zip.provider; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Vector; import java.util.zip.Deflater; import java.util.zip.ZipException; /** * Reimplementation of {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} that handles 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>--------------------------------------------------------------------------------------------------------------<br> * <br> * This class is based off the <code>org.apache.tools.zip</code> package of the <i>Apache Ant</i> project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant.</p> * * @author Apache Ant, Maxence Bernard */ public class ZipOutputStream extends OutputStream implements ZipConstants { /** Current entry */ private ZipEntry entry; /** Current ZipEntryOutputStream corresponding to the entry being written */ private ZipEntryOutputStream zeos; /** Additional info about current entry */ private ZipEntryInfo entryInfo; /** The global zip file comment */ private String comment = ""; /** Compression level for zip entries */ private int level = DEFAULT_DEFLATER_COMPRESSION; /** Compression method zip entries */ private int method = DEFLATED; /** Deflater instance that is used to compress DEFLATED entries */ protected Deflater deflater = new Deflater(level, true); /** Buffer used by Deflater to deflate data */ protected byte[] deflaterBuf; /** List of zip entries written so far */ private Vector<ZipEntry> entries = new Vector<ZipEntry>(); /** Count the bytes written to out */ private long written = 0; /** The encoding to use for filenames and the file comment, UTF-8 by default */ private String encoding = UTF_8; /** Holds byte buffer instance used to convert short and longs, avoids creating lots of small arrays */ private ZipBuffer zipBuffer = new ZipBuffer(); /** 0 (zero) as ZipShort */ private static final byte[] SHORT_0 = ZipShort.getBytes(0); /** 0 (zero) as ZipLong */ private static final byte[] LONG_0 = ZipLong.getBytes(0); /** Three ZipLong zeros */ private static final byte[] LONG_TRIPLE_0 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; /** 8 as ZipShort */ private static final byte[] SHORT_8 = ZipShort.getBytes(8); /** 10 as ZipShort */ private static final byte[] SHORT_10 = ZipShort.getBytes(10); /** 20 as ZipShort */ private static final byte[] SHORT_20 = ZipShort.getBytes(20); /** 2048 as ZipShort */ private static final byte[] SHORT_2048 = ZipShort.getBytes(2048); /** 2056 as ZipShort */ private static final byte[] SHORT_2056 = ZipShort.getBytes(2056); /** * The underlying stream this ZipOutputStream writes zip-compressed data to. */ protected OutputStream out; /** * Is the underlying stream a RandomAccessOutputStream? Avoids excessive instanceof comparisons. */ private boolean hasRandomAccess; /** * Creates a new <code>ZipOutputStream</code> that writes Zip-compressed data to the given <code>OutputStream</code>. * If a {@link RandomAccessOutputStream} is supplied, the Zip entries will be written without data descriptor, * which will yield a slightly smaller file. * * @param out the underlying OutputStream stream where compressed data is written to */ public ZipOutputStream(OutputStream out) { this.out = out; this.hasRandomAccess = out instanceof RandomAccessOutputStream; // Use BufferPool to avoid excessive memory allocation and garbage collection. deflaterBuf = BufferPool.getByteArray(DEFAULT_DEFLATER_BUFFER_SIZE); } /** * This method indicates whether this archive is writing to a {@link RandomAccessOutputStream}. * * @return true if seekable */ public boolean isSeekable() { return hasRandomAccess; } /** * The encoding to use for filenames and the file comment. * * <p>For a list of possible values see <a * 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>. * Defaults to the platform's default character encoding.</p> * * @param encoding the encoding value */ 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. */ public String getEncoding() { return encoding; } /** * Returns <code>true</code> if the given encoding is either "UTF-8", "UTF8" (case-insensitive) or * <code>null</code>. * * @param encoding the encoding String to test * @return true if the given encoding is either "UTF-8", "UTF8" or null */ private static boolean isUTF8(String encoding) { return encoding==null || encoding.equalsIgnoreCase("UTF-8") || encoding.equalsIgnoreCase("UTF8"); } /** * Finishs writing the contents and closes this as well as the * underlying stream. * * @throws IOException on error */ public void finish() throws IOException { closeEntry(); long cdOffset = written; int nbEntries = entries.size(); ZipEntry ze; for (int i=0; i <nbEntries; i++) { ze = entries.elementAt(i); written += writeCentralFileHeader(ze, out, encoding, ze.getEntryInfo().headerOffset, !hasRandomAccess, zipBuffer); } long cdLength = written - cdOffset; writeCentralDirectoryEnd(out, nbEntries, cdLength, cdOffset, comment, encoding, zipBuffer); entries.removeAllElements(); } /** * Writes all necessary data for this entry. This method must be called after an entry opened by * {@link #putNextEntry(ZipEntry)} has finished being written. * * @throws IOException on error */ public void closeEntry() throws IOException { if (entry == null) return; finalizeEntryData(entry, zeos, out, !hasRandomAccess, zipBuffer); written += entry.getCompressedSize(); if(!hasRandomAccess) written += writeDataDescriptor(entry, out, zipBuffer); entry = null; entryInfo = null; zeos.close(); zeos = null; } /** * Writes the size and CRC information of an entry. This method is to be called right after a file entry's data * has been written. * * <p>The size and CRC information is written to the given <code>OutputStream</code>, either as a data descriptor or * in the entry's local file header, and is set in the given {@link ZipEntry} instance. * * @param entry the entry * @param zeos the Zip entry's output stream * @param out the * @param useDataDescriptor if true, a data descriptor will be written to out. If false, size and CRC information * will be written in the local file header (requires out to be a RandomAccessOutputStream). * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred */ protected static void finalizeEntryData(ZipEntry entry, ZipEntryOutputStream zeos, OutputStream out, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { long crc = zeos.getCrc(); if (entry.getMethod() == DEFLATED) { ((DeflatedOutputStream)zeos).finishDeflate(); entry.setSize(adjustToLong(zeos.getTotalIn())); long compressedSize = adjustToLong(zeos.getTotalOut()); entry.setCompressedSize(compressedSize); entry.setCrc(crc); } else { // Method is STORED long size = zeos.getTotalOut(); entry.setSize(size); entry.setCompressedSize(size); entry.setCrc(crc); } // If random access output, write the local file header containing // the correct CRC and compressed/uncompressed sizes if (!useDataDescriptor) { RandomAccessOutputStream raos = (RandomAccessOutputStream)out; long save = raos.getOffset(); raos.seek(entry.getEntryInfo().headerOffset + 14); raos.write(ZipLong.getBytes(entry.getCrc(), zipBuffer.longBuffer)); raos.write(ZipLong.getBytes(entry.getCompressedSize(), zipBuffer.longBuffer)); raos.write(ZipLong.getBytes(entry.getSize(), zipBuffer.longBuffer)); raos.seek(save); } } /** * Start writing the given entry. The entry is written by calling the <code>write()</code> of this class. * When the entry has finished being written, {@link #closeEntry()} must be called. * * @param ze the entry to write * @throws IOException on error */ public void putNextEntry(ZipEntry ze) throws IOException { closeEntry(); entry = ze; entryInfo = new ZipEntryInfo(); entry.setEntryInfo(entryInfo); entries.addElement(entry); int entryMethod = entry.getMethod(); if (entryMethod == -1) { // method not specified in the entry, use the one set in this ZipOutputStream entryMethod = method; entry.setMethod(method); } if (entry.getTime() == -1) { // date not specified in the entry, set it to now entry.setTime(System.currentTimeMillis()); } if(entryMethod == DEFLATED) { deflater.reset(); deflater.setLevel(level); zeos = new DeflatedOutputStream(out, deflater, deflaterBuf); } else { zeos = new StoredOutputStream(out); } entryInfo.headerOffset = written; written += writeLocalFileHeader(entry, out, encoding, !hasRandomAccess, zipBuffer); entryInfo.dataOffset = written; } /** * Sets the file comment. * * @param comment the comment */ public void setComment(String comment) { this.comment = comment; } /** * Sets the compression level for subsequent entries. * * <p>Default is Deflater.DEFAULT_COMPRESSION.</p> * * @param level the compression level. * @throws IllegalArgumentException if an invalid compression level is specified. */ public void setLevel(int level) { if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { throw new IllegalArgumentException( "Invalid compression level: " + level); } this.level = level; } /** * Sets the default compression method for subsequent entries. * * <p>Default is DEFLATED.</p> * * @param method an <code>int</code> from java.util.zip.ZipEntry */ public void setMethod(int method) { this.method = method; } /** * Writes the local file header entry. * * @param ze the entry to write * @param out the OutputStream to write the header to * @param encoding the encoding to use for writing the entry's filename. If UTF-8 is used, the general purpose bit * flag will be set accordingly. * @param useDataDescriptor indicates whether a data descriptor will follow the file entry's data. The general * purpose bit flag will be set accordingly. * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the size (number of bytes) of the written local file header * @throws IOException if an I/O error occurred */ protected static long writeLocalFileHeader(ZipEntry ze, OutputStream out, String encoding, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { out.write(LFH_SIG); // written += 4; int zipMethod = ze.getMethod(); // version needed to extract // general purpose bit flag writeVersionAndGPBF(out, encoding, useDataDescriptor); // nbWritten += 4; // compression method out.write(ZipShort.getBytes(zipMethod, zipBuffer.shortBuffer)); // written += 2; // last mod. time and date out.write(ZipLong.getBytes(ze.getDosTime(), zipBuffer.longBuffer)); // written += 4; // CRC // compressed length // uncompressed length // this information is not known at this stage so it will be set after the data has been written, // either in the data descriptor (if used), or here by seeking (requires random access) out.write(LONG_TRIPLE_0); // 12 zero bytes // written += 12; // file name length byte[] name = getBytes(ze.getName(), encoding); out.write(ZipShort.getBytes(name.length, zipBuffer.shortBuffer)); // written += 2; // extra field length byte[] extra = ze.getLocalFileDataExtra(); out.write(ZipShort.getBytes(extra.length, zipBuffer.shortBuffer)); // written += 2; // Number of bytes written by this method so far long written = 30; // file name out.write(name); written += name.length; // extra field out.write(extra); written += extra.length; return written; } /** * Writes the data descriptor, using the CRC, compressed and uncompressed size attributes contained in the * given ZipEntry. * The length of the field is returned, it is always 16 bytes. * * @param ze the entry for which to write the data descriptor * @param out the OutputStream where to write the data descriptor to * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the number of bytes that were written, i.e. the size of the data descriptor (16 bytes) * @throws IOException if an I/O error occurred */ protected static long writeDataDescriptor(ZipEntry ze, OutputStream out, ZipBuffer zipBuffer) throws IOException { out.write(DD_SIG); out.write(ZipLong.getBytes(ze.getCrc(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getCompressedSize(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getSize(), zipBuffer.longBuffer)); return 16; } /** * Writes central file header's 'Version made by' field, using the platform contained in the given ZipEntry. * The length of the field is returned, it is always 2 bytes. * * @param ze the entry for which to write the 'Version made by' field * @param out the OutputStream where to write the field * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the number of bytes that were written, i.e. the size of the 'Version made by' field (2 bytes) * @throws IOException if an I/O error occurred */ protected static long writeVersionMadeBy(ZipEntry ze, OutputStream out, ZipBuffer zipBuffer) throws IOException { out.write(ZipShort.getBytes((ze.getPlatform() << 8) | 20, zipBuffer.shortBuffer)); return 2; } /** * Writes the central file header for the given entry. * * @param ze the entry for which to write the central file header * @param out the OutputStream to write the central file header to * @param encoding the encoding to use for writing the filename and optional comment * @param localFileHeaderOffset the offset to the local file header start * @param useDataDescriptor true if a data descriptor is used for the entry * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred * @return the number of bytes that were written, i.e. the size of the central file header */ protected static long writeCentralFileHeader(ZipEntry ze, OutputStream out, String encoding, long localFileHeaderOffset, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { out.write(CFH_SIG); // nbWritten += 4; // version made by writeVersionMadeBy(ze, out, zipBuffer); // nbWritten += 2; // version needed to extract // general purpose bit flag writeVersionAndGPBF(out, encoding, useDataDescriptor); // nbWritten += 4; // compression method out.write(ZipShort.getBytes(ze.getMethod(), zipBuffer.shortBuffer)); // nbWritten += 2; // last mod. time and date out.write(ZipLong.getBytes(ze.getDosTime(), zipBuffer.longBuffer)); // nbWritten += 4; // CRC // compressed length // uncompressed length out.write(ZipLong.getBytes(ze.getCrc(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getCompressedSize(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getSize(), zipBuffer.longBuffer)); // nbWritten += 12; // file name length byte[] name = getBytes(ze.getName(), encoding); out.write(ZipShort.getBytes(name.length, zipBuffer.shortBuffer)); // nbWritten += 2; // extra field length byte[] extra = ze.getCentralDirectoryExtra(); out.write(ZipShort.getBytes(extra.length, zipBuffer.shortBuffer)); // nbWritten += 2; // file comment length String comm = ze.getComment(); if (comm == null) { comm = ""; } byte[] commentB = getBytes(comm, encoding); out.write(ZipShort.getBytes(commentB.length, zipBuffer.shortBuffer)); // nbWritten += 2; // disk number start out.write(SHORT_0); // nbWritten += 2; // internal file attributes out.write(ZipShort.getBytes(ze.getInternalAttributes(), zipBuffer.shortBuffer)); // nbWritten += 2; // external file attributes out.write(ZipLong.getBytes(ze.getExternalAttributes(), zipBuffer.longBuffer)); // nbWritten += 4; // relative offset of LFH out.write(ZipLong.getBytes(localFileHeaderOffset, zipBuffer.longBuffer)); // nbWritten += 4; long nbWritten = 46; // file name out.write(name); nbWritten += name.length; // extra field out.write(extra); nbWritten += extra.length; // file comment out.write(commentB); nbWritten += commentB.length; return nbWritten; } /** * Writes the 'version needed to extract' (2 bytes) and 'general purpose bit flag' (2 bytes) fields. * * @param out the OutputStream to write the fields to * @param encoding the encoding used for writing the filename and optional comment * @param useDataDescriptor true if a data descriptor is used for the entry * @return the number of bytes that were written, i.e. 4 * @throws IOException if an I/O error occurred */ protected static long writeVersionAndGPBF(OutputStream out, String encoding, boolean useDataDescriptor) throws IOException { boolean isUTF8 = isUTF8(encoding); // General purpose bit flag : // Bit 11 signals UTF-8 is used // Bit 3 signals a data descriptor is used if (useDataDescriptor) { // requires version 2 as we are going to store length info in the data descriptor out.write(SHORT_20); // General purpose bit flag out.write(isUTF8? SHORT_2056 // Bit 3 | Bit 11 = 2056 :SHORT_8 // Bit 3 = 8 ); } else { // Version out.write(SHORT_10); // General purpose bit flag out.write(isUTF8? SHORT_2048 // Bit 11 = 2048 :SHORT_0 // No bit set ); } return 4; } /** * Writes the end of the central directory record. * * @param out the OutputStream to write the end of the central directory record to * @param nbEntries number of entries the Zip file contains * @param cdLength length (in bytes) of the central directory record * @param cdOffset offset from the beginning of the Zip file to the start of the central directory record * @param comment the optional Zip file comment * @param encoding the encoding to use for writing the optional Zip comment * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred */ protected static void writeCentralDirectoryEnd(OutputStream out, int nbEntries, long cdLength, long cdOffset, String comment, String encoding, ZipBuffer zipBuffer) throws IOException { out.write(EOCD_SIG); // disk numbers out.write(LONG_0); // 2x SHORT_0 // number of entries ZipShort.getBytes(nbEntries, zipBuffer.shortBuffer); out.write(zipBuffer.shortBuffer); out.write(zipBuffer.shortBuffer); // length and location of CD out.write(ZipLong.getBytes(cdLength, zipBuffer.longBuffer)); out.write(ZipLong.getBytes(cdOffset, zipBuffer.longBuffer)); // ZIP file comment byte[] data = getBytes(comment, encoding); out.write(ZipShort.getBytes(data.length, zipBuffer.shortBuffer)); out.write(data); } /** * Retrieve the bytes for the given String in the encoding set for * this Stream. * @param name the string to get bytes from * @param encoding the encoding the string is encoded with * @return the bytes as a byte array * @throws ZipException on error */ protected static byte[] getBytes(String name, String encoding) throws ZipException { if (encoding == null) { return name.getBytes(); } else { try { return name.getBytes(encoding); } catch (UnsupportedEncodingException uee) { throw new ZipException(uee.getMessage()); } } } /** * Returns a long that is the unsigned intepretation of the given (signed) int. * * @param i the value to treat as unsigned int * @return the unsigned int as a long */ protected static long adjustToLong(int i) { return i & 0xFFFFFFFFl; } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// /** * Writes the given bytes to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * <code>OutputStream</code>. * * @param b the byte array to write * @param offset the start position to write from * @param length the number of bytes to write * @throws IOException on error */ @Override public void write(byte[] b, int offset, int length) throws IOException { (zeos==null?out:zeos).write(b, offset, length); } /** * Writes the given bytes to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * <code>OutputStream</code>. * * @param b the byte array to write * @throws IOException on error */ @Override public void write(byte[] b) throws IOException { (zeos==null?out:zeos).write(b, 0, b.length); } /** * Writes a single byte to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * <code>OutputStream</code>. * * <p>Delegates to the three arg method.</p> * @param b the byte to write * @throws IOException on error */ @Override public void write(int b) throws IOException { (zeos==null?out:zeos).write(b); } /** * Closes this output stream and releases any system resources associated with the stream. * * @exception IOException if an I/O error occurs. */ @Override public void close() throws IOException { finish(); if(deflaterBuf !=null) { // Only if close() has not already been called already BufferPool.releaseByteArray(deflaterBuf); deflaterBuf = null; } out.close(); } /** * Flushes this output stream and forces any buffered output bytes to be written out to the stream. * * @exception IOException if an I/O error occurs. */ @Override public void flush() throws IOException { out.flush(); } }