All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.util.zip.BasicZipOutputStream Maven / Gradle / Ivy

/*
 * Copyright (C) 2006-2010 Schlichtherle IT Services
 *
 * Licensed 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 de.schlichtherle.util.zip;

import de.schlichtherle.io.util.LEDataOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.ZipException;

/**
 * This class is not intended for public use!
 * The methods in this class are unsynchronized and
 * {@link #entries}/{@link #getEntry} enumerate/return {@link ZipEntry}
 * instances which are shared with this class rather than clones of them.
 * This is used within TrueZIP in order to benefit from the slightly better
 * performance.
 *
 * @author Christian Schlichtherle
 * @version $Id$
 * @since TrueZIP 6.4
 * @see ZipOutputStream
 */
public class BasicZipOutputStream
        extends FilterOutputStream {

    /**
     * The default character set used for entry names and comments in ZIP
     * compatible files.
     * This is {@value} for compatibility with Sun's JDK implementation.
     * Note that you should use "IBM437" for ordinary ZIP files
     * instead.
     */
    public static final String DEFAULT_CHARSET = ZIP.DEFAULT_CHARSET;

    /** The charset to use for entry names and comments. */
    private final String charset;

    /** CRC instance to avoid parsing DEFLATED data twice. */
    private final CRC32 crc = new CRC32();

    /** This instance is used for deflated output. */
    private final ZipDeflater def = new ZipDeflater();

    /** This buffer holds deflated data for output. */
    private final byte[] dbuf = new byte[ZIP.FLATER_BUF_LENGTH];

    private final byte[] sbuf = new byte[1];

    /** The file comment. */
    private String comment = "";

    /** Default compression method for next entry. */
    private short method = ZIP.DEFLATED;

    /**
     * The list of ZIP entries started to be written so far.
     * Maps entry names to zip entries.
     */
    private final Map entries = new LinkedHashMap();

    /** Start of entry data. */
    private long dataStart;

    /** Start of central directory. */
    private long cdOffset;

    private boolean finished;

    private boolean closed;

    /** Current entry. */
    private ZipEntry entry;

    /**
     * Whether or not we need to deflate the current entry.
     * This can be used together with the {@code DEFLATED} method to
     * write already compressed entry data into the ZIP file.
     */
    private boolean deflate;

    /**
     * Creates a new ZIP output stream decorating the given output stream,
     * using the {@value #DEFAULT_CHARSET} charset.
     *
     * @throws NullPointerException If {@code out} is {@code null}.
     */
    public BasicZipOutputStream(final OutputStream out)
    throws NullPointerException {
        super(toLEDataOutputStream(out));

        // Check parameters (fail fast).
        if (out == null)
            throw new NullPointerException();

        this.charset = DEFAULT_CHARSET;
    }

    /**
     * Creates a new ZIP output stream decorating the given output stream.
     *
     * @throws NullPointerException If {@code out} or {@code charset} is
     *         {@code null}.
     * @throws UnsupportedEncodingException If {@code charset} is not supported
     *         by this JVM.
     */
    public BasicZipOutputStream(
            final OutputStream out,
            final String charset)
    throws  NullPointerException,
            UnsupportedEncodingException {
        super(toLEDataOutputStream(out));

        // Check parameters (fail fast).
        if (out == null || charset == null)
            throw new NullPointerException();
        "".getBytes(charset); // may throw UnsupportedEncodingException!

        this.charset = charset;
    }

    private static LEDataOutputStream toLEDataOutputStream(OutputStream out) {
        return out instanceof LEDataOutputStream
                ? (LEDataOutputStream) out
                : new LEDataOutputStream(out);
    }

    /**
     * Returns the charset to use for filenames and the file comment.
     */
    public String getEncoding() {
        return charset;
    }

    /**
     * Returns the number of ZIP entries written so far.
     */
    public int size() {
        return entries.size();
    }

    /**
     * Returns an enumeration of the ZIP entries written so far.
     * Note that the enumerated entries are shared with this class.
     * It is illegal to put more entries into this ZIP output stream
     * concurrently or modify the state of the enumerated entries.
     */
    public Enumeration entries() {
        return Collections.enumeration(entries.values());
    }

    /**
     * Returns the {@link ZipEntry} for the given name or
     * {@code null} if no entry with that name exists.
     * Note that the returned entry is shared with this class.
     * It is illegal to change its state!
     *
     * @param name Name of the ZIP entry.
     */
    public ZipEntry getEntry(String name) {
        return (ZipEntry) entries.get(name);
    }

    /**
     * Returns the file comment.
     */
    public String getComment() {
        return comment;
    }

    /**
     * Sets the file comment.
     */
    public void setComment(String comment) {
        this.comment = comment;
    }

    /**
     * Returns the compression level currently used.
     */
    public int getLevel() {
        return def.getLevel();
    }

    /**
     * Sets the compression level for subsequent entries.
     */
    public void setLevel(int level) {
	def.setLevel(level);
    }

    /**
     * Returns the default compression method for subsequent entries.
     * This property is only used if a {@link ZipEntry} does not specify a
     * compression method.
     *
     * @see #setMethod
     * @see ZipEntry#getMethod
     */
    public int getMethod() {
        return method;
    }

    /**
     * Sets the default compression method for subsequent entries.
     * This property is only used if a {@link ZipEntry} does not specify a
     * compression method.
     * 

* Legal values are {@link ZipEntry#STORED} (uncompressed) and * {@link ZipEntry#DEFLATED} (compressed). * The initial value is {@link ZipEntry#DEFLATED}. * * @see #getMethod * @see ZipEntry#setMethod */ public void setMethod(int method) { if (method != ZIP.STORED && method != ZIP.DEFLATED) throw new IllegalArgumentException( "Invalid compression method: " + method); this.method = (short) method; } /** * Returns the total number of (compressed) bytes this stream has written * to the underlying stream. */ public long length() { return ((LEDataOutputStream) out).size(); } /** * Returns {@code true} if and only if this * {@code BasicZipOutputStream} is currently writing a ZIP entry. */ public boolean isBusy() { return entry != null; } /** * Equivalent to * {@link #putNextEntry(ZipEntry, boolean) putNextEntry(entry, true)}. */ public final void putNextEntry(final ZipEntry entry) throws IOException { putNextEntry(entry, true); } /** * Starts writing the next ZIP entry to the underlying stream. * Note that if two or more entries with the same name are written * consecutively to this stream, the last entry written will shadow * all other entries, i.e. all of them are written to the ZIP compatible * file (and hence require space), but only the last will be accessible * from the central directory. * This is unlike the genuine {@link java.util.zip.ZipOutputStream * java.util.zip.ZipOutputStream} which would throw a {@link ZipException} * in this method when the second entry with the same name is to be written. * * @param entry The ZIP entry to write. * @param deflate Whether or not the entry data should be deflated. * This should be set to {@code false} if and only if you are * writing data which has been read from a ZIP archive file and * has not been inflated again. * The entries' properties CRC, compressed size and uncompressed * size must be set appropriately. * @throws ZipException If and only if writing the entry is impossible * because the resulting file would not comply to the ZIP file * format specification. * @throws IOException On any I/O related issue. */ public void putNextEntry(final ZipEntry entry, final boolean deflate) throws IOException { closeEntry(); final String name = entry.getName(); /*if (entries.get(name) != null) throw new ZipException(name + " (duplicate entry)");*/ { final long size = entry.getNameLength(charset) + entry.getExtraLength() + entry.getCommentLength(charset); if (size > UShort.MAX_VALUE) throw new ZipException(entry.getName() + ": sum of name, extra fields and comment too long: " + size); } int method = entry.getMethod(); if (method == ZipEntry.UNKNOWN) method = getMethod(); switch (method) { case ZIP.STORED: checkLocalFileHeaderData(entry); this.deflate = false; break; case ZIP.DEFLATED: if (!deflate) checkLocalFileHeaderData(entry); this.deflate = deflate; break; default: throw new ZipException(entry.getName() + ": unsupported compression method: " + method); } if (entry.getPlatform() == ZipEntry.UNKNOWN) entry.setPlatform(ZIP.PLATFORM_FAT); if (entry.getMethod() == ZipEntry.UNKNOWN) entry.setMethod(method); if (entry.getTime() == ZipEntry.UNKNOWN) entry.setTime(System.currentTimeMillis()); // Write LFH BEFORE putting the entry in the map. this.entry = entry; writeLocalFileHeader(); // Store entry now so that an immediate subsequent call to getEntry(...) // returns it. entries.put(name, entry); } private static void checkLocalFileHeaderData(final ZipEntry entry) throws ZipException { if (entry.getCrc() == ZipEntry.UNKNOWN) throw new ZipException("Unknown CRC Checksum!"); if (entry.getCompressedSize32() == ZipEntry.UNKNOWN) throw new ZipException("Unknown Compressed Size!"); if (entry.getSize32() == ZipEntry.UNKNOWN) throw new ZipException("Unknown Uncompressed Size!"); } /** @throws IOException On any I/O related issue. */ private void writeLocalFileHeader() throws IOException { assert entry != null; final ZipEntry entry = this.entry; final LEDataOutputStream dos = (LEDataOutputStream) out; final long crc = entry.getCrc(); final long csize = entry.getCompressedSize(); final long size = entry.getSize(); final long csize32 = entry.getCompressedSize32(); final long size32 = entry.getSize32(); final long offset = dos.size(); final boolean dd // data descriptor? = crc == ZipEntry.UNKNOWN || csize == ZipEntry.UNKNOWN || size == ZipEntry.UNKNOWN; final boolean zip64 // ZIP64 extensions? = csize >= UInt.MAX_VALUE || size >= UInt.MAX_VALUE || offset >= UInt.MAX_VALUE || ZIP.ZIP64_EXT; // Compose General Purpose Bit Flag. // See appendix D of PKWARE's ZIP File Format Specification. final boolean utf8 = ZIP.UTF8.equalsIgnoreCase(charset); final int general = (dd ? (1 << 3) : 0) | (utf8 ? (1 << 11) : 0); // Start changes. finished = false; // Local File Header Signature. dos.writeInt(ZIP.LFH_SIG); // Version Needed To Extract. dos.writeShort(zip64 ? 45 : dd ? 20 : 10); // General Purpose Bit Flag. dos.writeShort(general); // Compression Method. dos.writeShort(entry.getMethod()); // Last Mod. Time / Date in DOS format. dos.writeInt((int) entry.getDosTime()); // CRC-32. // Compressed Size. // Uncompressed Size. if (dd) { dos.writeInt(0); dos.writeInt(0); dos.writeInt(0); } else { dos.writeInt((int) crc); dos.writeInt((int) csize32); dos.writeInt((int) size32); } // File Name Length. final byte[] name = entry.getName().getBytes(charset); dos.writeShort(name.length); // Extra Field Length. final byte[] extra = entry.getExtra(!dd); assert extra != null; dos.writeShort(extra.length); // File Name. dos.write(name); // Extra Field(s). dos.write(extra); // Commit changes. entry.setGeneral(general); entry.setOffset(offset); dataStart = dos.size(); } /** * @throws IOException On any I/O related issue. */ public void write(int b) throws IOException { byte[] buf = sbuf; buf[0] = (byte) b; write(buf, 0, 1); } /** * @throws IOException On any I/O related issue. */ public void write(final byte[] b, final int off, final int len) throws IOException { if (entry != null) { if (len == 0) // let negative values pass for an exception return; if (deflate) { // Fast implementation. assert !def.finished(); def.setInput(b, off, len); while (!def.needsInput()) deflate(); crc.update(b, off, len); } else { out.write(b, off, len); if (entry.getMethod() != ZIP.DEFLATED) crc.update(b, off, len); } } else { out.write(b, off, len); } } private void deflate() throws IOException { final int dlen = def.deflate(dbuf, 0, dbuf.length); if (dlen > 0) out.write(dbuf, 0, dlen); } /** * Writes all necessary data for this entry to the underlying stream. * * @throws ZipException If and only if writing the entry is impossible * because the resulting file would not comply to the ZIP file * format specification. * @throws IOException On any I/O related issue. */ public void closeEntry() throws IOException { if (entry == null) return; switch (entry.getMethod()) { case ZIP.STORED: final long expectedCrc = crc.getValue(); if (expectedCrc != entry.getCrc()) { throw new ZipException(entry.getName() + ": bad CRC-32: 0x" + Long.toHexString(entry.getCrc()) + " expected: 0x" + Long.toHexString(expectedCrc)); } final long written = ((LEDataOutputStream) out).size(); final long entrySize = written - dataStart; if (entry.getSize() != entrySize) { throw new ZipException(entry.getName() + ": bad Uncompressed Size: " + entry.getSize() + " expected: " + entrySize); } break; case ZIP.DEFLATED: if (deflate) { assert !def.finished(); def.finish(); while (!def.finished()) deflate(); entry.setCrc(crc.getValue()); entry.setCompressedSize(def.getBytesWritten()); entry.setSize(def.getBytesRead()); def.reset(); } else { // Note: There is no way to check whether the written // data matches the crc, the compressed size and the // uncompressed size! } break; default: throw new ZipException(entry.getName() + ": unsupported Compression Method: " + entry.getMethod()); } writeDataDescriptor(); flush(); crc.reset(); entry = null; } /** * @throws IOException On any I/O related issue. */ private void writeDataDescriptor() throws IOException { final ZipEntry entry = this.entry; assert entry != null; if (!entry.getGeneralBit(3)) return; final LEDataOutputStream dos = (LEDataOutputStream) out; final long crc = entry.getCrc(); final long csize = entry.getCompressedSize(); final long size = entry.getSize(); final long offset = entry.getOffset(); // Offset MUST be considered in decision about ZIP64 format - see // description of Data Descriptor in ZIP File Format Specification! final boolean zip64 // ZIP64 extensions? = csize >= UInt.MAX_VALUE || size >= UInt.MAX_VALUE || offset >= UInt.MAX_VALUE || ZIP.ZIP64_EXT; // Data Descriptor Signature. dos.writeInt(ZIP.DD_SIG); // CRC-32. dos.writeInt((int) crc); // Compressed Size. // Uncompressed Size. if (zip64) { dos.writeLong(csize); dos.writeLong(size); } else { dos.writeInt((int) csize); dos.writeInt((int) size); } } /** * Closes the current entry and writes the Central Directory to the * underlying output stream. *

* Notes: *

    *
  • The underlying stream is not closed.
  • *
  • Unlike Sun's implementation in J2SE 1.4.2, you may continue to use * this ZIP output stream with putNextEntry(...) and the like. * When you finally close the stream, the central directory will * contain all entries written.
  • *
* * @throws ZipException If and only if writing the entry is impossible * because the resulting file would not comply to the ZIP file * format specification. * @throws IOException On any I/O related issue. */ public void finish() throws IOException { if (finished) return; // Order is important here! finished = true; closeEntry(); final LEDataOutputStream dos = (LEDataOutputStream) out; cdOffset = dos.size(); for (final Iterator i = entries.values().iterator(); i.hasNext(); ) writeCentralFileHeader((ZipEntry) i.next()); writeEndOfCentralDirectory(); } /** * Writes a Central File Header record. * * @throws IOException On any I/O related issue. */ private void writeCentralFileHeader(final ZipEntry entry) throws IOException { assert entry != null; final LEDataOutputStream dos = (LEDataOutputStream) out; final long csize32 = entry.getCompressedSize32(); final long size32 = entry.getSize32(); final long offset32 = entry.getOffset32(); final boolean dd = entry.getGeneralBit(3); final boolean zip64 // ZIP64 extensions? = csize32 >= UInt.MAX_VALUE || size32 >= UInt.MAX_VALUE || offset32 >= UInt.MAX_VALUE || ZIP.ZIP64_EXT; // Central File Header. dos.writeInt(ZIP.CFH_SIG); // Version Made By. dos.writeShort((entry.getPlatform() << 8) | 63); // Version Needed To Extract. dos.writeShort(zip64 ? 45 : dd ? 20 : 10); // General Purpose Bit Flag. dos.writeShort(entry.getGeneral()); // Compression Method. dos.writeShort(entry.getMethod()); // Last Mod. File Time / Date. dos.writeInt((int) entry.getDosTime()); // CRC-32. // Compressed Size. // Uncompressed Size. dos.writeInt((int) entry.getCrc()); dos.writeInt((int) csize32); dos.writeInt((int) size32); // File Name Length. final byte[] name = entry.getName().getBytes(charset); dos.writeShort(name.length); // Extra Field Length. final byte[] extra = entry.getExtra(); assert extra != null; dos.writeShort(extra.length); // File Comment Length. String comment = entry.getComment(); if (comment == null) comment = ""; final byte[] data = comment.getBytes(charset); dos.writeShort(data.length); // Disk Number Start. dos.writeShort(0); // Internal File Attributes. dos.writeShort(0); // External File Attributes. dos.writeInt(entry.isDirectory() ? 0x10 : 0); // fixed issue #27. // Relative Offset Of Local File Header. dos.writeInt((int) offset32); // File Name. dos.write(name); // Extra Field(s). dos.write(extra); // File Comment. dos.write(data); } /** * Writes the End Of Central Directory record. * * @throws IOException On any I/O related issue. */ private void writeEndOfCentralDirectory() throws IOException { final LEDataOutputStream dos = (LEDataOutputStream) out; final long cdEntries = entries.size(); final long cdSize = dos.size() - cdOffset; final long cdOffset = this.cdOffset; final boolean cdEntriesZip64 = cdEntries > UShort.MAX_VALUE || ZIP.ZIP64_EXT; final boolean cdSizeZip64 = cdSize > UInt .MAX_VALUE || ZIP.ZIP64_EXT; final boolean cdOffsetZip64 = cdOffset > UInt .MAX_VALUE || ZIP.ZIP64_EXT; final int cdEntries16 = cdEntriesZip64 ? UShort.MAX_VALUE : (int) cdEntries; final long cdSize32 = cdSizeZip64 ? UInt .MAX_VALUE : cdSize; final long cdOffset32 = cdOffsetZip64 ? UInt .MAX_VALUE : cdOffset; final boolean zip64 // ZIP64 extensions? = cdEntriesZip64 || cdSizeZip64 || cdOffsetZip64; if (zip64) { final long zip64eocdrOffset // relative offset of the zip64 end of central directory record = dos.size(); // ZIP64 End Of Central Directory Record Signature. dos.writeInt(ZIP.ZIP64_EOCDR_SIG); // Size Of ZIP64 End Of Central Directory Record. dos.writeLong(ZIP.ZIP64_EOCDR_MIN_LEN - 12); // Version Made By. dos.writeShort(63); // Version Needed To Extract. dos.writeShort(45); // Number Of This Disk. dos.writeInt(0); // Number Of The Disk With The Start Of The Central Directory. dos.writeInt(0); // Total Number Of Entries In The Central Directory On This Disk. dos.writeLong(cdEntries); // Total Number Of Entries In The Central Directory. dos.writeLong(cdEntries); // Size Of The Central Directory. dos.writeLong(cdSize); // Offset Of Start Of Central Directory With Respect To The // Starting Disk Number. dos.writeLong(cdOffset); // ZIP64 End Of Central Directory Locator Signature. dos.writeInt(ZIP.ZIP64_EOCDL_SIG); // Number Of The Disk With The Start Of The ZIP64 End Of Central Directory. dos.writeInt(0); // Relative Offset Of The ZIP64 End Of Central Directory Record. dos.writeLong(zip64eocdrOffset); // Total Number Of Disks. dos.writeInt(1); } // End Of Central Directory Record Signature. dos.writeInt(ZIP.EOCDR_SIG); // Disk numbers. dos.writeShort(0); dos.writeShort(0); // Number of entries. dos.writeShort(cdEntries16); dos.writeShort(cdEntries16); // Length and offset of Central Directory. dos.writeInt((int) cdSize32); dos.writeInt((int) cdOffset32); // ZIP file comment. String comment = getComment(); if (comment == null) comment = ""; final byte[] data = comment.getBytes(charset); dos.writeShort(data.length); dos.write(data); } /** * Closes this output stream and releases any system resources * associated with the stream. * This closes the open output stream writing to this ZIP file, * if any. * * @throws IOException On any I/O related issue. */ public void close() throws IOException { if (closed) return; // Order is important here! closed = true; try { finish(); } finally { entries.clear(); super.close(); } } /** * A Deflater which can be asked for its current deflation level and * counts input and output data length as a long integer value. */ private static class ZipDeflater extends Deflater { private int level = Deflater.DEFAULT_COMPRESSION; private long read = 0, written = 0; public ZipDeflater() { super(Deflater.DEFAULT_COMPRESSION, true); } public void setInput(byte[] b, int off, int len) { super.setInput(b, off, len); read += len; } public int getLevel() { return level; } public void setLevel(int level) { super.setLevel(level); this.level = level; } public int deflate(byte[] b, int off, int len) { int dlen = super.deflate(b, off, len); written += dlen; return dlen; } /** * Returns the total number of uncompressed bytes input so far. */ public long getBytesRead() { return read; } /** * Returns the total number of compressed bytes output so far. */ public long getBytesWritten() { return written; } public void reset() { super.reset(); read = written = 0; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy