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

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

Go to download

TrueZIP is a Java based Virtual File System (VFS) to enable transparent, multi-threaded read/write access to archive files (ZIP, TAR etc.) as if they were directories. Archive files may be arbitrarily nested and the nesting level is only limited by heap and file system size.

The newest version!
/*
 * 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 - 2024 Weber Informatics LLC | Privacy Policy