SquirrelJME/SquirrelJME

View on GitHub
modules/zip/src/main/java/net/multiphasicapps/zip/streamwriter/ZipStreamWriter.java

Summary

Maintainability
A
0 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package net.multiphasicapps.zip.streamwriter;

import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.LinkedList;
import net.multiphasicapps.io.CRC32Calculator;
import net.multiphasicapps.io.DataEndianess;
import net.multiphasicapps.io.ExtendedDataOutputStream;
import net.multiphasicapps.zip.ZipCRCConstants;
import net.multiphasicapps.zip.ZipCompressionType;

/**
 * This class is used to write to ZIP files in an unknown and stream based
 * manner where the size of the contents is completely unknown.
 *
 * When the stream is closed, the central directory of the ZIP file will be
 * written to the end of the file.
 *
 * This class is not thread safe.
 *
 * @since 2016/07/09
 */
public class ZipStreamWriter
    implements Closeable
{
    /** The magic number for local files. */
    private static final int _LOCAL_FILE_MAGIC_NUMBER =
        0x04034B50;
    
    /** Data descriptor magic. */
    private static final int _DATA_DESCRIPTOR_MAGIC_NUMBER =
        0x08074B50;
    
    /** Central directory entry magic number. */
    private static final int _CENTRAL_DIRECTORY_MAGIC_NUMBER =
        0x02014B50;
    
    /** End of central directory magic number. */
    private static final int _END_DIRECTORY_MAGIC_NUMBER =
        0x06054B50;
    
    /** The maximum permitted file size. */
    private static final long _MAX_FILE_SIZE =
        0xFFFFFFFFL;
    
    /** General purpose flags for entries (use descriptor; UTF-8 names). */
    private static final int _GENERAL_PURPOSE_FLAGS =
        (1 << 3) | (1 << 11);
    
    /** The output stream to write to. */
    protected final ExtendedDataOutputStream output;
    
    /** Table of contents. */
    private final LinkedList<__TOCEntry__> _toc =
        new LinkedList<>();
    
    /** Was this stream closed? */
    private boolean _closed;
    
    /** The current entry output (the inner portion). */
    private __InnerOutputStream__ _inner;
    
    /** The current entry output (the outer portion). */
    private __OuterOutputStream__ _outer;
    
    /** The best version number. */
    private int _bestversion =
        Math.max(20, ZipCompressionType.DEFLATE.extractVersion());
    
    /**
     * This initializes the stream for writing ZIP file data.
     *
     * @param __os The output stream to write to.
     * @throws NullPointerException On null arguments.
     * @since 2016/07/09
     */
    public ZipStreamWriter(OutputStream __os)
        throws NullPointerException
    {
        // Check
        if (__os == null)
            throw new NullPointerException("NARG");
        
        // Create stream
        ExtendedDataOutputStream output;
        this.output = (output = new ExtendedDataOutputStream(__os));
        
        // Use little endian data by default
        output.setEndianess(DataEndianess.LITTLE);
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/07/09
     */
    @Override
    public void close()
        throws IOException
    {
        // Do nothing if already closed
        if (this._closed)
            return;
        
        /* {@squirreljme.error BF16 Cannot close the ZIP writer because
        an entry is still being written.} */
        if (this._inner != null || this._outer != null)
            throw new IOException("BF16");
        
        // Mark closed to prevent failing closes from writing multiple
        // times
        this._closed = true;
        
        // Get output and the TOC entries
        ExtendedDataOutputStream output = this.output;
        LinkedList<__TOCEntry__> toc = this._toc;
        int numtoc = toc.size();
        
        // The position where the central directory starts
        long cdstart = output.size();
        
        // The current time all entries will use for their date
        // 0bhhhhh_mmmmmm_sssss
        // 0byyyyyy_mmmm_ddddd
        Debugging.todoNote("Implement correct timestamp.", new Object[] {});
        int time = 0b01111_011110_00000,
            date = 0b100110_0011_01000;
        
        // Write all entries
        int bestversion = this._bestversion;
        for (__TOCEntry__ entry : toc)
        {
            // The entry position
            long epos = output.size();
            
            if (epos > ZipStreamWriter._MAX_FILE_SIZE)
                throw new IOException();
            
            // Write directory header
            output.writeInt(ZipStreamWriter._CENTRAL_DIRECTORY_MAGIC_NUMBER);
            
            // The created by version (use the highest version)
            output.writeShort(bestversion);
            
            // Version needed to extract
            ZipCompressionType ecomp = entry._compression;
            output.writeShort(ecomp.extractVersion());
            
            // General purpose flags
            output.writeShort(ZipStreamWriter._GENERAL_PURPOSE_FLAGS);
            
            // Compression method
            output.writeShort(ecomp.method());
            
            // Date/time ZIP was created (closed)
            output.writeShort(time);
            output.writeShort(date);
            
            // CRC and sizes
            output.writeInt(entry._crc);
            output.writeInt((int)entry._compressed);
            output.writeInt((int)entry._uncompressed);
            
            // Write name length
            byte[] efn = entry._name;
            output.writeShort(efn.length);
            
            // No extra data
            output.writeShort(0);
            
            // No comment
            output.writeShort(0);
            
            // Always the first disk
            output.writeShort(0);
            
            // No iternal or external attributes
            output.writeShort(0);
            output.writeInt(0);
            
            // Relative offset to local header
            output.writeInt((int)entry._localposition);
            
            // Write file name
            output.write(efn);
        }
        
        // The position where it ends
        long cdend = output.size();
        
        // Write magic number
        output.writeInt(ZipStreamWriter._END_DIRECTORY_MAGIC_NUMBER);
        
        // Only a single disk is written
        output.writeShort(0);
        output.writeShort(0);
        
        // Number of entries on this disk and in all of them
        output.writeShort(numtoc);
        output.writeShort(numtoc);
        
        // The size of the central directory
        output.writeInt((int)(cdend - cdstart));
        
        // Offset to the central directory
        output.writeInt((int)cdstart);
        
        // No comment
        output.writeShort(0);
    }
    
    /**
     * Flushes the output.
     *
     * @throws IOException If it could not be flushed.
     * @since 2016/07/09
     */
    public void flush()
        throws IOException
    {
        this.output.flush();
    }
    
    /**
     * Starts writing a new entry in the output ZIP using the default
     * compression.
     *
     * @param __name The name of the entry.
     * @return An {@link OutputStream} which is used to write the ZIP file
     * data.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2016/12/27
     */
    public OutputStream nextEntry(String __name)
        throws IOException, NullPointerException
    {
        return this.nextEntry(__name, ZipCompressionType.DEFAULT_COMPRESSION);
    }
    
    /**
     * Starts writing a new entry in the output ZIP.
     *
     * @param __name The name of the entry.
     * @param __comp The compression method used.
     * @return An {@link OutputStream} which is used to write the ZIP file
     * data.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2016/07/15
     */
    public OutputStream nextEntry(String __name, ZipCompressionType __comp)
        throws IOException, NullPointerException
    {
        // Check
        if (__name == null || __comp == null)
            throw new NullPointerException("NARG");
        
        // Lock
        LinkedList<__TOCEntry__> toc = this._toc;
        
        /* {@squirreljme.error BF17 Cannot write new entry because the ZIP
        has been closed.} */
        if (this._closed)
            throw new IOException("BF17");
        
        /* {@squirreljme.error BF18 Cannot create a new entry for output
        because the previous entry has not be closed.} */
        if (this._inner != null || this._outer != null)
            throw new IOException("BF18");
        
        /* {@squirreljme.error BF19 A ZIP file cannot have more than
        65536 entries.} */
        if (toc.size() >= 65535)
            throw new IOException("BF19");
        
        /* {@squirreljme.error BF1a The length of the input file exceeds
        65535 UTF-8 characters. (The filename length)} */
        byte[] utfname = __name.getBytes("utf-8");
        int fnn;
        if ((fnn = utfname.length) > 65535)
            throw new IOException(String.format("BF1a %d", fnn));
        
        // Setup contents
        __TOCEntry__ last = new __TOCEntry__(this.output.size(), utfname,
            __comp);
        toc.addLast(last);
        
        // Write ZIP header data
        ExtendedDataOutputStream output = this.output;
        output.writeInt(ZipStreamWriter._LOCAL_FILE_MAGIC_NUMBER);
        
        // Extract version
        output.writeShort(__comp.extractVersion());
        
        // General purpose flag
        output.writeShort(ZipStreamWriter._GENERAL_PURPOSE_FLAGS);
        
        // Method
        output.writeShort(__comp.method());
        
        // Modification date/time
        output.writeShort(0);
        output.writeShort(0);
        
        // CRC-32 and compress/uncompressed size are unknown
        output.writeInt(0);
        output.writeInt(0);
        output.writeInt(0);
        
        // Write file name bytes
        output.writeShort(fnn);
        
        // No extra field
        output.writeShort(0);
        
        // Write file name
        output.write(utfname);
        
        // Setup inner stream (for compressed size)
        __InnerOutputStream__ inner = new __InnerOutputStream__();
        
        // Wrap inner with the compression algorithm
        OutputStream wrapped = __comp.outputStream(inner);
        
        // Wrap that with the outer stream (uncompressed size)
        __OuterOutputStream__ outer = new __OuterOutputStream__(wrapped);
        
        // Set
        this._inner = inner;
        this._outer = outer;
        
        // Return the outer stream
        return outer;
    }
    
    /**
     * Closes the current entry.
     *
     * @throws IOException If it could not be closed.
     * @since 2016/07/15
     */
    private void __closeEntry()
        throws IOException
    {
        // Lock
        LinkedList<__TOCEntry__> toc = this._toc;
        
        __InnerOutputStream__ inner = this._inner;
        __OuterOutputStream__ outer = this._outer;
        
        /* {@squirreljme.error BF1b Cannot close entry because a current
        one is not being used.} */
        if (inner == null || outer == null)
            throw new IOException("BF1b");
        
        // Flush both sides
        inner.flush();
        outer.flush();
        
        // Need to fill the size information and CRC for later
        __TOCEntry__ last = toc.getLast();
        
        // Get sizes
        long uncomp = outer._size;
        long comp = inner._size;
        
        /* {@squirreljme.error BF1c Either one or both of the compressed
        or uncompressed file sizes exceeds 4GiB. (The uncompressed size;
        The compressed size)} */
        if (uncomp >= ZipStreamWriter._MAX_FILE_SIZE || comp >= ZipStreamWriter._MAX_FILE_SIZE)
            throw new IOException(String.format("BF1c %d %d", uncomp,
                comp));
        
        // Store sizes
        last._uncompressed = uncomp;
        last._compressed = comp;
        
        // Determine CRC
        int crc = outer.crccalc.checksum();
        last._crc = crc;
        
        // The magic number of the data descriptor is not needed, however
        // it helps prevent some abiguity when the input data stream is
        // not compressed and contains a ZIP file.
        ExtendedDataOutputStream output = this.output;
        output.writeInt(ZipStreamWriter._DATA_DESCRIPTOR_MAGIC_NUMBER);
        
        // Write CRC and sizes
        output.writeInt((int)crc);
        output.writeInt((int)comp);
        output.writeInt((int)uncomp);
        
        // Clear streams to allow for next entry
        this._inner = null;
        this._outer = null;
    }
    
    /**
     * The inner and outer streams are very similar.
     *
     * @since 2016/07/15
     */
    private abstract static class __BaseOutputStream__
        extends OutputStream
    {
        /** The wrapped stream. */
        protected final OutputStream wrapped;
        
        /** Is the outer side finished? */
        protected boolean finished;
        
        /** The decompressed size. */
        volatile int _size;
        
        /**
         * Initializes a new output stream for writing an entry.
         *
         * @param __os The output stream to wrap.
         * @throws NullPointerException On null arguments.
         * @since 2016/07/15
         */
        private __BaseOutputStream__(OutputStream __os)
            throws NullPointerException
        {
            // Check
            if (__os == null)
                throw new NullPointerException("NARG");
            
            // Set
            this.wrapped = __os;
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public abstract void close()
            throws IOException;
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public void flush()
            throws IOException
        {
            // Ignore if finished since the streams should be disconnected
            // at this time
            if (this.finished)
                return;
            
            // Forward flush
            this.wrapped.flush();
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public void write(int __b)
            throws IOException
        {
            /* {@squirreljme.error BF1d Cannot write a single byte because
            the stream is closed.} */
            if (this.finished)
                throw new IOException("BF1d");
            
            /* {@squirreljme.error BF1e Cannot write a single byte because
            the ZIP entry would exceed 4GiB.} */
            int oldsize = this._size, newsize = oldsize + 1;
            if (newsize < 0 || newsize < oldsize)
                throw new IOException("BF1e");
            
            // Write data and increase size
            this.wrapped.write(__b);
            this._size = newsize;
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public void write(byte[] __b, int __o, int __l)
            throws IndexOutOfBoundsException, IOException, NullPointerException
        {
            // Check
            if (__b == null)
                throw new NullPointerException("NARG");
            int n = __b.length;
            if (__o < 0 || __l < 0 || (__o + __l) > n)
                throw new IndexOutOfBoundsException("IOOB");
            
            /* {@squirreljme.error BF1f Cannot write multiple bytes because
            the stream is closed.} */
            if (this.finished)
                throw new IOException("BF1f");
            
            /* {@squirreljme.error BF1g Cannot write multiple bytes because
            the ZIP entry would exceed 4GiB.} */
            int oldsize = this._size, newsize = oldsize + __l;
            if (newsize < 0 || newsize < oldsize)
                throw new IOException("BF1g");
            
            // Write data and increase size
            this.wrapped.write(__b, __o, __l);
            this._size = newsize;
        }
    }
    
    /**
     * This is an output stream which is used when writing an entry.
     *
     * @since 2016/07/15
     */
    private final class __InnerOutputStream__
        extends __BaseOutputStream__
    {
        /**
         * Initializes a new output stream for writing an entry.
         *
         * @since 2016/07/15
         */
        private __InnerOutputStream__()
        {
            super(ZipStreamWriter.this.output);
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public final void close()
            throws IOException
        {
            // Ignore if already finished
            if (this.finished)
                return;
            
            // Close and finish
            this.finished = true;
            ZipStreamWriter.this.__closeEntry();
        }
    }
    
    /**
     * This is an output stream which is used when writing an entry.
     *
     * @since 2016/07/15
     */
    private final class __OuterOutputStream__
        extends __BaseOutputStream__
    {
        /** CRC calculation. */
        protected final CRC32Calculator crccalc =
            new CRC32Calculator(ZipCRCConstants.CRC_REFLECT_DATA,
                ZipCRCConstants.CRC_REFLECT_REMAINDER,
                ZipCRCConstants.CRC_POLYNOMIAL, ZipCRCConstants.CRC_REMAINDER,
                ZipCRCConstants.CRC_FINALXOR);
        
        /**
         * Initializes a new output stream for writing an entry.
         *
         * @param __os The output stream to wrap.
         * @since 2016/07/15
         */
        private __OuterOutputStream__(OutputStream __os)
        {
            super(__os);
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public final void close()
            throws IOException
        {
            // Ignore if already finished
            if (this.finished)
                return;
            
            // Close the wrapped stream
            this.finished = true;
            this.wrapped.close();
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/15
         */
        @Override
        public void flush()
            throws IOException
        {
            super.flush();
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/16
         */
        @Override
        public void write(int __b)
            throws IOException
        {
            // Send to output
            super.write(__b);
            
            // Calculate CRC
            this.crccalc.offer((byte)__b);
        }
        
        /**
         * {@inheritDoc}
         * @since 2016/07/16
         */
        @Override
        public void write(byte[] __b, int __o, int __l)
            throws IndexOutOfBoundsException, IOException, NullPointerException
        {
            // Send to output
            super.write(__b, __o, __l);
            
            // Calculate CRC
            this.crccalc.offer(__b, __o, __l);
        }
    }
}