/*
 *  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 org.apache.tools.zip;

import static org.apache.tools.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION;
import static org.apache.tools.zip.ZipConstants.DWORD;
import static org.apache.tools.zip.ZipConstants.INITIAL_VERSION;
import static org.apache.tools.zip.ZipConstants.SHORT;
import static org.apache.tools.zip.ZipConstants.WORD;
import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC;
import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC_SHORT;
import static org.apache.tools.zip.ZipConstants.ZIP64_MIN_VERSION;
import static org.apache.tools.zip.ZipLong.putLong;
import static org.apache.tools.zip.ZipShort.putShort;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
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 class will try to use {@link java.io.RandomAccessFile
 * RandomAccessFile} when you know that the output is going to go to a
 * file.</p>
 *
 * <p>If RandomAccessFile cannot be used, this implementation will use
 * a Data Descriptor to store size and CRC information for {@link
 * #DEFLATED DEFLATED} entries, this means, you don't need to
 * calculate them yourself.  Unfortunately this is not possible for
 * the {@link #STORED STORED} method, here setting the CRC and
 * uncompressed size information is required before {@link
 * #putNextEntry putNextEntry} can be called.</p>
 *
 * <p>As of Apache Ant 1.9.0 it transparently supports Zip64
 * extensions and thus individual entries and archives larger than 4
 * GB or with more than 65536 entries in most cases but explicit
 * control is provided via {@link #setUseZip64}.  If the stream can not
 * user RandomAccessFile and you try to write a ZipEntry of
 * unknown size then Zip64 extensions will be disabled by default.</p>
 */
public class ZipOutputStream extends FilterOutputStream {

    private static final int BUFFER_SIZE = 512;
    private static final int LFH_SIG_OFFSET = 0;
    private static final int LFH_VERSION_NEEDED_OFFSET = 4;
    private static final int LFH_GPB_OFFSET = 6;
    private static final int LFH_METHOD_OFFSET = 8;
    private static final int LFH_TIME_OFFSET = 10;
    private static final int LFH_CRC_OFFSET = 14;
    private static final int LFH_COMPRESSED_SIZE_OFFSET = 18;
    private static final int LFH_ORIGINAL_SIZE_OFFSET = 22;
    private static final int LFH_FILENAME_LENGTH_OFFSET = 26;
    private static final int LFH_EXTRA_LENGTH_OFFSET = 28;
    private static final int LFH_FILENAME_OFFSET = 30;
    private static final int CFH_SIG_OFFSET = 0;
    private static final int CFH_VERSION_MADE_BY_OFFSET = 4;
    private static final int CFH_VERSION_NEEDED_OFFSET = 6;
    private static final int CFH_GPB_OFFSET = 8;
    private static final int CFH_METHOD_OFFSET = 10;
    private static final int CFH_TIME_OFFSET = 12;
    private static final int CFH_CRC_OFFSET = 16;
    private static final int CFH_COMPRESSED_SIZE_OFFSET = 20;
    private static final int CFH_ORIGINAL_SIZE_OFFSET = 24;
    private static final int CFH_FILENAME_LENGTH_OFFSET = 28;
    private static final int CFH_EXTRA_LENGTH_OFFSET = 30;
    private static final int CFH_COMMENT_LENGTH_OFFSET = 32;
    private static final int CFH_DISK_NUMBER_OFFSET = 34;
    private static final int CFH_INTERNAL_ATTRIBUTES_OFFSET = 36;
    private static final int CFH_EXTERNAL_ATTRIBUTES_OFFSET = 38;
    private static final int CFH_LFH_OFFSET = 42;
    private static final int CFH_FILENAME_OFFSET = 46;

    /**
     * indicates if this archive is finished.
     */
    private boolean finished = false;

    /*
     * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs
     * when it gets handed a really big buffer.  See
     * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396
     *
     * Using a buffer size of 8 kB proved to be a good compromise
     */
    private static final int DEFLATER_BLOCK_SIZE = 8192;

    /**
     * Compression method for deflated entries.
     *
     * @since 1.1
     */
    public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;

    /**
     * Default compression level for deflated entries.
     *
     * @since Ant 1.7
     */
    public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;

    /**
     * Compression method for stored entries.
     *
     * @since 1.1
     */
    public static final int STORED = java.util.zip.ZipEntry.STORED;

    /**
     * default encoding for file names and comment.
     */
    static final String DEFAULT_ENCODING = null;

    /**
     * General purpose flag, which indicates that filenames are
     * written in utf-8.
     * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead
     */
    @Deprecated
    public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG;

    private static final byte[] EMPTY = new byte[0];

    /**
     * Current entry.
     *
     * @since 1.1
     */
    private CurrentEntry entry;

    /**
     * The file comment.
     *
     * @since 1.1
     */
    private String comment = "";

    /**
     * Compression level for next entry.
     *
     * @since 1.1
     */
    private int level = 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 = java.util.zip.ZipEntry.DEFLATED;

    /**
     * List of ZipEntries written so far.
     *
     * @since 1.1
     */
    private final List<ZipEntry> entries = new LinkedList<ZipEntry>();

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

    /**
     * Count the bytes written to out.
     *
     * @since 1.1
     */
    private long written = 0;

    /**
     * Start of central directory.
     *
     * @since 1.1
     */
    private long cdOffset = 0;

    /**
     * Length of central directory.
     *
     * @since 1.1
     */
    private long cdLength = 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};

    private static final byte[] ONE = ZipLong.getBytes(1L);

    /**
     * Holds the offsets of the LFH starts for each entry.
     *
     * @since 1.1
     */
    private final Map<ZipEntry, Long> offsets = new HashMap<ZipEntry, Long>();

    /**
     * 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>
     *
     * @since 1.3
     */
    private String encoding = null;

    /**
     * The zip encoding to use for filenames and the file comment.
     * <p>
     * This field is of internal use and will be set in {@link
     * #setEncoding(String)}.
     * </p>
     */
    private ZipEncoding zipEncoding =
        ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING);

   // CheckStyle:VisibilityModifier OFF - bc

    /**
     * This Deflater object is used for output.
     *
     */
    protected final Deflater def = new Deflater(level, true);

    /**
     * This buffer serves as a Deflater.
     *
     * <p>This attribute is only protected to provide a level of API
     * backwards compatibility.  This class used to extend {@link
     * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
     * Revision 1.13.</p>
     *
     * @since 1.14
     */
    protected byte[] buf = new byte[BUFFER_SIZE];

    // CheckStyle:VisibilityModifier ON

    /**
     * Optional random access output.
     *
     * @since 1.14
     */
    private final RandomAccessFile raf;

    /**
     * whether to use the general purpose bit flag when writing UTF-8
     * filenames or not.
     */
    private boolean useUTF8Flag = true;

    /**
     * Whether to encode non-encodable file names as UTF-8.
     */
    private boolean fallbackToUTF8 = false;

    /**
     * whether to create UnicodePathExtraField-s for each entry.
     */
    private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;

    /**
     * Whether anything inside this archive has used a ZIP64 feature.
     */
    private boolean hasUsedZip64 = false;

    private Zip64Mode zip64Mode = Zip64Mode.AsNeeded;

    private final Calendar calendarInstance = Calendar.getInstance();

    /**
     * Creates a new ZIP OutputStream filtering the underlying stream.
     * @param out the outputstream to zip
     * @since 1.1
     */
    public ZipOutputStream(OutputStream out) {
        super(out);
        this.raf = null;
    }

    /**
     * Creates a new ZIP OutputStream writing to a File.  Will use
     * random access if possible.
     * @param file the file to zip to
     * @since 1.14
     * @throws IOException on error
     */
    public ZipOutputStream(File file) throws IOException {
        super(null);
        RandomAccessFile _raf = null;
        try {
            _raf = new RandomAccessFile(file, "rw");
            _raf.setLength(0);
        } catch (IOException e) {
            if (_raf != null) {
                try {
                    _raf.close();
                } catch (IOException inner) { // NOPMD
                    // ignore
                }
                _raf = null;
            }
            out = new FileOutputStream(file);
        }
        raf = _raf;
    }

    /**
     * This method indicates whether this archive is writing to a
     * seekable stream (i.e., to a random access file).
     *
     * <p>For seekable streams, you don't need to calculate the CRC or
     * uncompressed size for {@link #STORED} entries before
     * invoking {@link #putNextEntry}.
     * @return true if seekable
     * @since 1.17
     */
    public boolean isSeekable() {
        return raf != null;
    }

    /**
     * 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
     * @since 1.3
     */
    public void setEncoding(final String encoding) {
        this.encoding = encoding;
        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
        if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) {
            useUTF8Flag = false;
        }
    }

    /**
     * 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;
    }

    /**
     * Whether to set the language encoding flag if the file name
     * encoding is UTF-8.
     *
     * <p>Defaults to true.</p>
     *
     * @param b boolean
     */
    public void setUseLanguageEncodingFlag(boolean b) {
        useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding);
    }

    /**
     * Whether to create Unicode Extra Fields.
     *
     * <p>Defaults to NEVER.</p>
     *
     * @param b boolean
     */
    public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) {
        createUnicodeExtraFields = b;
    }

    /**
     * Whether to fall back to UTF and the language encoding flag if
     * the file name cannot be encoded using the specified encoding.
     *
     * <p>Defaults to false.</p>
     *
     * @param b boolean
     */
    public void setFallbackToUTF8(boolean b) {
        fallbackToUTF8 = b;
    }

    /**
     * Whether Zip64 extensions will be used.
     *
     * <p>When setting the mode to {@link Zip64Mode#Never Never},
     * {@link #putNextEntry}, {@link #closeEntry}, {@link
     * #finish} or {@link #close} may throw a {@link
     * Zip64RequiredException} if the entry's size or the total size
     * of the archive exceeds 4GB or there are more than 65536 entries
     * inside the archive.  Any archive created in this mode will be
     * readable by implementations that don't support Zip64.</p>
     *
     * <p>When setting the mode to {@link Zip64Mode#Always Always},
     * Zip64 extensions will be used for all entries.  Any archive
     * created in this mode may be unreadable by implementations that
     * don't support Zip64 even if all its contents would be.</p>
     *
     * <p>When setting the mode to {@link Zip64Mode#AsNeeded
     * AsNeeded}, Zip64 extensions will transparently be used for
     * those entries that require them.  This mode can only be used if
     * the uncompressed size of the {@link ZipEntry} is known
     * when calling {@link #putNextEntry} or the archive is written
     * to a seekable output (i.e. you have used the {@link
     * #ZipOutputStream(java.io.File) File-arg constructor}) -
     * this mode is not valid when the output stream is not seekable
     * and the uncompressed size is unknown when {@link
     * #putNextEntry} is called.</p>
     *
     * <p>If no entry inside the resulting archive requires Zip64
     * extensions then {@link Zip64Mode#Never Never} will create the
     * smallest archive.  {@link Zip64Mode#AsNeeded AsNeeded} will
     * create a slightly bigger archive if the uncompressed size of
     * any entry has initially been unknown and create an archive
     * identical to {@link Zip64Mode#Never Never} otherwise.  {@link
     * Zip64Mode#Always Always} will create an archive that is at
     * least 24 bytes per entry bigger than the one {@link
     * Zip64Mode#Never Never} would create.</p>
     *
     * <p>Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless
     * {@link #putNextEntry} is called with an entry of unknown
     * size and data is written to a non-seekable stream - in this
     * case the default is {@link Zip64Mode#Never Never}.</p>
     *
     * @param mode Zip64Mode
     * @since 1.3
     */
    public void setUseZip64(Zip64Mode mode) {
        zip64Mode = mode;
    }

    /**
     * {@inheritDoc}
     * @throws Zip64RequiredException if the archive's size exceeds 4
     * GByte or there are more than 65535 entries inside the archive
     * and {@link #setUseZip64} is {@link Zip64Mode#Never}.
     */
    public void finish() throws IOException {
        if (finished) {
            throw new IOException("This archive has already been finished");
        }

        if (entry != null) {
            closeEntry();
        }

        cdOffset = written;
        writeCentralDirectoryInChunks();
        cdLength = written - cdOffset;
        writeZip64CentralDirectory();
        writeCentralDirectoryEnd();
        offsets.clear();
        entries.clear();
        def.end();
        finished = true;
    }

    private void writeCentralDirectoryInChunks() throws IOException {
        final int NUM_PER_WRITE = 1000;
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(70 * NUM_PER_WRITE);
        int count = 0;
        for (ZipEntry ze : entries) {
            byteArrayOutputStream.write(createCentralFileHeader(ze));
            if (++count > NUM_PER_WRITE) {
                writeCounted(byteArrayOutputStream.toByteArray());
                byteArrayOutputStream.reset();
                count = 0;
            }
        }
        writeCounted(byteArrayOutputStream.toByteArray());
    }

    /**
     * Writes all necessary data for this entry.
     *
     * @since 1.1
     * @throws IOException on error
     * @throws Zip64RequiredException if the entry's uncompressed or
     * compressed size exceeds 4 GByte and {@link #setUseZip64}
     * is {@link Zip64Mode#Never}.
     */
    public void closeEntry() throws IOException {
        preClose();

        flushDeflater();

        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
        long bytesWritten = written - entry.dataStart;
        long realCrc = crc.getValue();
        crc.reset();

        final boolean actuallyNeedsZip64 =
            handleSizesAndCrc(bytesWritten, realCrc, effectiveMode);

        closeEntry(actuallyNeedsZip64);
    }

    private void closeEntry(boolean actuallyNeedsZip64) throws IOException {
        if (raf != null) {
            rewriteSizesAndCrc(actuallyNeedsZip64);
        }

        writeDataDescriptor(entry.entry);
        entry = null;
    }

    private void preClose() throws IOException {
        if (finished) {
            throw new IOException("Stream has already been finished");
        }

        if (entry == null) {
            throw new IOException("No current entry to close");
        }

        if (!entry.hasWritten) {
            write(EMPTY, 0, 0);
        }
    }

    /**
     * Ensures all bytes sent to the deflater are written to the stream.
     */
    private void flushDeflater() throws IOException {
        if (entry.entry.getMethod() == DEFLATED) {
            def.finish();
            while (!def.finished()) {
                deflate();
            }
        }
    }

    /**
     * Ensures the current entry's size and CRC information is set to
     * the values just written, verifies it isn't too big in the
     * Zip64Mode.Never case and returns whether the entry would
     * require a Zip64 extra field.
     *
     * @param bytesWritten long
     * @param crc long
     * @param effectiveMode Zip64Mode
     * @return boolean
     * @throws ZipException if size or CRC is incorrect
     */
    private boolean handleSizesAndCrc(long bytesWritten, long crc,
                                      Zip64Mode effectiveMode)
        throws ZipException {
        if (entry.entry.getMethod() == DEFLATED) {
            /* It turns out def.getBytesRead() returns wrong values if
             * the size exceeds 4 GB on Java < Java7
            entry.entry.setSize(def.getBytesRead());
            */
            entry.entry.setSize(entry.bytesRead);
            entry.entry.setCompressedSize(bytesWritten);
            entry.entry.setCrc(crc);

            def.reset();
        } else if (raf == null) {
            if (entry.entry.getCrc() != crc) {
                throw new ZipException("bad CRC checksum for entry "
                                       + entry.entry.getName() + ": "
                                       + Long.toHexString(entry.entry.getCrc())
                                       + " instead of "
                                       + Long.toHexString(crc));
            }

            if (entry.entry.getSize() != bytesWritten) {
                throw new ZipException("bad size for entry "
                                       + entry.entry.getName() + ": "
                                       + entry.entry.getSize()
                                       + " instead of "
                                       + bytesWritten);
            }
        } else { /* method is STORED and we used RandomAccessFile */
            entry.entry.setSize(bytesWritten);
            entry.entry.setCompressedSize(bytesWritten);
            entry.entry.setCrc(crc);
        }

        return checkIfNeedsZip64(effectiveMode);
    }

    /**
     * Ensures the current entry's size and CRC information is set to
     * the values just written, verifies it isn't too big in the
     * Zip64Mode.Never case and returns whether the entry would
     * require a Zip64 extra field.
     *
     * @param effectiveMode Zip64Mode
     * @return boolean
     * @throws ZipException if the entry is too big for Zip64Mode.Never
     */
    private boolean checkIfNeedsZip64(Zip64Mode effectiveMode)
            throws ZipException {
        final boolean actuallyNeedsZip64 = isZip64Required(entry.entry,
                                                           effectiveMode);
        if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) {
            throw new Zip64RequiredException(Zip64RequiredException
                                             .getEntryTooBigMessage(entry.entry));
        }
        return actuallyNeedsZip64;
    }

    private boolean isZip64Required(ZipEntry entry1, Zip64Mode requestedMode) {
        return requestedMode == Zip64Mode.Always || isTooLageForZip32(entry1);
    }

    private boolean isTooLageForZip32(ZipEntry zipArchiveEntry) {
        return zipArchiveEntry.getSize() >= ZIP64_MAGIC
            || zipArchiveEntry.getCompressedSize() >= ZIP64_MAGIC;
    }

    /**
     * When using random access output, write the local file header
     * and potentially the ZIP64 extra containing the correct CRC and
     * compressed/uncompressed sizes.
     *
     * @param actuallyNeedsZip64 boolean
     */
    private void rewriteSizesAndCrc(boolean actuallyNeedsZip64)
        throws IOException {
        long save = raf.getFilePointer();

        raf.seek(entry.localDataStart);
        writeOut(ZipLong.getBytes(entry.entry.getCrc()));
        if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) {
            writeOut(ZipLong.getBytes(entry.entry.getCompressedSize()));
            writeOut(ZipLong.getBytes(entry.entry.getSize()));
        } else {
            writeOut(ZipLong.ZIP64_MAGIC.getBytes());
            writeOut(ZipLong.ZIP64_MAGIC.getBytes());
        }

        if (hasZip64Extra(entry.entry)) {
            // seek to ZIP64 extra, skip header and size information
            raf.seek(entry.localDataStart + 3 * WORD + 2 * SHORT
                     + getName(entry.entry).limit() + 2 * SHORT);
            // inside the ZIP64 extra uncompressed size comes
            // first, unlike the LFH, CD or data descriptor
            writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize()));
            writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize()));

            if (!actuallyNeedsZip64) {
                // do some cleanup:
                // * rewrite version needed to extract
                raf.seek(entry.localDataStart  - 5 * SHORT);
                writeOut(ZipShort.getBytes(INITIAL_VERSION));

                // * remove ZIP64 extra so it doesn't get written
                //   to the central directory
                entry.entry.removeExtraField(Zip64ExtendedInformationExtraField
                                             .HEADER_ID);
                entry.entry.setExtra();

                // * reset hasUsedZip64 if it has been set because
                //   of this entry
                if (entry.causedUseOfZip64) {
                    hasUsedZip64 = false;
                }
            }
        }
        raf.seek(save);
    }

    /**
     * {@inheritDoc}
     * @throws Zip64RequiredException if the entry's uncompressed or
     * compressed size is known to exceed 4 GByte and {@link #setUseZip64}
     * is {@link Zip64Mode#Never}.
     */
    public void putNextEntry(ZipEntry archiveEntry) throws IOException {
        if (finished) {
            throw new IOException("Stream has already been finished");
        }

        if (entry != null) {
            closeEntry();
        }

        entry = new CurrentEntry(archiveEntry);
        entries.add(entry.entry);

        setDefaults(entry.entry);

        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
        validateSizeInformation(effectiveMode);

        if (shouldAddZip64Extra(entry.entry, effectiveMode)) {

            Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry);

            // just a placeholder, real data will be in data
            // descriptor or inserted later via RandomAccessFile
            ZipEightByteInteger size = ZipEightByteInteger.ZERO;
            ZipEightByteInteger compressedSize = ZipEightByteInteger.ZERO;
            if (entry.entry.getMethod() == STORED
                && entry.entry.getSize() != -1) {
                // actually, we already know the sizes
                size = new ZipEightByteInteger(entry.entry.getSize());
                compressedSize = size;
            }
            z64.setSize(size);
            z64.setCompressedSize(compressedSize);
            entry.entry.setExtra();
        }

        if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
            def.setLevel(level);
            hasCompressionLevelChanged = false;
        }
        writeLocalFileHeader(entry.entry);
    }

    /**
     * Provides default values for compression method and last
     * modification time.
     *
     * @param entry ZipEntry
     */
    private void setDefaults(ZipEntry entry) {
        if (entry.getMethod() == -1) { // not specified
            entry.setMethod(method);
        }

        if (entry.getTime() == -1) { // not specified
            entry.setTime(System.currentTimeMillis());
        }
    }

    /**
     * Throws an exception if the size is unknown for a stored entry
     * that is written to a non-seekable output or the entry is too
     * big to be written without Zip64 extra but the mode has been set
     * to Never.
     *
     * @param effectiveMode Zip64Mode
     */
    private void validateSizeInformation(Zip64Mode effectiveMode)
        throws ZipException {
        // Size/CRC not required if RandomAccessFile is used
        if (entry.entry.getMethod() == STORED && raf == null) {
            if (entry.entry.getSize() == -1) {
                throw new ZipException("uncompressed size is required for"
                                       + " STORED method when not writing to a"
                                       + " file");
            }
            if (entry.entry.getCrc() == -1) {
                throw new ZipException("crc checksum is required for STORED"
                                       + " method when not writing to a file");
            }
            entry.entry.setCompressedSize(entry.entry.getSize());
        }

        if ((entry.entry.getSize() >= ZIP64_MAGIC
             || entry.entry.getCompressedSize() >= ZIP64_MAGIC)
            && effectiveMode == Zip64Mode.Never) {
            throw new Zip64RequiredException(Zip64RequiredException
                                             .getEntryTooBigMessage(entry.entry));
        }
    }

    /**
     * Whether to add a Zip64 extended information extra field to the
     * local file header.
     *
     * <p>Returns true if</p>
     *
     * <ul>
     * <li>mode is Always</li>
     * <li>or we already know it is going to be needed</li>
     * <li>or the size is unknown and we can ensure it won't hurt
     * other implementations if we add it (i.e. we can erase its
     * usage</li>
     * </ul>
     *
     * @param entry ZipEntry
     * @param mode Zip64Mode
     */
    private boolean shouldAddZip64Extra(ZipEntry entry, Zip64Mode mode) {
        return mode == Zip64Mode.Always
            || entry.getSize() >= ZIP64_MAGIC
            || entry.getCompressedSize() >= ZIP64_MAGIC
            || (entry.getSize() == -1
                && raf != null && mode != Zip64Mode.Never);
    }

    /**
     * Set 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.
     * @since 1.1
     */
    public void setLevel(int level) {
        if (level < Deflater.DEFAULT_COMPRESSION
            || level > Deflater.BEST_COMPRESSION) {
            throw new IllegalArgumentException("Invalid compression level: "
                                               + level);
        }
        hasCompressionLevelChanged = (this.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
     * @since 1.1
     */
    public void setMethod(int method) {
        this.method = method;
    }

    /**
     * Whether this stream is able to write the given entry.
     *
     * <p>May return false if it is set up to use encryption or a
     * compression method that hasn't been implemented yet.</p>
     *
     * @param ae ZipEntry
     * @return boolean
     */
    public boolean canWriteEntryData(ZipEntry ae) {
        return ZipUtil.canHandleEntryData(ae);
    }

    /**
     * Writes bytes to ZIP entry.
     *
     * @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 {
        if (entry == null) {
            throw new IllegalStateException("No current entry");
        }
        ZipUtil.checkRequestedFeatures(entry.entry);
        entry.hasWritten = true;
        if (entry.entry.getMethod() == DEFLATED) {
            writeDeflated(b, offset, length);
        } else {
            writeCounted(b, offset, length);
        }
        crc.update(b, offset, length);
    }

    /**
     * Write bytes to output or random access file.
     *
     * @param data the byte array to write
     * @throws IOException on error
     */
    private void writeCounted(byte[] data) throws IOException {
        writeCounted(data, 0, data.length);
    }

    private void writeCounted(byte[] data, int offset, int length) throws IOException {
        writeOut(data, offset, length);
        written += length;
    }

    /**
     * write implementation for DEFLATED entries.
     *
     * @param b byte[]
     * @param offset int
     * @param length int
     */
    private void writeDeflated(byte[] b, int offset, int length)
        throws IOException {
        if (length > 0 && !def.finished()) {
            entry.bytesRead += length;
            if (length <= DEFLATER_BLOCK_SIZE) {
                def.setInput(b, offset, length);
                deflateUntilInputIsNeeded();
            } else {
                final int fullblocks = length / DEFLATER_BLOCK_SIZE;
                for (int i = 0; i < fullblocks; i++) {
                    def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE,
                                 DEFLATER_BLOCK_SIZE);
                    deflateUntilInputIsNeeded();
                }
                final int done = fullblocks * DEFLATER_BLOCK_SIZE;
                if (done < length) {
                    def.setInput(b, offset + done, length - done);
                    deflateUntilInputIsNeeded();
                }
            }
        }
    }

    /**
     * Closes this output stream and releases any system resources
     * associated with the stream.
     *
     * @throws IOException  if an I/O error occurs.
     * @throws Zip64RequiredException if the archive's size exceeds 4
     * GByte or there are more than 65535 entries inside the archive
     * and {@link #setUseZip64} is {@link Zip64Mode#Never}.
     */
    @Override
    public void close() throws IOException {
        if (!finished) {
            finish();
        }
        destroy();
    }

    /**
     * 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 {
        if (out != null) {
            out.flush();
        }
    }

    /*
     * Various ZIP constants
     */
    /**
     * local file header signature
     *
     * @since 1.1
     */
    protected static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); //NOSONAR
    /**
     * data descriptor signature
     *
     * @since 1.1
     */
    protected static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); //NOSONAR
    /**
     * central file header signature
     *
     * @since 1.1
     */
    protected static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); //NOSONAR
    /**
     * end of central dir signature
     *
     * @since 1.1
     */
    protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); //NOSONAR
    /**
     * ZIP64 end of central dir signature
     */
    static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); //NOSONAR
    /**
     * ZIP64 end of central dir locator signature
     */
    static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); //NOSONAR

    /**
     * Writes next block of compressed data to the output stream.
     *
     * @throws IOException on error
     * @since 1.14
     */
    protected final void deflate() throws IOException {
        int len = def.deflate(buf, 0, buf.length);
        if (len > 0) {
            writeCounted(buf, 0, len);
        }
    }

    /**
     * Writes the local file header entry
     *
     * @param ze the entry to write
     * @throws IOException on error
     * @since 1.1
     */
    protected void writeLocalFileHeader(ZipEntry ze) throws IOException {

        boolean encodable = zipEncoding.canEncode(ze.getName());
        ByteBuffer name = getName(ze);

        if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
            addUnicodeExtraFields(ze, encodable, name);
        }

        final byte[] localHeader = createLocalFileHeader(ze, name, encodable);
        final long localHeaderStart = written;
        offsets.put(ze, localHeaderStart);
        entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset
        writeCounted(localHeader);
        entry.dataStart = written;
    }

    private byte[] createLocalFileHeader(ZipEntry ze, ByteBuffer name, boolean encodable)  {
        byte[] extra = ze.getLocalFileDataExtra();
        final int nameLen = name.limit() - name.position();
        int len = LFH_FILENAME_OFFSET + nameLen + extra.length;
        byte[] buf = new byte[len];

        System.arraycopy(LFH_SIG,  0, buf, LFH_SIG_OFFSET, WORD);

        //store method in local variable to prevent multiple method calls
        final int zipMethod = ze.getMethod();

        putShort(versionNeededToExtract(zipMethod, hasZip64Extra(ze)),
                 buf, LFH_VERSION_NEEDED_OFFSET);

        GeneralPurposeBit generalPurposeBit =
            getGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8);
        generalPurposeBit.encode(buf, LFH_GPB_OFFSET);

        // compression method
        putShort(zipMethod, buf, LFH_METHOD_OFFSET);

        ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, LFH_TIME_OFFSET);

        // CRC
        if (zipMethod == DEFLATED || raf != null) {
            System.arraycopy(LZERO, 0, buf, LFH_CRC_OFFSET, WORD);
        } else {
            putLong(ze.getCrc(), buf, LFH_CRC_OFFSET);
        }

        // compressed length
        // uncompressed length
        if (hasZip64Extra(entry.entry)) {
            // point to ZIP64 extended information extra field for
            // sizes, may get rewritten once sizes are known if
            // stream is seekable
            ZipLong.ZIP64_MAGIC.putLong(buf, LFH_COMPRESSED_SIZE_OFFSET);
            ZipLong.ZIP64_MAGIC.putLong(buf, LFH_ORIGINAL_SIZE_OFFSET);
        } else if (zipMethod == DEFLATED || raf != null) {
            System.arraycopy(LZERO, 0, buf, LFH_COMPRESSED_SIZE_OFFSET, WORD);
            System.arraycopy(LZERO, 0, buf, LFH_ORIGINAL_SIZE_OFFSET, WORD);
        } else { // Stored
            putLong(ze.getSize(), buf, LFH_COMPRESSED_SIZE_OFFSET);
            putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
        }
        // file name length
        putShort(nameLen, buf, LFH_FILENAME_LENGTH_OFFSET);

        // extra field length
        putShort(extra.length, buf, LFH_EXTRA_LENGTH_OFFSET);

        // file name
        System.arraycopy(name.array(), name.arrayOffset(), buf,
                         LFH_FILENAME_OFFSET, nameLen);

        System.arraycopy(extra, 0, buf, LFH_FILENAME_OFFSET + nameLen, extra.length);
        return buf;
    }

    /**
     * Adds UnicodeExtra fields for name and file comment if mode is
     * ALWAYS or the data cannot be encoded using the configured
     * encoding.
     *
     * @param ze ZipEntry
     * @param encodable boolean
     * @param name ByteBuffer
     */
    private void addUnicodeExtraFields(ZipEntry ze, boolean encodable,
                                       ByteBuffer name)
        throws IOException {
        if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
            || !encodable) {
            ze.addExtraField(new UnicodePathExtraField(ze.getName(),
                                                       name.array(),
                                                       name.arrayOffset(),
                                                       name.limit()
                                                       - name.position()));
        }

        String comm = ze.getComment();
        if (comm != null && !"".equals(comm)) {

            boolean commentEncodable = zipEncoding.canEncode(comm);

            if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
                || !commentEncodable) {
                ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
                ze.addExtraField(new UnicodeCommentExtraField(comm,
                                                              commentB.array(),
                                                              commentB.arrayOffset(),
                                                              commentB.limit()
                                                              - commentB.position())
                                 );
            }
        }
    }

    /**
     * Writes the data descriptor entry.
     *
     * @param ze the entry to write
     * @throws IOException on error
     * @since 1.1
     */
    protected void writeDataDescriptor(ZipEntry ze) throws IOException {
        if (ze.getMethod() != DEFLATED || raf != null) {
            return;
        }
        writeCounted(DD_SIG);
        writeCounted(ZipLong.getBytes(ze.getCrc()));
        if (!hasZip64Extra(ze)) {
            writeCounted(ZipLong.getBytes(ze.getCompressedSize()));
            writeCounted(ZipLong.getBytes(ze.getSize()));
        } else {
            writeCounted(ZipEightByteInteger.getBytes(ze.getCompressedSize()));
            writeCounted(ZipEightByteInteger.getBytes(ze.getSize()));
        }
    }

    /**
     * Writes the central file header entry.
     *
     * @param ze the entry to write
     * @throws IOException on error
     * @throws Zip64RequiredException if the archive's size exceeds 4
     * GByte and {@link Zip64Mode #setUseZip64} is {@link
     * Zip64Mode#Never}.
     */
    protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
        byte[] centralFileHeader = createCentralFileHeader(ze);
        writeCounted(centralFileHeader);
    }

    private byte[] createCentralFileHeader(ZipEntry ze) throws IOException {
        final long lfhOffset = offsets.get(ze);
        final boolean needsZip64Extra = hasZip64Extra(ze)
                || ze.getCompressedSize() >= ZIP64_MAGIC
                || ze.getSize() >= ZIP64_MAGIC
                || lfhOffset >= ZIP64_MAGIC;

        if (needsZip64Extra && zip64Mode == Zip64Mode.Never) {
            // must be the offset that is too big, otherwise an
            // exception would have been throw in putArchiveEntry or
            // closeArchiveEntry
            throw new Zip64RequiredException(Zip64RequiredException
                    .ARCHIVE_TOO_BIG_MESSAGE);
        }


        handleZip64Extra(ze, lfhOffset, needsZip64Extra);

        return createCentralFileHeader(ze, getName(ze), lfhOffset, needsZip64Extra);
    }

    /**
     * Writes the central file header entry.
     *
     * @param ze the entry to write
     * @param name The encoded name
     * @param lfhOffset Local file header offset for this file
     * @throws IOException on error
     */
    private byte[] createCentralFileHeader(ZipEntry ze, ByteBuffer name, long lfhOffset,
                                           boolean needsZip64Extra) throws IOException {
        byte[] extra = ze.getCentralDirectoryExtra();

        // file comment length
        String comm = ze.getComment();
        if (comm == null) {
            comm = "";
        }

        ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
        final int nameLen = name.limit() - name.position();
        final int commentLen = commentB.limit() - commentB.position();
        int len = CFH_FILENAME_OFFSET + nameLen + extra.length + commentLen;
        byte[] buf = new byte[len];

        System.arraycopy(CFH_SIG,  0, buf, CFH_SIG_OFFSET, WORD);

        // version made by
        // CheckStyle:MagicNumber OFF
        putShort((ze.getPlatform() << 8) | (!hasUsedZip64 ? DATA_DESCRIPTOR_MIN_VERSION : ZIP64_MIN_VERSION),
                buf, CFH_VERSION_MADE_BY_OFFSET);

        final int zipMethod = ze.getMethod();
        final boolean encodable = zipEncoding.canEncode(ze.getName());
        putShort(versionNeededToExtract(zipMethod, needsZip64Extra), buf, CFH_VERSION_NEEDED_OFFSET);
        getGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8).encode(buf, CFH_GPB_OFFSET);

        // compression method
        putShort(zipMethod, buf, CFH_METHOD_OFFSET);


        // last mod. time and date
        ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, CFH_TIME_OFFSET);

        // CRC
        // compressed length
        // uncompressed length
        putLong(ze.getCrc(), buf, CFH_CRC_OFFSET);
        if (ze.getCompressedSize() >= ZIP64_MAGIC
                || ze.getSize() >= ZIP64_MAGIC) {
            ZipLong.ZIP64_MAGIC.putLong(buf, CFH_COMPRESSED_SIZE_OFFSET);
            ZipLong.ZIP64_MAGIC.putLong(buf, CFH_ORIGINAL_SIZE_OFFSET);
        } else {
            putLong(ze.getCompressedSize(), buf, CFH_COMPRESSED_SIZE_OFFSET);
            putLong(ze.getSize(), buf, CFH_ORIGINAL_SIZE_OFFSET);
        }

        putShort(nameLen, buf, CFH_FILENAME_LENGTH_OFFSET);

        // extra field length
        putShort(extra.length, buf, CFH_EXTRA_LENGTH_OFFSET);

        putShort(commentLen, buf, CFH_COMMENT_LENGTH_OFFSET);

        // disk number start
        System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, SHORT);

        // internal file attributes
        putShort(ze.getInternalAttributes(), buf, CFH_INTERNAL_ATTRIBUTES_OFFSET);

        // external file attributes
        putLong(ze.getExternalAttributes(), buf, CFH_EXTERNAL_ATTRIBUTES_OFFSET);

        // relative offset of LFH
        putLong(Math.min(lfhOffset, ZIP64_MAGIC), buf, CFH_LFH_OFFSET);

        // file name
        System.arraycopy(name.array(), name.arrayOffset(), buf, CFH_FILENAME_OFFSET, nameLen);

        int extraStart = CFH_FILENAME_OFFSET + nameLen;
        System.arraycopy(extra, 0, buf, extraStart, extra.length);

        int commentStart = extraStart + extra.length;

        // file comment
        System.arraycopy(commentB.array(), commentB.arrayOffset(), buf, commentStart, commentLen);
        return buf;
    }

    /**
     * If the entry needs Zip64 extra information inside the central
     * directory then configure its data.
     *
     * @param ze ZipEntry
     * @param lfhOffset long
     * @param needsZip64Extra boolean
     */
    private void handleZip64Extra(ZipEntry ze, long lfhOffset,
                                  boolean needsZip64Extra) {
        if (needsZip64Extra) {
            Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze);
            if (ze.getCompressedSize() >= ZIP64_MAGIC
                || ze.getSize() >= ZIP64_MAGIC) {
                z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
                z64.setSize(new ZipEightByteInteger(ze.getSize()));
            } else {
                // reset value that may have been set for LFH
                z64.setCompressedSize(null);
                z64.setSize(null);
            }
            if (lfhOffset >= ZIP64_MAGIC) {
                z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset));
            }
            ze.setExtra();
        }
    }

    /**
     * Writes the &quot;End of central dir record&quot;.
     *
     * @throws IOException on error
     * @throws Zip64RequiredException if the archive's size exceeds 4
     * GByte or there are more than 65535 entries inside the archive
     * and {@link Zip64Mode #setUseZip64} is {@link Zip64Mode#Never}.
     */
    protected void writeCentralDirectoryEnd() throws IOException {
        writeCounted(EOCD_SIG);

        // disk numbers
        writeCounted(ZERO);
        writeCounted(ZERO);

        // number of entries
        int numberOfEntries = entries.size();
        if (numberOfEntries > ZIP64_MAGIC_SHORT
            && zip64Mode == Zip64Mode.Never) {
            throw new Zip64RequiredException(Zip64RequiredException
                                             .TOO_MANY_ENTRIES_MESSAGE);
        }
        if (cdOffset > ZIP64_MAGIC && zip64Mode == Zip64Mode.Never) {
            throw new Zip64RequiredException(Zip64RequiredException
                                             .ARCHIVE_TOO_BIG_MESSAGE);
        }

        byte[] num = ZipShort.getBytes(Math.min(numberOfEntries,
                                                ZIP64_MAGIC_SHORT));
        writeCounted(num);
        writeCounted(num);

        // length and location of CD
        writeCounted(ZipLong.getBytes(Math.min(cdLength, ZIP64_MAGIC)));
        writeCounted(ZipLong.getBytes(Math.min(cdOffset, ZIP64_MAGIC)));

        // ZIP file comment
        ByteBuffer data = this.zipEncoding.encode(comment);
        int dataLen = data.limit() - data.position();
        writeCounted(ZipShort.getBytes(dataLen));
        writeCounted(data.array(), data.arrayOffset(), dataLen);
    }

    /**
     * Convert a Date object to a DOS date/time field.
     *
     * @param time the <code>Date</code> to convert
     * @return the date as a <code>ZipLong</code>
     * @since 1.1
     * @deprecated use ZipUtil#toDosTime
     */
    @Deprecated
    protected static ZipLong toDosTime(Date time) {
        return ZipUtil.toDosTime(time);
    }

    /**
     * Convert a Date object to a DOS date/time field.
     *
     * <p>Stolen from InfoZip's <code>fileio.c</code></p>
     *
     * @param t number of milliseconds since the epoch
     * @return the date as a byte array
     * @since 1.26
     * @deprecated use ZipUtil#toDosTime
     */
    @Deprecated
    protected static byte[] toDosTime(long t) {
        return ZipUtil.toDosTime(t);
    }

    /**
     * Retrieve the bytes for the given String in the encoding set for
     * this Stream.
     *
     * @param name the string to get bytes from
     * @return the bytes as a byte array
     * @throws ZipException on error
     *
     * @since 1.3
     */
    protected byte[] getBytes(String name) throws ZipException {
        try {
            ByteBuffer b =
                ZipEncodingHelper.getZipEncoding(encoding).encode(name);
            byte[] result = new byte[b.limit()];
            System.arraycopy(b.array(), b.arrayOffset(), result, 0,
                             result.length);
            return result;
        } catch (IOException ex) {
            throw new ZipException("Failed to encode name: " + ex.getMessage());
        }
    }

    /**
     * Writes the &quot;ZIP64 End of central dir record&quot; and
     * &quot;ZIP64 End of central dir locator&quot;.
     *
     * @throws IOException on error
     */
    protected void writeZip64CentralDirectory() throws IOException {
        if (zip64Mode == Zip64Mode.Never) {
            return;
        }

        if (!hasUsedZip64
            && (cdOffset >= ZIP64_MAGIC || cdLength >= ZIP64_MAGIC
                || entries.size() >= ZIP64_MAGIC_SHORT)) {
            // actually "will use"
            hasUsedZip64 = true;
        }

        if (!hasUsedZip64) {
            return;
        }

        long offset = written;

        writeOut(ZIP64_EOCD_SIG);
        // size, we don't have any variable length as we don't support
        // the extensible data sector, yet
        writeOut(ZipEightByteInteger
                 .getBytes(SHORT   /* version made by */
                           + SHORT /* version needed to extract */
                           + WORD  /* disk number */
                           + WORD  /* disk with central directory */
                           + DWORD /* number of entries in CD on this disk */
                           + DWORD /* total number of entries */
                           + DWORD /* size of CD */
                           + DWORD /* offset of CD */
                           ));

        // version made by and version needed to extract
        writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION));
        writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION));

        // disk numbers - four bytes this time
        writeOut(LZERO);
        writeOut(LZERO);

        // number of entries
        byte[] num = ZipEightByteInteger.getBytes(entries.size());
        writeOut(num);
        writeOut(num);

        // length and location of CD
        writeOut(ZipEightByteInteger.getBytes(cdLength));
        writeOut(ZipEightByteInteger.getBytes(cdOffset));

        // no "zip64 extensible data sector" for now

        // and now the "ZIP64 end of central directory locator"
        writeOut(ZIP64_EOCD_LOC_SIG);

        // disk number holding the ZIP64 EOCD record
        writeOut(LZERO);
        // relative offset of ZIP64 EOCD record
        writeOut(ZipEightByteInteger.getBytes(offset));
        // total number of disks
        writeOut(ONE);
    }

    /**
     * Write bytes to output or random access file.
     *
     * @param data the byte array to write
     * @throws IOException on error
     *
     * @since 1.14
     */
    protected final void writeOut(byte[] data) throws IOException {
        writeOut(data, 0, data.length);
    }

    /**
     * Write bytes to output or random access file.
     *
     * @param data 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
     *
     * @since 1.14
     */
    protected final void writeOut(byte[] data, int offset, int length)
        throws IOException {
        if (raf != null) {
            raf.write(data, offset, length);
        } else {
            out.write(data, offset, length);
        }
    }

    /**
     * Assumes a negative integer really is a positive integer that
     * has wrapped around and re-creates the original value.
     *
     * @param i the value to treat as unsigned int.
     * @return the unsigned int as a long.
     * @since 1.34
     * @deprecated use ZipUtil#adjustToLong
     */
    @Deprecated
    protected static long adjustToLong(int i) {
        return ZipUtil.adjustToLong(i);
    }

    private void deflateUntilInputIsNeeded() throws IOException {
        while (!def.needsInput()) {
            deflate();
        }
    }

    private GeneralPurposeBit getGeneralPurposeBits(final int zipMethod, final boolean utfFallback) {
        GeneralPurposeBit b = new GeneralPurposeBit();
        b.useUTF8ForNames(useUTF8Flag || utfFallback);
        if (isDeflatedToOutputStream(zipMethod)) {
            b.useDataDescriptor(true);
        }
        return b;
    }

    private int versionNeededToExtract(final int zipMethod, final boolean zip64) {
        if (zip64) {
            return ZIP64_MIN_VERSION;
        }
        // requires version 2 as we are going to store length info
        // in the data descriptor
        return (isDeflatedToOutputStream(zipMethod))
            ? DATA_DESCRIPTOR_MIN_VERSION : INITIAL_VERSION;
    }

    private boolean isDeflatedToOutputStream(int zipMethod) {
        return zipMethod == DEFLATED && raf == null;
    }

    /**
     * Get the existing ZIP64 extended information extra field or
     * create a new one and add it to the entry.
     *
     * @param ze ZipEntry
     * @return Zip64ExtendedInformationExtraField
     */
    private Zip64ExtendedInformationExtraField getZip64Extra(ZipEntry ze) {
        if (entry != null) {
            entry.causedUseOfZip64 = !hasUsedZip64;
        }
        hasUsedZip64 = true;
        Zip64ExtendedInformationExtraField z64 =
            (Zip64ExtendedInformationExtraField)
            ze.getExtraField(Zip64ExtendedInformationExtraField
                             .HEADER_ID);
        if (z64 == null) {
            /*
              System.err.println("Adding z64 for " + ze.getName()
              + ", method: " + ze.getMethod()
              + " (" + (ze.getMethod() == STORED) + ")"
              + ", raf: " + (raf != null));
            */
            z64 = new Zip64ExtendedInformationExtraField();
        }

        // even if the field is there already, make sure it is the first one
        ze.addAsFirstExtraField(z64);

        return z64;
    }

    /**
     * Is there a ZIP64 extended information extra field for the
     * entry?
     *
     * @param ze ZipEntry
     * @return boolean
     */
    private boolean hasZip64Extra(ZipEntry ze) {
        return ze.getExtraField(Zip64ExtendedInformationExtraField
                                .HEADER_ID)
            != null;
    }

    /**
     * If the mode is AsNeeded and the entry is a compressed entry of
     * unknown size that gets written to a non-seekable stream the
     * change the default to Never.
     *
     * @param ze ZipEntry
     * @return Zip64Mode
     */
    private Zip64Mode getEffectiveZip64Mode(ZipEntry ze) {
        if (zip64Mode != Zip64Mode.AsNeeded
            || raf != null
            || ze.getMethod() != DEFLATED
            || ze.getSize() != -1) {
            return zip64Mode;
        }
        return Zip64Mode.Never;
    }

    private ZipEncoding getEntryEncoding(ZipEntry ze) {
        boolean encodable = zipEncoding.canEncode(ze.getName());
        return !encodable && fallbackToUTF8
            ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
    }

    private ByteBuffer getName(ZipEntry ze) throws IOException {
        return getEntryEncoding(ze).encode(ze.getName());
    }

    /**
     * Closes the underlying stream/file without finishing the
     * archive, the result will likely be a corrupt archive.
     *
     * <p>This method only exists to support tests that generate
     * corrupt archives so they can clean up any temporary files.</p>
     *
     * @throws IOException if close() fails
     */
    void destroy() throws IOException {
        if (raf != null) {
            raf.close();
        }
        if (out != null) {
            out.close();
        }
    }

    /**
     * enum that represents the possible policies for creating Unicode
     * extra fields.
     */
    public static final class UnicodeExtraFieldPolicy {
        /**
         * Always create Unicode extra fields.
         */
        public static final UnicodeExtraFieldPolicy ALWAYS =
            new UnicodeExtraFieldPolicy("always");
        /**
         * Never create Unicode extra fields.
         */
        public static final UnicodeExtraFieldPolicy NEVER =
            new UnicodeExtraFieldPolicy("never");
        /**
         * Create Unicode extra fields for filenames that cannot be
         * encoded using the specified encoding.
         */
        public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE =
            new UnicodeExtraFieldPolicy("not encodeable");

        private final String name;
        private UnicodeExtraFieldPolicy(String n) {
            name = n;
        }
        @Override
        public String toString() {
            return name;
        }
    }

    /**
     * Structure collecting information for the entry that is
     * currently being written.
     */
    private static final class CurrentEntry {
        private CurrentEntry(ZipEntry entry) {
            this.entry = entry;
        }
        /**
         * Current ZIP entry.
         */
        private final ZipEntry entry;
        /**
         * Offset for CRC entry in the local file header data for the
         * current entry starts here.
         */
        private long localDataStart = 0;
        /**
         * Data for local header data
         */
        private long dataStart = 0;
        /**
         * Number of bytes read for the current entry (can't rely on
         * Deflater#getBytesRead) when using DEFLATED.
         */
        private long bytesRead = 0;
        /**
         * Whether current entry was the first one using ZIP64 features.
         */
        private boolean causedUseOfZip64 = false;
        /**
         * Whether write() has been called at all.
         *
         * <p>In order to create a valid archive {@link
         * #closeEntry closeEntry} will write an empty
         * array to get the CRC right if nothing has been written to
         * the stream at all.</p>
         */
        private boolean hasWritten;
    }

}
