001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017package org.apache.commons.compress.archivers.zip;
018
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.nio.ByteBuffer;
025import java.nio.channels.SeekableByteChannel;
026import java.nio.charset.Charset;
027import java.nio.charset.StandardCharsets;
028import java.nio.file.LinkOption;
029import java.nio.file.OpenOption;
030import java.nio.file.Path;
031import java.util.HashMap;
032import java.util.LinkedList;
033import java.util.List;
034import java.util.Map;
035import java.util.zip.Deflater;
036import java.util.zip.ZipException;
037
038import org.apache.commons.compress.archivers.ArchiveEntry;
039import org.apache.commons.compress.archivers.ArchiveOutputStream;
040import org.apache.commons.compress.utils.ByteUtils;
041
042/**
043 * Reimplementation of {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} to handle the extended functionality of this package, especially
044 * internal/external file attributes and extra fields with different layouts for local file data and central directory entries.
045 * <p>
046 * This class will try to use {@link java.nio.channels.SeekableByteChannel} when it knows that the output is going to go to a file and no split archive shall be
047 * created.
048 * </p>
049 * <p>
050 * If SeekableByteChannel cannot be used, this implementation will use a Data Descriptor to store size and CRC information for {@link #DEFLATED DEFLATED}
051 * entries, you don't need to calculate them yourself. Unfortunately, this is not possible for the {@link #STORED STORED} method, where setting the CRC and
052 * uncompressed size information is required before {@link #putArchiveEntry(ZipArchiveEntry)} can be called.
053 * </p>
054 * <p>
055 * As of Apache Commons Compress 1.3, the class transparently supports Zip64 extensions and thus individual entries and archives larger than 4 GB or with more
056 * than 65,536 entries in most cases but explicit control is provided via {@link #setUseZip64}. If the stream can not use SeekableByteChannel and you try to
057 * write a ZipArchiveEntry of unknown size, then Zip64 extensions will be disabled by default.
058 * </p>
059 *
060 * @NotThreadSafe
061 */
062public class ZipArchiveOutputStream extends ArchiveOutputStream<ZipArchiveEntry> {
063
064    /**
065     * Structure collecting information for the entry that is currently being written.
066     */
067    private static final class CurrentEntry {
068
069        /**
070         * Current ZIP entry.
071         */
072        private final ZipArchiveEntry entry;
073
074        /**
075         * Offset for CRC entry in the local file header data for the current entry starts here.
076         */
077        private long localDataStart;
078
079        /**
080         * Data for local header data
081         */
082        private long dataStart;
083
084        /**
085         * Number of bytes read for the current entry (can't rely on Deflater#getBytesRead) when using DEFLATED.
086         */
087        private long bytesRead;
088
089        /**
090         * Whether current entry was the first one using ZIP64 features.
091         */
092        private boolean causedUseOfZip64;
093
094        /**
095         * Whether write() has been called at all.
096         *
097         * <p>
098         * In order to create a valid archive {@link #closeArchiveEntry closeArchiveEntry} will write an empty array to get the CRC right if nothing has been
099         * written to the stream at all.
100         * </p>
101         */
102        private boolean hasWritten;
103
104        private CurrentEntry(final ZipArchiveEntry entry) {
105            this.entry = entry;
106        }
107    }
108
109    private static final class EntryMetaData {
110        private final long offset;
111        private final boolean usesDataDescriptor;
112
113        private EntryMetaData(final long offset, final boolean usesDataDescriptor) {
114            this.offset = offset;
115            this.usesDataDescriptor = usesDataDescriptor;
116        }
117    }
118
119    /**
120     * enum that represents the possible policies for creating Unicode extra fields.
121     */
122    public static final class UnicodeExtraFieldPolicy {
123
124        /**
125         * Always create Unicode extra fields.
126         */
127        public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always");
128
129        /**
130         * Never create Unicode extra fields.
131         */
132        public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never");
133
134        /**
135         * Creates Unicode extra fields for file names that cannot be encoded using the specified encoding.
136         */
137        public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = new UnicodeExtraFieldPolicy("not encodeable");
138
139        private final String name;
140
141        private UnicodeExtraFieldPolicy(final String n) {
142            name = n;
143        }
144
145        @Override
146        public String toString() {
147            return name;
148        }
149    }
150
151    static final int BUFFER_SIZE = 512;
152    private static final int LFH_SIG_OFFSET = 0;
153    private static final int LFH_VERSION_NEEDED_OFFSET = 4;
154    private static final int LFH_GPB_OFFSET = 6;
155    private static final int LFH_METHOD_OFFSET = 8;
156    private static final int LFH_TIME_OFFSET = 10;
157    private static final int LFH_CRC_OFFSET = 14;
158    private static final int LFH_COMPRESSED_SIZE_OFFSET = 18;
159    private static final int LFH_ORIGINAL_SIZE_OFFSET = 22;
160    private static final int LFH_FILENAME_LENGTH_OFFSET = 26;
161    private static final int LFH_EXTRA_LENGTH_OFFSET = 28;
162    private static final int LFH_FILENAME_OFFSET = 30;
163    private static final int CFH_SIG_OFFSET = 0;
164    private static final int CFH_VERSION_MADE_BY_OFFSET = 4;
165    private static final int CFH_VERSION_NEEDED_OFFSET = 6;
166    private static final int CFH_GPB_OFFSET = 8;
167    private static final int CFH_METHOD_OFFSET = 10;
168    private static final int CFH_TIME_OFFSET = 12;
169    private static final int CFH_CRC_OFFSET = 16;
170    private static final int CFH_COMPRESSED_SIZE_OFFSET = 20;
171    private static final int CFH_ORIGINAL_SIZE_OFFSET = 24;
172    private static final int CFH_FILENAME_LENGTH_OFFSET = 28;
173    private static final int CFH_EXTRA_LENGTH_OFFSET = 30;
174    private static final int CFH_COMMENT_LENGTH_OFFSET = 32;
175    private static final int CFH_DISK_NUMBER_OFFSET = 34;
176    private static final int CFH_INTERNAL_ATTRIBUTES_OFFSET = 36;
177
178    private static final int CFH_EXTERNAL_ATTRIBUTES_OFFSET = 38;
179
180    private static final int CFH_LFH_OFFSET = 42;
181
182    private static final int CFH_FILENAME_OFFSET = 46;
183
184    /**
185     * Compression method for deflated entries.
186     */
187    public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
188
189    /**
190     * Default compression level for deflated entries.
191     */
192    public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
193
194    /**
195     * Compression method for stored entries.
196     */
197    public static final int STORED = java.util.zip.ZipEntry.STORED;
198
199    /**
200     * Default encoding for file names and comment.
201     */
202    static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
203
204    /**
205     * General purpose flag, which indicates that file names are written in UTF-8.
206     *
207     * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead
208     */
209    @Deprecated
210    public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG;
211
212    /**
213     * Helper, a 0 as ZipShort.
214     */
215    private static final byte[] ZERO = { 0, 0 };
216
217    /**
218     * Helper, a 0 as ZipLong.
219     */
220    private static final byte[] LZERO = { 0, 0, 0, 0 };
221
222    private static final byte[] ONE = ZipLong.getBytes(1L);
223
224    /*
225     * Various ZIP constants shared between this class, ZipArchiveInputStream and ZipFile
226     */
227    /**
228     * local file header signature
229     */
230    static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); // NOSONAR
231
232    /**
233     * data descriptor signature
234     */
235    static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); // NOSONAR
236
237    /**
238     * central file header signature
239     */
240    static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); // NOSONAR
241
242    /**
243     * end of central dir signature
244     */
245    static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); // NOSONAR
246
247    /**
248     * ZIP64 end of central dir signature
249     */
250    static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); // NOSONAR
251
252    /**
253     * ZIP64 end of central dir locator signature
254     */
255    static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); // NOSONAR
256
257    /** Indicates if this archive is finished. protected for use in Jar implementation */
258    protected boolean finished;
259
260    /**
261     * Current entry.
262     */
263    private CurrentEntry entry;
264
265    /**
266     * The file comment.
267     */
268    private String comment = "";
269
270    /**
271     * Compression level for next entry.
272     */
273    private int level = DEFAULT_COMPRESSION;
274
275    /**
276     * Has the compression level changed when compared to the last entry?
277     */
278    private boolean hasCompressionLevelChanged;
279
280    /**
281     * Default compression method for next entry.
282     */
283    private int method = java.util.zip.ZipEntry.DEFLATED;
284
285    /**
286     * List of ZipArchiveEntries written so far.
287     */
288    private final List<ZipArchiveEntry> entries = new LinkedList<>();
289
290    private final StreamCompressor streamCompressor;
291
292    /**
293     * Start of central directory.
294     */
295    private long cdOffset;
296
297    /**
298     * Length of central directory.
299     */
300    private long cdLength;
301
302    /**
303     * Disk number start of central directory.
304     */
305    private long cdDiskNumberStart;
306
307    /**
308     * Length of end of central directory
309     */
310    private long eocdLength;
311
312    /**
313     * Holds some book-keeping data for each entry.
314     */
315    private final Map<ZipArchiveEntry, EntryMetaData> metaData = new HashMap<>();
316
317    /**
318     * The encoding to use for file names and the file comment.
319     *
320     * <p>
321     * For a list of possible values see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">Supported Encodings</a>.
322     * Defaults to UTF-8.
323     * </p>
324     */
325    private Charset charset = DEFAULT_CHARSET;
326
327    /**
328     * The ZIP encoding to use for file names and the file comment.
329     *
330     * This field is of internal use and will be set in {@link #setEncoding(String)}.
331     */
332    private ZipEncoding zipEncoding = ZipEncodingHelper.getZipEncoding(DEFAULT_CHARSET);
333
334    /**
335     * This Deflater object is used for output.
336     */
337    protected final Deflater def;
338
339    private final OutputStream outputStream;
340
341    /**
342     * whether to use the general purpose bit flag when writing UTF-8 file names or not.
343     */
344    private boolean useUTF8Flag = true;
345
346    /**
347     * Whether to encode non-encodable file names as UTF-8.
348     */
349    private boolean fallbackToUTF8;
350
351    /**
352     * whether to create UnicodePathExtraField-s for each entry.
353     */
354    private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;
355
356    /**
357     * Whether anything inside this archive has used a ZIP64 feature.
358     *
359     * @since 1.3
360     */
361    private boolean hasUsedZip64;
362
363    private Zip64Mode zip64Mode = Zip64Mode.AsNeeded;
364
365    private final byte[] copyBuffer = new byte[32768];
366
367    /**
368     * Whether we are creating a split zip
369     */
370    private final boolean isSplitZip;
371
372    /**
373     * Holds the number of Central Directories on each disk, this is used when writing Zip64 End Of Central Directory and End Of Central Directory
374     */
375    private final Map<Integer, Integer> numberOfCDInDiskData = new HashMap<>();
376
377    /**
378     * Creates a new ZIP OutputStream writing to a File. Will use random access if possible.
379     *
380     * @param file the file to ZIP to
381     * @throws IOException on error
382     */
383    public ZipArchiveOutputStream(final File file) throws IOException {
384        this(file.toPath());
385    }
386
387    /**
388     * Creates a split ZIP Archive.
389     *
390     * <p>
391     * The files making up the archive will use Z01, Z02, ... extensions and the last part of it will be the given {@code
392     * file}.
393     * </p>
394     *
395     * <p>
396     * Even though the stream writes to a file this stream will behave as if no random access was possible. This means the sizes of stored entries need to be
397     * known before the actual entry data is written.
398     * </p>
399     *
400     * @param file         the file that will become the last part of the split archive
401     * @param zipSplitSize maximum size of a single part of the split archive created by this stream. Must be between 64kB and about 4GB.
402     *
403     * @throws IOException              on error
404     * @throws IllegalArgumentException if zipSplitSize is not in the required range
405     * @since 1.20
406     */
407    public ZipArchiveOutputStream(final File file, final long zipSplitSize) throws IOException {
408        this(file.toPath(), zipSplitSize);
409    }
410
411    /**
412     * Creates a new ZIP OutputStream filtering the underlying stream.
413     *
414     * @param out the outputstream to zip
415     */
416    public ZipArchiveOutputStream(final OutputStream out) {
417        this.outputStream = out;
418        this.def = new Deflater(level, true);
419        this.streamCompressor = StreamCompressor.create(out, def);
420        this.isSplitZip = false;
421    }
422
423    /**
424     * Creates a split ZIP Archive.
425     * <p>
426     * The files making up the archive will use Z01, Z02, ... extensions and the last part of it will be the given {@code
427     * file}.
428     * </p>
429     * <p>
430     * Even though the stream writes to a file this stream will behave as if no random access was possible. This means the sizes of stored entries need to be
431     * known before the actual entry data is written.
432     * </p>
433     *
434     * @param path         the path to the file that will become the last part of the split archive
435     * @param zipSplitSize maximum size of a single part of the split archive created by this stream. Must be between 64kB and about 4GB.
436     * @throws IOException              on error
437     * @throws IllegalArgumentException if zipSplitSize is not in the required range
438     * @since 1.22
439     */
440    public ZipArchiveOutputStream(final Path path, final long zipSplitSize) throws IOException {
441        this.def = new Deflater(level, true);
442        this.outputStream = new ZipSplitOutputStream(path, zipSplitSize);
443        this.streamCompressor = StreamCompressor.create(this.outputStream, def);
444        this.isSplitZip = true;
445    }
446
447    /**
448     * Creates a new ZIP OutputStream writing to a Path. Will use random access if possible.
449     *
450     * @param file    the file to ZIP to
451     * @param options options specifying how the file is opened.
452     * @throws IOException on error
453     * @since 1.21
454     */
455    public ZipArchiveOutputStream(final Path file, final OpenOption... options) throws IOException {
456        this.def = new Deflater(level, true);
457        this.outputStream = options.length == 0 ? new FileRandomAccessOutputStream(file) : new FileRandomAccessOutputStream(file, options);
458        this.streamCompressor = StreamCompressor.create(outputStream, def);
459        this.isSplitZip = false;
460    }
461
462    /**
463     * Creates a new ZIP OutputStream writing to a SeekableByteChannel.
464     *
465     * <p>
466     * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to write to an in-memory archive using random access.
467     * </p>
468     *
469     * @param channel the channel to ZIP to
470     * @since 1.13
471     */
472    public ZipArchiveOutputStream(final SeekableByteChannel channel) {
473        this.outputStream = new SeekableChannelRandomAccessOutputStream(channel);
474        this.def = new Deflater(level, true);
475        this.streamCompressor = StreamCompressor.create(outputStream, def);
476        this.isSplitZip = false;
477    }
478
479    /**
480     * Adds an archive entry with a raw input stream.
481     * <p>
482     * If crc, size and compressed size are supplied on the entry, these values will be used as-is. Zip64 status is re-established based on the settings in this
483     * stream, and the supplied value is ignored.
484     * </p>
485     * <p>
486     * The entry is put and closed immediately.
487     * </p>
488     *
489     * @param entry     The archive entry to add
490     * @param rawStream The raw input stream of a different entry. May be compressed/encrypted.
491     * @throws IOException If copying fails
492     */
493    public void addRawArchiveEntry(final ZipArchiveEntry entry, final InputStream rawStream) throws IOException {
494        final ZipArchiveEntry ae = new ZipArchiveEntry(entry);
495        if (hasZip64Extra(ae)) {
496            // Will be re-added as required. this may make the file generated with this method
497            // somewhat smaller than standard mode,
498            // since standard mode is unable to remove the ZIP 64 header.
499            ae.removeExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
500        }
501        final boolean is2PhaseSource = ae.getCrc() != ZipArchiveEntry.CRC_UNKNOWN && ae.getSize() != ArchiveEntry.SIZE_UNKNOWN
502                && ae.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN;
503        putArchiveEntry(ae, is2PhaseSource);
504        copyFromZipInputStream(rawStream);
505        closeCopiedEntry(is2PhaseSource);
506    }
507
508    /**
509     * Adds UnicodeExtra fields for name and file comment if mode is ALWAYS or the data cannot be encoded using the configured encoding.
510     */
511    private void addUnicodeExtraFields(final ZipArchiveEntry ze, final boolean encodable, final ByteBuffer name) throws IOException {
512        if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !encodable) {
513            ze.addExtraField(new UnicodePathExtraField(ze.getName(), name.array(), name.arrayOffset(), name.limit() - name.position()));
514        }
515
516        final String comm = ze.getComment();
517        if (comm != null && !comm.isEmpty()) {
518
519            final boolean commentEncodable = zipEncoding.canEncode(comm);
520
521            if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !commentEncodable) {
522                final ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
523                ze.addExtraField(new UnicodeCommentExtraField(comm, commentB.array(), commentB.arrayOffset(), commentB.limit() - commentB.position()));
524            }
525        }
526    }
527
528    /**
529     * Whether this stream is able to write the given entry.
530     * <p>
531     * May return false if it is set up to use encryption or a compression method that hasn't been implemented yet.
532     * </p>
533     *
534     * @since 1.1
535     */
536    @Override
537    public boolean canWriteEntryData(final ArchiveEntry ae) {
538        if (ae instanceof ZipArchiveEntry) {
539            final ZipArchiveEntry zae = (ZipArchiveEntry) ae;
540            return zae.getMethod() != ZipMethod.IMPLODING.getCode() && zae.getMethod() != ZipMethod.UNSHRINKING.getCode() && ZipUtil.canHandleEntryData(zae);
541        }
542        return false;
543    }
544
545    /**
546     * Verifies the sizes aren't too big in the Zip64Mode.Never case and returns whether the entry would require a Zip64 extra field.
547     */
548    private boolean checkIfNeedsZip64(final Zip64Mode effectiveMode) throws ZipException {
549        final boolean actuallyNeedsZip64 = isZip64Required(entry.entry, effectiveMode);
550        if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) {
551            throw new Zip64RequiredException(Zip64RequiredException.getEntryTooBigMessage(entry.entry));
552        }
553        return actuallyNeedsZip64;
554    }
555
556    /**
557     * Closes this output stream and releases any system resources associated with the stream.
558     *
559     * @throws IOException            if an I/O error occurs.
560     * @throws Zip64RequiredException if the archive's size exceeds 4 GByte or there are more than 65535 entries inside the archive and {@link #setUseZip64} is
561     *                                {@link Zip64Mode#Never}.
562     */
563    @Override
564    public void close() throws IOException {
565        try {
566            if (!finished) {
567                finish();
568            }
569        } finally {
570            destroy();
571        }
572    }
573
574    /**
575     * Writes all necessary data for this entry.
576     *
577     * @throws IOException            on error
578     * @throws Zip64RequiredException if the entry's uncompressed or compressed size exceeds 4 GByte and {@link #setUseZip64} is {@link Zip64Mode#Never}.
579     */
580    @Override
581    public void closeArchiveEntry() throws IOException {
582        preClose();
583
584        flushDeflater();
585
586        final long bytesWritten = streamCompressor.getTotalBytesWritten() - entry.dataStart;
587        final long realCrc = streamCompressor.getCrc32();
588        entry.bytesRead = streamCompressor.getBytesRead();
589        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
590        final boolean actuallyNeedsZip64 = handleSizesAndCrc(bytesWritten, realCrc, effectiveMode);
591        closeEntry(actuallyNeedsZip64, false);
592        streamCompressor.reset();
593    }
594
595    /**
596     * Writes all necessary data for this entry.
597     *
598     * @param phased This entry is second phase of a 2-phase ZIP creation, size, compressed size and crc are known in ZipArchiveEntry
599     * @throws IOException            on error
600     * @throws Zip64RequiredException if the entry's uncompressed or compressed size exceeds 4 GByte and {@link #setUseZip64} is {@link Zip64Mode#Never}.
601     */
602    private void closeCopiedEntry(final boolean phased) throws IOException {
603        preClose();
604        entry.bytesRead = entry.entry.getSize();
605        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
606        final boolean actuallyNeedsZip64 = checkIfNeedsZip64(effectiveMode);
607        closeEntry(actuallyNeedsZip64, phased);
608    }
609
610    private void closeEntry(final boolean actuallyNeedsZip64, final boolean phased) throws IOException {
611        if (!phased && outputStream instanceof RandomAccessOutputStream) {
612            rewriteSizesAndCrc(actuallyNeedsZip64);
613        }
614
615        if (!phased) {
616            writeDataDescriptor(entry.entry);
617        }
618        entry = null;
619    }
620
621    private void copyFromZipInputStream(final InputStream src) throws IOException {
622        if (entry == null) {
623            throw new IllegalStateException("No current entry");
624        }
625        ZipUtil.checkRequestedFeatures(entry.entry);
626        entry.hasWritten = true;
627        int length;
628        while ((length = src.read(copyBuffer)) >= 0) {
629            streamCompressor.writeCounted(copyBuffer, 0, length);
630            count(length);
631        }
632    }
633
634    /**
635     * Creates a new ZIP entry taking some information from the given file and using the provided name.
636     * <p>
637     * The name will be adjusted to end with a forward slash "/" if the file is a directory. If the file is not a directory a potential trailing forward slash
638     * will be stripped from the entry name.
639     * </p>
640     * <p>
641     * Must not be used if the stream has already been closed.
642     * </p>
643     */
644    @Override
645    public ZipArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
646        if (finished) {
647            throw new IOException("Stream has already been finished");
648        }
649        return new ZipArchiveEntry(inputFile, entryName);
650    }
651
652    /**
653     * Creates a new ZIP entry taking some information from the given file and using the provided name.
654     * <p>
655     * The name will be adjusted to end with a forward slash "/" if the file is a directory. If the file is not a directory a potential trailing forward slash
656     * will be stripped from the entry name.
657     * </p>
658     * <p>
659     * Must not be used if the stream has already been closed.
660     * </p>
661     *
662     * @param inputPath path to create the entry from.
663     * @param entryName name of the entry.
664     * @param options   options indicating how symbolic links are handled.
665     * @return a new instance.
666     * @throws IOException if an I/O error occurs.
667     * @since 1.21
668     */
669    @Override
670    public ZipArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
671        if (finished) {
672            throw new IOException("Stream has already been finished");
673        }
674        return new ZipArchiveEntry(inputPath, entryName);
675    }
676
677    private byte[] createCentralFileHeader(final ZipArchiveEntry ze) throws IOException {
678
679        final EntryMetaData entryMetaData = metaData.get(ze);
680        final boolean needsZip64Extra = hasZip64Extra(ze) || ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC || ze.getSize() >= ZipConstants.ZIP64_MAGIC
681                || entryMetaData.offset >= ZipConstants.ZIP64_MAGIC || ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT
682                || zip64Mode == Zip64Mode.Always || zip64Mode == Zip64Mode.AlwaysWithCompatibility;
683
684        if (needsZip64Extra && zip64Mode == Zip64Mode.Never) {
685            // must be the offset that is too big, otherwise an
686            // exception would have been throw in putArchiveEntry or
687            // closeArchiveEntry
688            throw new Zip64RequiredException(Zip64RequiredException.ARCHIVE_TOO_BIG_MESSAGE);
689        }
690
691        handleZip64Extra(ze, entryMetaData.offset, needsZip64Extra);
692
693        return createCentralFileHeader(ze, getName(ze), entryMetaData, needsZip64Extra);
694    }
695
696    /**
697     * Writes the central file header entry.
698     *
699     * @param ze            the entry to write
700     * @param name          The encoded name
701     * @param entryMetaData meta data for this file
702     * @throws IOException on error
703     */
704    private byte[] createCentralFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, final EntryMetaData entryMetaData, final boolean needsZip64Extra)
705            throws IOException {
706        if (isSplitZip) {
707            // calculate the disk number for every central file header,
708            // this will be used in writing End Of Central Directory and Zip64 End Of Central Directory
709            final int currentSplitSegment = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex();
710            if (numberOfCDInDiskData.get(currentSplitSegment) == null) {
711                numberOfCDInDiskData.put(currentSplitSegment, 1);
712            } else {
713                final int originalNumberOfCD = numberOfCDInDiskData.get(currentSplitSegment);
714                numberOfCDInDiskData.put(currentSplitSegment, originalNumberOfCD + 1);
715            }
716        }
717
718        final byte[] extra = ze.getCentralDirectoryExtra();
719        final int extraLength = extra.length;
720
721        // file comment length
722        String comm = ze.getComment();
723        if (comm == null) {
724            comm = "";
725        }
726
727        final ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
728        final int nameLen = name.limit() - name.position();
729        final int commentLen = commentB.limit() - commentB.position();
730        final int len = CFH_FILENAME_OFFSET + nameLen + extraLength + commentLen;
731        final byte[] buf = new byte[len];
732
733        System.arraycopy(CFH_SIG, 0, buf, CFH_SIG_OFFSET, ZipConstants.WORD);
734
735        // version made by
736        // CheckStyle:MagicNumber OFF
737        ZipShort.putShort(ze.getPlatform() << 8 | (!hasUsedZip64 ? ZipConstants.DATA_DESCRIPTOR_MIN_VERSION : ZipConstants.ZIP64_MIN_VERSION), buf,
738                CFH_VERSION_MADE_BY_OFFSET);
739
740        final int zipMethod = ze.getMethod();
741        final boolean encodable = zipEncoding.canEncode(ze.getName());
742        ZipShort.putShort(versionNeededToExtract(zipMethod, needsZip64Extra, entryMetaData.usesDataDescriptor), buf, CFH_VERSION_NEEDED_OFFSET);
743        getGeneralPurposeBits(!encodable && fallbackToUTF8, entryMetaData.usesDataDescriptor).encode(buf, CFH_GPB_OFFSET);
744
745        // compression method
746        ZipShort.putShort(zipMethod, buf, CFH_METHOD_OFFSET);
747
748        // last mod. time and date
749        ZipUtil.toDosTime(ze.getTime(), buf, CFH_TIME_OFFSET);
750
751        // CRC
752        // compressed length
753        // uncompressed length
754        ZipLong.putLong(ze.getCrc(), buf, CFH_CRC_OFFSET);
755        if (ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC || ze.getSize() >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always
756                || zip64Mode == Zip64Mode.AlwaysWithCompatibility) {
757            ZipLong.ZIP64_MAGIC.putLong(buf, CFH_COMPRESSED_SIZE_OFFSET);
758            ZipLong.ZIP64_MAGIC.putLong(buf, CFH_ORIGINAL_SIZE_OFFSET);
759        } else {
760            ZipLong.putLong(ze.getCompressedSize(), buf, CFH_COMPRESSED_SIZE_OFFSET);
761            ZipLong.putLong(ze.getSize(), buf, CFH_ORIGINAL_SIZE_OFFSET);
762        }
763
764        ZipShort.putShort(nameLen, buf, CFH_FILENAME_LENGTH_OFFSET);
765
766        // extra field length
767        ZipShort.putShort(extraLength, buf, CFH_EXTRA_LENGTH_OFFSET);
768
769        ZipShort.putShort(commentLen, buf, CFH_COMMENT_LENGTH_OFFSET);
770
771        // disk number start
772        if (isSplitZip) {
773            if (ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always) {
774                ZipShort.putShort(ZipConstants.ZIP64_MAGIC_SHORT, buf, CFH_DISK_NUMBER_OFFSET);
775            } else {
776                ZipShort.putShort((int) ze.getDiskNumberStart(), buf, CFH_DISK_NUMBER_OFFSET);
777            }
778        } else {
779            System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, ZipConstants.SHORT);
780        }
781
782        // internal file attributes
783        ZipShort.putShort(ze.getInternalAttributes(), buf, CFH_INTERNAL_ATTRIBUTES_OFFSET);
784
785        // external file attributes
786        ZipLong.putLong(ze.getExternalAttributes(), buf, CFH_EXTERNAL_ATTRIBUTES_OFFSET);
787
788        // relative offset of LFH
789        if (entryMetaData.offset >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always) {
790            ZipLong.putLong(ZipConstants.ZIP64_MAGIC, buf, CFH_LFH_OFFSET);
791        } else {
792            ZipLong.putLong(Math.min(entryMetaData.offset, ZipConstants.ZIP64_MAGIC), buf, CFH_LFH_OFFSET);
793        }
794
795        // file name
796        System.arraycopy(name.array(), name.arrayOffset(), buf, CFH_FILENAME_OFFSET, nameLen);
797
798        final int extraStart = CFH_FILENAME_OFFSET + nameLen;
799        System.arraycopy(extra, 0, buf, extraStart, extraLength);
800
801        final int commentStart = extraStart + extraLength;
802
803        // file comment
804        System.arraycopy(commentB.array(), commentB.arrayOffset(), buf, commentStart, commentLen);
805        return buf;
806    }
807
808    private byte[] createLocalFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, final boolean encodable, final boolean phased,
809            final long archiveOffset) {
810        final ZipExtraField oldEx = ze.getExtraField(ResourceAlignmentExtraField.ID);
811        if (oldEx != null) {
812            ze.removeExtraField(ResourceAlignmentExtraField.ID);
813        }
814        final ResourceAlignmentExtraField oldAlignmentEx = oldEx instanceof ResourceAlignmentExtraField ? (ResourceAlignmentExtraField) oldEx : null;
815
816        int alignment = ze.getAlignment();
817        if (alignment <= 0 && oldAlignmentEx != null) {
818            alignment = oldAlignmentEx.getAlignment();
819        }
820
821        if (alignment > 1 || oldAlignmentEx != null && !oldAlignmentEx.allowMethodChange()) {
822            final int oldLength = LFH_FILENAME_OFFSET + name.limit() - name.position() + ze.getLocalFileDataExtra().length;
823
824            final int padding = (int) (-archiveOffset - oldLength - ZipExtraField.EXTRAFIELD_HEADER_SIZE - ResourceAlignmentExtraField.BASE_SIZE
825                    & alignment - 1);
826            ze.addExtraField(new ResourceAlignmentExtraField(alignment, oldAlignmentEx != null && oldAlignmentEx.allowMethodChange(), padding));
827        }
828
829        final byte[] extra = ze.getLocalFileDataExtra();
830        final int nameLen = name.limit() - name.position();
831        final int len = LFH_FILENAME_OFFSET + nameLen + extra.length;
832        final byte[] buf = new byte[len];
833
834        System.arraycopy(LFH_SIG, 0, buf, LFH_SIG_OFFSET, ZipConstants.WORD);
835
836        // store method in local variable to prevent multiple method calls
837        final int zipMethod = ze.getMethod();
838        final boolean dataDescriptor = usesDataDescriptor(zipMethod, phased);
839
840        ZipShort.putShort(versionNeededToExtract(zipMethod, hasZip64Extra(ze), dataDescriptor), buf, LFH_VERSION_NEEDED_OFFSET);
841
842        final GeneralPurposeBit generalPurposeBit = getGeneralPurposeBits(!encodable && fallbackToUTF8, dataDescriptor);
843        generalPurposeBit.encode(buf, LFH_GPB_OFFSET);
844
845        // compression method
846        ZipShort.putShort(zipMethod, buf, LFH_METHOD_OFFSET);
847
848        ZipUtil.toDosTime(ze.getTime(), buf, LFH_TIME_OFFSET);
849
850        // CRC
851        if (phased || !(zipMethod == DEFLATED || outputStream instanceof RandomAccessOutputStream)) {
852            ZipLong.putLong(ze.getCrc(), buf, LFH_CRC_OFFSET);
853        } else {
854            System.arraycopy(LZERO, 0, buf, LFH_CRC_OFFSET, ZipConstants.WORD);
855        }
856
857        // compressed length
858        // uncompressed length
859        if (hasZip64Extra(entry.entry)) {
860            // point to ZIP64 extended information extra field for
861            // sizes, may get rewritten once sizes are known if
862            // stream is seekable
863            ZipLong.ZIP64_MAGIC.putLong(buf, LFH_COMPRESSED_SIZE_OFFSET);
864            ZipLong.ZIP64_MAGIC.putLong(buf, LFH_ORIGINAL_SIZE_OFFSET);
865        } else if (phased) {
866            ZipLong.putLong(ze.getCompressedSize(), buf, LFH_COMPRESSED_SIZE_OFFSET);
867            ZipLong.putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
868        } else if (zipMethod == DEFLATED || outputStream instanceof RandomAccessOutputStream) {
869            System.arraycopy(LZERO, 0, buf, LFH_COMPRESSED_SIZE_OFFSET, ZipConstants.WORD);
870            System.arraycopy(LZERO, 0, buf, LFH_ORIGINAL_SIZE_OFFSET, ZipConstants.WORD);
871        } else { // Stored
872            ZipLong.putLong(ze.getSize(), buf, LFH_COMPRESSED_SIZE_OFFSET);
873            ZipLong.putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
874        }
875        // file name length
876        ZipShort.putShort(nameLen, buf, LFH_FILENAME_LENGTH_OFFSET);
877
878        // extra field length
879        ZipShort.putShort(extra.length, buf, LFH_EXTRA_LENGTH_OFFSET);
880
881        // file name
882        System.arraycopy(name.array(), name.arrayOffset(), buf, LFH_FILENAME_OFFSET, nameLen);
883
884        // extra fields
885        System.arraycopy(extra, 0, buf, LFH_FILENAME_OFFSET + nameLen, extra.length);
886
887        return buf;
888    }
889
890    /**
891     * Writes next block of compressed data to the output stream.
892     *
893     * @throws IOException on error
894     */
895    protected final void deflate() throws IOException {
896        streamCompressor.deflate();
897    }
898
899    /**
900     * Closes the underlying stream/file without finishing the archive, the result will likely be a corrupt archive.
901     * <p>
902     * This method only exists to support tests that generate corrupt archives so they can clean up any temporary files.
903     * </p>
904     */
905    void destroy() throws IOException {
906        if (outputStream != null) {
907            outputStream.close();
908        }
909    }
910
911    /**
912     * {@inheritDoc}
913     *
914     * @throws Zip64RequiredException if the archive's size exceeds 4 GByte or there are more than 65535 entries inside the archive and {@link #setUseZip64} is
915     *                                {@link Zip64Mode#Never}.
916     */
917    @Override
918    public void finish() throws IOException {
919        if (finished) {
920            throw new IOException("This archive has already been finished");
921        }
922
923        if (entry != null) {
924            throw new IOException("This archive contains unclosed entries.");
925        }
926
927        final long cdOverallOffset = streamCompressor.getTotalBytesWritten();
928        cdOffset = cdOverallOffset;
929        if (isSplitZip) {
930            // when creating a split zip, the offset should be
931            // the offset to the corresponding segment disk
932            final ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream) this.outputStream;
933            cdOffset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten();
934            cdDiskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex();
935        }
936        writeCentralDirectoryInChunks();
937
938        cdLength = streamCompressor.getTotalBytesWritten() - cdOverallOffset;
939
940        // calculate the length of end of central directory, as it may be used in writeZip64CentralDirectory
941        final ByteBuffer commentData = this.zipEncoding.encode(comment);
942        final long commentLength = (long) commentData.limit() - commentData.position();
943        eocdLength = ZipConstants.WORD /* length of EOCD_SIG */
944                + ZipConstants.SHORT /* number of this disk */
945                + ZipConstants.SHORT /* disk number of start of central directory */
946                + ZipConstants.SHORT /* total number of entries on this disk */
947                + ZipConstants.SHORT /* total number of entries */
948                + ZipConstants.WORD /* size of central directory */
949                + ZipConstants.WORD /* offset of start of central directory */
950                + ZipConstants.SHORT /* ZIP comment length */
951                + commentLength /* ZIP comment */;
952
953        writeZip64CentralDirectory();
954        writeCentralDirectoryEnd();
955        metaData.clear();
956        entries.clear();
957        streamCompressor.close();
958        if (isSplitZip) {
959            // trigger the ZipSplitOutputStream to write the final split segment
960            outputStream.close();
961        }
962        finished = true;
963    }
964
965    /**
966     * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
967     *
968     * @throws IOException if an I/O error occurs.
969     */
970    @Override
971    public void flush() throws IOException {
972        if (outputStream != null) {
973            outputStream.flush();
974        }
975    }
976
977    /**
978     * Ensures all bytes sent to the deflater are written to the stream.
979     */
980    private void flushDeflater() throws IOException {
981        if (entry.entry.getMethod() == DEFLATED) {
982            streamCompressor.flushDeflater();
983        }
984    }
985
986    /**
987     * Returns the total number of bytes written to this stream.
988     *
989     * @return the number of written bytes
990     * @since 1.22
991     */
992    @Override
993    public long getBytesWritten() {
994        return streamCompressor.getTotalBytesWritten();
995    }
996
997    /**
998     * If the mode is AsNeeded and the entry is a compressed entry of unknown size that gets written to a non-seekable stream then change the default to Never.
999     *
1000     * @since 1.3
1001     */
1002    private Zip64Mode getEffectiveZip64Mode(final ZipArchiveEntry ze) {
1003        if (zip64Mode != Zip64Mode.AsNeeded || outputStream instanceof RandomAccessOutputStream ||
1004                ze.getMethod() != DEFLATED || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN) {
1005            return zip64Mode;
1006        }
1007        return Zip64Mode.Never;
1008    }
1009
1010    /**
1011     * The encoding to use for file names and the file comment.
1012     *
1013     * @return null if using the platform's default character encoding.
1014     */
1015    public String getEncoding() {
1016        return charset != null ? charset.name() : null;
1017    }
1018
1019    private ZipEncoding getEntryEncoding(final ZipArchiveEntry ze) {
1020        final boolean encodable = zipEncoding.canEncode(ze.getName());
1021        return !encodable && fallbackToUTF8 ? ZipEncodingHelper.ZIP_ENCODING_UTF_8 : zipEncoding;
1022    }
1023
1024    private GeneralPurposeBit getGeneralPurposeBits(final boolean utfFallback, final boolean usesDataDescriptor) {
1025        final GeneralPurposeBit b = new GeneralPurposeBit();
1026        b.useUTF8ForNames(useUTF8Flag || utfFallback);
1027        if (usesDataDescriptor) {
1028            b.useDataDescriptor(true);
1029        }
1030        return b;
1031    }
1032
1033    private ByteBuffer getName(final ZipArchiveEntry ze) throws IOException {
1034        return getEntryEncoding(ze).encode(ze.getName());
1035    }
1036
1037    /**
1038     * Gets the existing ZIP64 extended information extra field or create a new one and add it to the entry.
1039     *
1040     * @since 1.3
1041     */
1042    private Zip64ExtendedInformationExtraField getZip64Extra(final ZipArchiveEntry ze) {
1043        if (entry != null) {
1044            entry.causedUseOfZip64 = !hasUsedZip64;
1045        }
1046        hasUsedZip64 = true;
1047        final ZipExtraField extra = ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
1048        Zip64ExtendedInformationExtraField z64 = extra instanceof Zip64ExtendedInformationExtraField ? (Zip64ExtendedInformationExtraField) extra : null;
1049        if (z64 == null) {
1050            /*
1051             * System.err.println("Adding z64 for " + ze.getName() + ", method: " + ze.getMethod() + " (" + (ze.getMethod() == STORED) + ")" + ", channel: " +
1052             * (channel != null));
1053             */
1054            z64 = new Zip64ExtendedInformationExtraField();
1055        }
1056
1057        // even if the field is there already, make sure it is the first one
1058        ze.addAsFirstExtraField(z64);
1059
1060        return z64;
1061    }
1062
1063    /**
1064     * 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
1065     * whether the entry would require a Zip64 extra field.
1066     */
1067    private boolean handleSizesAndCrc(final long bytesWritten, final long crc, final Zip64Mode effectiveMode) throws ZipException {
1068        if (entry.entry.getMethod() == DEFLATED) {
1069            /*
1070             * It turns out def.getBytesRead() returns wrong values if the size exceeds 4 GB on Java < Java7 entry.entry.setSize(def.getBytesRead());
1071             */
1072            entry.entry.setSize(entry.bytesRead);
1073            entry.entry.setCompressedSize(bytesWritten);
1074            entry.entry.setCrc(crc);
1075
1076        } else if (!(outputStream instanceof RandomAccessOutputStream)) {
1077            if (entry.entry.getCrc() != crc) {
1078                throw new ZipException("Bad CRC checksum for entry " + entry.entry.getName() + ": " + Long.toHexString(entry.entry.getCrc()) + " instead of "
1079                        + Long.toHexString(crc));
1080            }
1081
1082            if (entry.entry.getSize() != bytesWritten) {
1083                throw new ZipException("Bad size for entry " + entry.entry.getName() + ": " + entry.entry.getSize() + " instead of " + bytesWritten);
1084            }
1085        } else { /* method is STORED and we used SeekableByteChannel */
1086            entry.entry.setSize(bytesWritten);
1087            entry.entry.setCompressedSize(bytesWritten);
1088            entry.entry.setCrc(crc);
1089        }
1090
1091        return checkIfNeedsZip64(effectiveMode);
1092    }
1093
1094    /**
1095     * If the entry needs Zip64 extra information inside the central directory then configure its data.
1096     */
1097    private void handleZip64Extra(final ZipArchiveEntry ze, final long lfhOffset, final boolean needsZip64Extra) {
1098        if (needsZip64Extra) {
1099            final Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze);
1100            if (ze.getCompressedSize() >= ZipConstants.ZIP64_MAGIC || ze.getSize() >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always
1101                    || zip64Mode == Zip64Mode.AlwaysWithCompatibility) {
1102                z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
1103                z64.setSize(new ZipEightByteInteger(ze.getSize()));
1104            } else {
1105                // reset value that may have been set for LFH
1106                z64.setCompressedSize(null);
1107                z64.setSize(null);
1108            }
1109
1110            final boolean needsToEncodeLfhOffset = lfhOffset >= ZipConstants.ZIP64_MAGIC || zip64Mode == Zip64Mode.Always;
1111            final boolean needsToEncodeDiskNumberStart = ze.getDiskNumberStart() >= ZipConstants.ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always;
1112
1113            if (needsToEncodeLfhOffset || needsToEncodeDiskNumberStart) {
1114                z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset));
1115            }
1116            if (needsToEncodeDiskNumberStart) {
1117                z64.setDiskStartNumber(new ZipLong(ze.getDiskNumberStart()));
1118            }
1119            ze.setExtra();
1120        }
1121    }
1122
1123    /**
1124     * Is there a ZIP64 extended information extra field for the entry?
1125     *
1126     * @since 1.3
1127     */
1128    private boolean hasZip64Extra(final ZipArchiveEntry ze) {
1129        return ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID) instanceof Zip64ExtendedInformationExtraField;
1130    }
1131
1132    /**
1133     * This method indicates whether this archive is writing to a seekable stream (i.e., to a random access file).
1134     * <p>
1135     * For seekable streams, you don't need to calculate the CRC or uncompressed size for {@link #STORED} entries before invoking
1136     * {@link #putArchiveEntry(ZipArchiveEntry)}.
1137     * </p>
1138     *
1139     * @return true if seekable
1140     */
1141    public boolean isSeekable() {
1142        return outputStream instanceof RandomAccessOutputStream;
1143    }
1144
1145    private boolean isTooLargeForZip32(final ZipArchiveEntry zipArchiveEntry) {
1146        return zipArchiveEntry.getSize() >= ZipConstants.ZIP64_MAGIC || zipArchiveEntry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC;
1147    }
1148
1149    private boolean isZip64Required(final ZipArchiveEntry entry1, final Zip64Mode requestedMode) {
1150        return requestedMode == Zip64Mode.Always || requestedMode == Zip64Mode.AlwaysWithCompatibility || isTooLargeForZip32(entry1);
1151    }
1152
1153    private void preClose() throws IOException {
1154        if (finished) {
1155            throw new IOException("Stream has already been finished");
1156        }
1157
1158        if (entry == null) {
1159            throw new IOException("No current entry to close");
1160        }
1161
1162        if (!entry.hasWritten) {
1163            write(ByteUtils.EMPTY_BYTE_ARRAY, 0, 0);
1164        }
1165    }
1166
1167    /**
1168     * {@inheritDoc}
1169     *
1170     * @throws ClassCastException     if entry is not an instance of ZipArchiveEntry
1171     * @throws Zip64RequiredException if the entry's uncompressed or compressed size is known to exceed 4 GByte and {@link #setUseZip64} is
1172     *                                {@link Zip64Mode#Never}.
1173     */
1174    @Override
1175    public void putArchiveEntry(final ZipArchiveEntry archiveEntry) throws IOException {
1176        putArchiveEntry(archiveEntry, false);
1177    }
1178
1179    /**
1180     * Writes the headers for an archive entry to the output stream. The caller must then write the content to the stream and call {@link #closeArchiveEntry()}
1181     * to complete the process.
1182     *
1183     * @param archiveEntry The archiveEntry
1184     * @param phased       If true size, compressedSize and crc required to be known up-front in the archiveEntry
1185     * @throws ClassCastException     if entry is not an instance of ZipArchiveEntry
1186     * @throws Zip64RequiredException if the entry's uncompressed or compressed size is known to exceed 4 GByte and {@link #setUseZip64} is
1187     *                                {@link Zip64Mode#Never}.
1188     */
1189    private void putArchiveEntry(final ZipArchiveEntry archiveEntry, final boolean phased) throws IOException {
1190        if (finished) {
1191            throw new IOException("Stream has already been finished");
1192        }
1193
1194        if (entry != null) {
1195            closeArchiveEntry();
1196        }
1197
1198        entry = new CurrentEntry(archiveEntry);
1199        entries.add(entry.entry);
1200
1201        setDefaults(entry.entry);
1202
1203        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
1204        validateSizeInformation(effectiveMode);
1205
1206        if (shouldAddZip64Extra(entry.entry, effectiveMode)) {
1207
1208            final Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry);
1209
1210            final ZipEightByteInteger size;
1211            final ZipEightByteInteger compressedSize;
1212            if (phased) {
1213                // sizes are already known
1214                size = new ZipEightByteInteger(entry.entry.getSize());
1215                compressedSize = new ZipEightByteInteger(entry.entry.getCompressedSize());
1216            } else if (entry.entry.getMethod() == STORED && entry.entry.getSize() != ArchiveEntry.SIZE_UNKNOWN) {
1217                // actually, we already know the sizes
1218                compressedSize = size = new ZipEightByteInteger(entry.entry.getSize());
1219            } else {
1220                // just a placeholder, real data will be in data
1221                // descriptor or inserted later via SeekableByteChannel
1222                compressedSize = size = ZipEightByteInteger.ZERO;
1223            }
1224            z64.setSize(size);
1225            z64.setCompressedSize(compressedSize);
1226            entry.entry.setExtra();
1227        }
1228
1229        if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
1230            def.setLevel(level);
1231            hasCompressionLevelChanged = false;
1232        }
1233        writeLocalFileHeader(archiveEntry, phased);
1234    }
1235
1236    /**
1237     * When using random access output, write the local file header and potentially the ZIP64 extra containing the correct CRC and compressed/uncompressed
1238     * sizes.
1239     */
1240    private void rewriteSizesAndCrc(final boolean actuallyNeedsZip64) throws IOException {
1241        final RandomAccessOutputStream randomStream = (RandomAccessOutputStream) outputStream;
1242        long dataStart = entry.localDataStart;
1243        if (randomStream instanceof ZipSplitOutputStream) {
1244            dataStart = ((ZipSplitOutputStream) randomStream).calculateDiskPosition(entry.entry.getDiskNumberStart(), dataStart);
1245        }
1246
1247        long position = dataStart;
1248        randomStream.writeFully(ZipLong.getBytes(entry.entry.getCrc()), position); position += ZipConstants.WORD;
1249        if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) {
1250            randomStream.writeFully(ZipLong.getBytes(entry.entry.getCompressedSize()), position); position += ZipConstants.WORD;
1251            randomStream.writeFully(ZipLong.getBytes(entry.entry.getSize()), position); position += ZipConstants.WORD;
1252        } else {
1253            randomStream.writeFully(ZipLong.ZIP64_MAGIC.getBytes(), position); position += ZipConstants.WORD;
1254            randomStream.writeFully(ZipLong.ZIP64_MAGIC.getBytes(), position); position += ZipConstants.WORD;
1255        }
1256
1257        if (hasZip64Extra(entry.entry)) {
1258            final ByteBuffer name = getName(entry.entry);
1259            final int nameLen = name.limit() - name.position();
1260            // seek to ZIP64 extra, skip header and size information
1261            position = dataStart + 3 * ZipConstants.WORD + 2 * ZipConstants.SHORT + nameLen + 2 * ZipConstants.SHORT;
1262            // inside the ZIP64 extra uncompressed size comes
1263            // first, unlike the LFH, CD or data descriptor
1264            randomStream.writeFully(ZipEightByteInteger.getBytes(entry.entry.getSize()), position); position += ZipConstants.DWORD;
1265            randomStream.writeFully(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize()), position); position += ZipConstants.DWORD;
1266
1267            if (!actuallyNeedsZip64) {
1268                // do some cleanup:
1269                // * rewrite version needed to extract
1270                position = dataStart - 5 * ZipConstants.SHORT;
1271                randomStream.writeFully(ZipShort.getBytes(versionNeededToExtract(entry.entry.getMethod(), false, false)), position);
1272                position += ZipConstants.SHORT;
1273
1274                // * remove ZIP64 extra, so it doesn't get written
1275                // to the central directory
1276                entry.entry.removeExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
1277                entry.entry.setExtra();
1278
1279                // * reset hasUsedZip64 if it has been set because
1280                // of this entry
1281                if (entry.causedUseOfZip64) {
1282                    hasUsedZip64 = false;
1283                }
1284            }
1285        }
1286    }
1287
1288    /**
1289     * Sets the file comment.
1290     *
1291     * @param comment the comment
1292     */
1293    public void setComment(final String comment) {
1294        this.comment = comment;
1295    }
1296
1297    /**
1298     * Whether to create Unicode Extra Fields.
1299     * <p>
1300     * Defaults to NEVER.
1301     * </p>
1302     *
1303     * @param b whether to create Unicode Extra Fields.
1304     */
1305    public void setCreateUnicodeExtraFields(final UnicodeExtraFieldPolicy b) {
1306        createUnicodeExtraFields = b;
1307    }
1308
1309    /**
1310     * Provides default values for compression method and last modification time.
1311     */
1312    private void setDefaults(final ZipArchiveEntry entry) {
1313        if (entry.getMethod() == -1) { // not specified
1314            entry.setMethod(method);
1315        }
1316
1317        if (entry.getTime() == -1) { // not specified
1318            entry.setTime(System.currentTimeMillis());
1319        }
1320    }
1321
1322    private void setEncoding(final Charset encoding) {
1323        this.charset = encoding;
1324        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
1325        if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) {
1326            useUTF8Flag = false;
1327        }
1328    }
1329
1330    /**
1331     * The encoding to use for file names and the file comment.
1332     * <p>
1333     * For a list of possible values see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">Supported Encodings</a>.
1334     * Defaults to UTF-8.
1335     * </p>
1336     *
1337     * @param encoding the encoding to use for file names, use null for the platform's default encoding
1338     */
1339    public void setEncoding(final String encoding) {
1340        setEncoding(Charset.forName(encoding));
1341    }
1342
1343    /**
1344     * Whether to fall back to UTF and the language encoding flag if the file name cannot be encoded using the specified encoding.
1345     * <p>
1346     * Defaults to false.
1347     * </p>
1348     *
1349     * @param b whether to fall back to UTF and the language encoding flag if the file name cannot be encoded using the specified encoding.
1350     */
1351    public void setFallbackToUTF8(final boolean b) {
1352        fallbackToUTF8 = b;
1353    }
1354
1355    /**
1356     * Sets the compression level for subsequent entries.
1357     * <p>
1358     * Default is Deflater.DEFAULT_COMPRESSION.
1359     * </p>
1360     *
1361     * @param level the compression level.
1362     * @throws IllegalArgumentException if an invalid compression level is specified.
1363     */
1364    public void setLevel(final int level) {
1365        if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) {
1366            throw new IllegalArgumentException("Invalid compression level: " + level);
1367        }
1368        if (this.level == level) {
1369            return;
1370        }
1371        hasCompressionLevelChanged = true;
1372        this.level = level;
1373    }
1374
1375    /**
1376     * Sets the default compression method for subsequent entries.
1377     * <p>
1378     * Default is DEFLATED.
1379     * </p>
1380     *
1381     * @param method an {@code int} from java.util.zip.ZipEntry
1382     */
1383    public void setMethod(final int method) {
1384        this.method = method;
1385    }
1386
1387    /**
1388     * Whether to set the language encoding flag if the file name encoding is UTF-8.
1389     * <p>
1390     * Defaults to true.
1391     * </p>
1392     *
1393     * @param b whether to set the language encoding flag if the file name encoding is UTF-8
1394     */
1395    public void setUseLanguageEncodingFlag(final boolean b) {
1396        useUTF8Flag = b && ZipEncodingHelper.isUTF8(charset);
1397    }
1398
1399    /**
1400     * Whether Zip64 extensions will be used.
1401     * <p>
1402     * When setting the mode to {@link Zip64Mode#Never Never}, {@link #putArchiveEntry}, {@link #closeArchiveEntry}, {@link #finish} or {@link #close} may throw
1403     * a {@link Zip64RequiredException} if the entry's size or the total size of the archive exceeds 4GB or there are more than 65,536 entries inside the
1404     * archive. Any archive created in this mode will be readable by implementations that don't support Zip64.
1405     * </p>
1406     * <p>
1407     * 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
1408     * unreadable by implementations that don't support Zip64 even if all its contents would be.
1409     * </p>
1410     * <p>
1411     * When setting the mode to {@link Zip64Mode#AsNeeded AsNeeded}, Zip64 extensions will transparently be used for those entries that require them. This mode
1412     * can only be used if the uncompressed size of the {@link ZipArchiveEntry} is known when calling {@link #putArchiveEntry} or the archive is written to a
1413     * seekable output (i.e. you have used the {@link #ZipArchiveOutputStream(java.io.File) File-arg constructor}) - this mode is not valid when the output
1414     * stream is not seekable and the uncompressed size is unknown when {@link #putArchiveEntry} is called.
1415     * </p>
1416     * <p>
1417     * If no entry inside the resulting archive requires Zip64 extensions then {@link Zip64Mode#Never Never} will create the smallest archive.
1418     * {@link Zip64Mode#AsNeeded AsNeeded} will create a slightly bigger archive if the uncompressed size of any entry has initially been unknown and create an
1419     * archive identical to {@link Zip64Mode#Never Never} otherwise. {@link Zip64Mode#Always Always} will create an archive that is at least 24 bytes per entry
1420     * bigger than the one {@link Zip64Mode#Never Never} would create.
1421     * </p>
1422     * <p>
1423     * Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless {@link #putArchiveEntry} is called with an entry of unknown size and data is written to a
1424     * non-seekable stream - in this case the default is {@link Zip64Mode#Never Never}.
1425     * </p>
1426     *
1427     * @since 1.3
1428     * @param mode Whether Zip64 extensions will be used.
1429     */
1430    public void setUseZip64(final Zip64Mode mode) {
1431        zip64Mode = mode;
1432    }
1433
1434    /**
1435     * Whether to add a Zip64 extended information extra field to the local file header.
1436     * <p>
1437     * Returns true if
1438     * </p>
1439     * <ul>
1440     * <li>mode is Always</li>
1441     * <li>or we already know it is going to be needed</li>
1442     * <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>
1443     * </ul>
1444     */
1445    private boolean shouldAddZip64Extra(final ZipArchiveEntry entry, final Zip64Mode mode) {
1446        return mode == Zip64Mode.Always || mode == Zip64Mode.AlwaysWithCompatibility || entry.getSize() >= ZipConstants.ZIP64_MAGIC
1447                || entry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC
1448                || entry.getSize() == ArchiveEntry.SIZE_UNKNOWN && outputStream instanceof RandomAccessOutputStream && mode != Zip64Mode.Never;
1449    }
1450
1451    /**
1452     * 4.4.1.4 If one of the fields in the end of central directory record is too small to hold required data, the field SHOULD be set to -1 (0xFFFF or
1453     * 0xFFFFFFFF) and the ZIP64 format record SHOULD be created.
1454     *
1455     * @return true if zip64 End Of Central Directory is needed
1456     */
1457    private boolean shouldUseZip64EOCD() {
1458        int numberOfThisDisk = 0;
1459        if (isSplitZip) {
1460            numberOfThisDisk = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex();
1461        }
1462        final int numOfEntriesOnThisDisk = numberOfCDInDiskData.getOrDefault(numberOfThisDisk, 0);
1463        return numberOfThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT /* number of this disk */
1464                || cdDiskNumberStart >= ZipConstants.ZIP64_MAGIC_SHORT /* number of the disk with the start of the central directory */
1465                || numOfEntriesOnThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT /* total number of entries in the central directory on this disk */
1466                || entries.size() >= ZipConstants.ZIP64_MAGIC_SHORT /* total number of entries in the central directory */
1467                || cdLength >= ZipConstants.ZIP64_MAGIC /* size of the central directory */
1468                || cdOffset >= ZipConstants.ZIP64_MAGIC; /*
1469                                                          * offset of start of central directory with respect to the starting disk number
1470                                                          */
1471    }
1472
1473    private boolean usesDataDescriptor(final int zipMethod, final boolean phased) {
1474        return !phased && zipMethod == DEFLATED && !(outputStream instanceof RandomAccessOutputStream);
1475    }
1476
1477    /**
1478     * If the Zip64 mode is set to never, then all the data in End Of Central Directory should not exceed their limits.
1479     *
1480     * @throws Zip64RequiredException if Zip64 is actually needed
1481     */
1482    private void validateIfZip64IsNeededInEOCD() throws Zip64RequiredException {
1483        // exception will only be thrown if the Zip64 mode is never while Zip64 is actually needed
1484        if (zip64Mode != Zip64Mode.Never) {
1485            return;
1486        }
1487
1488        int numberOfThisDisk = 0;
1489        if (isSplitZip) {
1490            numberOfThisDisk = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex();
1491        }
1492        if (numberOfThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT) {
1493            throw new Zip64RequiredException(Zip64RequiredException.DISK_NUMBER_TOO_BIG_MESSAGE);
1494        }
1495
1496        if (cdDiskNumberStart >= ZipConstants.ZIP64_MAGIC_SHORT) {
1497            throw new Zip64RequiredException(Zip64RequiredException.CENTRAL_DIRECTORY_DISK_NUMBER_TOO_BIG_MESSAGE);
1498        }
1499
1500        final int numOfEntriesOnThisDisk = numberOfCDInDiskData.getOrDefault(numberOfThisDisk, 0);
1501        if (numOfEntriesOnThisDisk >= ZipConstants.ZIP64_MAGIC_SHORT) {
1502            throw new Zip64RequiredException(Zip64RequiredException.TOO_MANY_ENTRIES_ON_DISK_MESSAGE);
1503        }
1504
1505        // number of entries
1506        if (entries.size() >= ZipConstants.ZIP64_MAGIC_SHORT) {
1507            throw new Zip64RequiredException(Zip64RequiredException.TOO_MANY_ENTRIES_MESSAGE);
1508        }
1509
1510        if (cdLength >= ZipConstants.ZIP64_MAGIC) {
1511            throw new Zip64RequiredException(Zip64RequiredException.CENTRAL_DIRECTORY_SIZE_TOO_BIG_MESSAGE);
1512        }
1513
1514        if (cdOffset >= ZipConstants.ZIP64_MAGIC) {
1515            throw new Zip64RequiredException(Zip64RequiredException.ARCHIVE_TOO_BIG_MESSAGE);
1516        }
1517    }
1518
1519    /**
1520     * 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
1521     * Zip64 extra but the mode has been set to Never.
1522     */
1523    private void validateSizeInformation(final Zip64Mode effectiveMode) throws ZipException {
1524        // Size/CRC not required if SeekableByteChannel is used
1525        if (entry.entry.getMethod() == STORED && !(outputStream instanceof RandomAccessOutputStream)) {
1526            if (entry.entry.getSize() == ArchiveEntry.SIZE_UNKNOWN) {
1527                throw new ZipException("Uncompressed size is required for" + " STORED method when not writing to a" + " file");
1528            }
1529            if (entry.entry.getCrc() == ZipArchiveEntry.CRC_UNKNOWN) {
1530                throw new ZipException("CRC checksum is required for STORED" + " method when not writing to a file");
1531            }
1532            entry.entry.setCompressedSize(entry.entry.getSize());
1533        }
1534
1535        if ((entry.entry.getSize() >= ZipConstants.ZIP64_MAGIC || entry.entry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC)
1536                && effectiveMode == Zip64Mode.Never) {
1537            throw new Zip64RequiredException(Zip64RequiredException.getEntryTooBigMessage(entry.entry));
1538        }
1539    }
1540
1541    private int versionNeededToExtract(final int zipMethod, final boolean zip64, final boolean usedDataDescriptor) {
1542        if (zip64) {
1543            return ZipConstants.ZIP64_MIN_VERSION;
1544        }
1545        if (usedDataDescriptor) {
1546            return ZipConstants.DATA_DESCRIPTOR_MIN_VERSION;
1547        }
1548        return versionNeededToExtractMethod(zipMethod);
1549    }
1550
1551    private int versionNeededToExtractMethod(final int zipMethod) {
1552        return zipMethod == DEFLATED ? ZipConstants.DEFLATE_MIN_VERSION : ZipConstants.INITIAL_VERSION;
1553    }
1554
1555    /**
1556     * Writes bytes to ZIP entry.
1557     *
1558     * @param b      the byte array to write
1559     * @param offset the start position to write from
1560     * @param length the number of bytes to write
1561     * @throws IOException on error
1562     */
1563    @Override
1564    public void write(final byte[] b, final int offset, final int length) throws IOException {
1565        if (entry == null) {
1566            throw new IllegalStateException("No current entry");
1567        }
1568        ZipUtil.checkRequestedFeatures(entry.entry);
1569        final long writtenThisTime = streamCompressor.write(b, offset, length, entry.entry.getMethod());
1570        count(writtenThisTime);
1571    }
1572
1573    /**
1574     * Writes the &quot;End of central dir record&quot;.
1575     *
1576     * @throws IOException            on error
1577     * @throws Zip64RequiredException if the archive's size exceeds 4 GByte or there are more than 65535 entries inside the archive and
1578     *                                {@link #setUseZip64(Zip64Mode)} is {@link Zip64Mode#Never}.
1579     */
1580    protected void writeCentralDirectoryEnd() throws IOException {
1581        if (!hasUsedZip64 && isSplitZip) {
1582            ((ZipSplitOutputStream) this.outputStream).prepareToWriteUnsplittableContent(eocdLength);
1583        }
1584
1585        validateIfZip64IsNeededInEOCD();
1586
1587        writeCounted(EOCD_SIG);
1588
1589        // number of this disk
1590        int numberOfThisDisk = 0;
1591        if (isSplitZip) {
1592            numberOfThisDisk = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex();
1593        }
1594        writeCounted(ZipShort.getBytes(numberOfThisDisk));
1595
1596        // disk number of the start of central directory
1597        writeCounted(ZipShort.getBytes((int) cdDiskNumberStart));
1598
1599        // number of entries
1600        final int numberOfEntries = entries.size();
1601
1602        // total number of entries in the central directory on this disk
1603        final int numOfEntriesOnThisDisk = isSplitZip ? numberOfCDInDiskData.getOrDefault(numberOfThisDisk, 0) : numberOfEntries;
1604        final byte[] numOfEntriesOnThisDiskData = ZipShort.getBytes(Math.min(numOfEntriesOnThisDisk, ZipConstants.ZIP64_MAGIC_SHORT));
1605        writeCounted(numOfEntriesOnThisDiskData);
1606
1607        // number of entries
1608        final byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, ZipConstants.ZIP64_MAGIC_SHORT));
1609        writeCounted(num);
1610
1611        // length and location of CD
1612        writeCounted(ZipLong.getBytes(Math.min(cdLength, ZipConstants.ZIP64_MAGIC)));
1613        writeCounted(ZipLong.getBytes(Math.min(cdOffset, ZipConstants.ZIP64_MAGIC)));
1614
1615        // ZIP file comment
1616        final ByteBuffer data = this.zipEncoding.encode(comment);
1617        final int dataLen = data.limit() - data.position();
1618        writeCounted(ZipShort.getBytes(dataLen));
1619        streamCompressor.writeCounted(data.array(), data.arrayOffset(), dataLen);
1620    }
1621
1622    private void writeCentralDirectoryInChunks() throws IOException {
1623        final int NUM_PER_WRITE = 1000;
1624        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(70 * NUM_PER_WRITE);
1625        int count = 0;
1626        for (final ZipArchiveEntry ze : entries) {
1627            byteArrayOutputStream.write(createCentralFileHeader(ze));
1628            if (++count > NUM_PER_WRITE) {
1629                writeCounted(byteArrayOutputStream.toByteArray());
1630                byteArrayOutputStream.reset();
1631                count = 0;
1632            }
1633        }
1634        writeCounted(byteArrayOutputStream.toByteArray());
1635    }
1636
1637    /**
1638     * Writes the central file header entry.
1639     *
1640     * @param ze the entry to write
1641     * @throws IOException            on error
1642     * @throws Zip64RequiredException if the archive's size exceeds 4 GByte and {@link #setUseZip64(Zip64Mode)} is {@link Zip64Mode#Never}.
1643     */
1644    protected void writeCentralFileHeader(final ZipArchiveEntry ze) throws IOException {
1645        final byte[] centralFileHeader = createCentralFileHeader(ze);
1646        writeCounted(centralFileHeader);
1647    }
1648
1649    /**
1650     * Write bytes to output or random access file.
1651     *
1652     * @param data the byte array to write
1653     * @throws IOException on error
1654     */
1655    private void writeCounted(final byte[] data) throws IOException {
1656        streamCompressor.writeCounted(data);
1657    }
1658
1659    /**
1660     * Writes the data descriptor entry.
1661     *
1662     * @param ze the entry to write
1663     * @throws IOException on error
1664     */
1665    protected void writeDataDescriptor(final ZipArchiveEntry ze) throws IOException {
1666        if (!usesDataDescriptor(ze.getMethod(), false)) {
1667            return;
1668        }
1669        writeCounted(DD_SIG);
1670        writeCounted(ZipLong.getBytes(ze.getCrc()));
1671        if (!hasZip64Extra(ze)) {
1672            writeCounted(ZipLong.getBytes(ze.getCompressedSize()));
1673            writeCounted(ZipLong.getBytes(ze.getSize()));
1674        } else {
1675            writeCounted(ZipEightByteInteger.getBytes(ze.getCompressedSize()));
1676            writeCounted(ZipEightByteInteger.getBytes(ze.getSize()));
1677        }
1678    }
1679
1680    /**
1681     * Writes the local file header entry
1682     *
1683     * @param ze the entry to write
1684     * @throws IOException on error
1685     */
1686    protected void writeLocalFileHeader(final ZipArchiveEntry ze) throws IOException {
1687        writeLocalFileHeader(ze, false);
1688    }
1689
1690    private void writeLocalFileHeader(final ZipArchiveEntry ze, final boolean phased) throws IOException {
1691        final boolean encodable = zipEncoding.canEncode(ze.getName());
1692        final ByteBuffer name = getName(ze);
1693
1694        if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
1695            addUnicodeExtraFields(ze, encodable, name);
1696        }
1697
1698        long localHeaderStart = streamCompressor.getTotalBytesWritten();
1699        if (isSplitZip) {
1700            // when creating a split zip, the offset should be
1701            // the offset to the corresponding segment disk
1702            final ZipSplitOutputStream splitOutputStream = (ZipSplitOutputStream) this.outputStream;
1703            ze.setDiskNumberStart(splitOutputStream.getCurrentSplitSegmentIndex());
1704            localHeaderStart = splitOutputStream.getCurrentSplitSegmentBytesWritten();
1705        }
1706
1707        final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased, localHeaderStart);
1708        metaData.put(ze, new EntryMetaData(localHeaderStart, usesDataDescriptor(ze.getMethod(), phased)));
1709        entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset
1710        writeCounted(localHeader);
1711        entry.dataStart = streamCompressor.getTotalBytesWritten();
1712    }
1713
1714    /**
1715     * Write bytes to output or random access file.
1716     *
1717     * @param data the byte array to write
1718     * @throws IOException on error
1719     */
1720    protected final void writeOut(final byte[] data) throws IOException {
1721        streamCompressor.writeOut(data, 0, data.length);
1722    }
1723
1724    /**
1725     * Write bytes to output or random access file.
1726     *
1727     * @param data   the byte array to write
1728     * @param offset the start position to write from
1729     * @param length the number of bytes to write
1730     * @throws IOException on error
1731     */
1732    protected final void writeOut(final byte[] data, final int offset, final int length) throws IOException {
1733        streamCompressor.writeOut(data, offset, length);
1734    }
1735
1736    /**
1737     * Write preamble data. For most of the time, this is used to make self-extracting zips.
1738     *
1739     * @param preamble data to write
1740     * @throws IOException if an entry already exists
1741     * @since 1.21
1742     */
1743    public void writePreamble(final byte[] preamble) throws IOException {
1744        writePreamble(preamble, 0, preamble.length);
1745    }
1746
1747    /**
1748     * Write preamble data. For most of the time, this is used to make self-extracting zips.
1749     *
1750     * @param preamble data to write
1751     * @param offset   the start offset in the data
1752     * @param length   the number of bytes to write
1753     * @throws IOException if an entry already exists
1754     * @since 1.21
1755     */
1756    public void writePreamble(final byte[] preamble, final int offset, final int length) throws IOException {
1757        if (entry != null) {
1758            throw new IllegalStateException("Preamble must be written before creating an entry");
1759        }
1760        this.streamCompressor.writeCounted(preamble, offset, length);
1761    }
1762
1763    /**
1764     * Writes the &quot;ZIP64 End of central dir record&quot; and &quot;ZIP64 End of central dir locator&quot;.
1765     *
1766     * @throws IOException on error
1767     * @since 1.3
1768     */
1769    protected void writeZip64CentralDirectory() throws IOException {
1770        if (zip64Mode == Zip64Mode.Never) {
1771            return;
1772        }
1773
1774        if (!hasUsedZip64 && shouldUseZip64EOCD()) {
1775            // actually "will use"
1776            hasUsedZip64 = true;
1777        }
1778
1779        if (!hasUsedZip64) {
1780            return;
1781        }
1782
1783        long offset = streamCompressor.getTotalBytesWritten();
1784        long diskNumberStart = 0L;
1785        if (isSplitZip) {
1786            // when creating a split zip, the offset of should be
1787            // the offset to the corresponding segment disk
1788            final ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream) this.outputStream;
1789            offset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten();
1790            diskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex();
1791        }
1792
1793        writeOut(ZIP64_EOCD_SIG);
1794        // size of zip64 end of central directory, we don't have any variable length
1795        // as we don't support the extensible data sector, yet
1796        writeOut(ZipEightByteInteger.getBytes(ZipConstants.SHORT /* version made by */
1797                + ZipConstants.SHORT /* version needed to extract */
1798                + ZipConstants.WORD /* disk number */
1799                + ZipConstants.WORD /* disk with central directory */
1800                + ZipConstants.DWORD /* number of entries in CD on this disk */
1801                + ZipConstants.DWORD /* total number of entries */
1802                + ZipConstants.DWORD /* size of CD */
1803                + (long) ZipConstants.DWORD /* offset of CD */
1804        ));
1805
1806        // version made by and version needed to extract
1807        writeOut(ZipShort.getBytes(ZipConstants.ZIP64_MIN_VERSION));
1808        writeOut(ZipShort.getBytes(ZipConstants.ZIP64_MIN_VERSION));
1809
1810        // number of this disk
1811        int numberOfThisDisk = 0;
1812        if (isSplitZip) {
1813            numberOfThisDisk = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex();
1814        }
1815        writeOut(ZipLong.getBytes(numberOfThisDisk));
1816
1817        // disk number of the start of central directory
1818        writeOut(ZipLong.getBytes(cdDiskNumberStart));
1819
1820        // total number of entries in the central directory on this disk
1821        final int numOfEntriesOnThisDisk = isSplitZip ? numberOfCDInDiskData.getOrDefault(numberOfThisDisk, 0) : entries.size();
1822        final byte[] numOfEntriesOnThisDiskData = ZipEightByteInteger.getBytes(numOfEntriesOnThisDisk);
1823        writeOut(numOfEntriesOnThisDiskData);
1824
1825        // number of entries
1826        final byte[] num = ZipEightByteInteger.getBytes(entries.size());
1827        writeOut(num);
1828
1829        // length and location of CD
1830        writeOut(ZipEightByteInteger.getBytes(cdLength));
1831        writeOut(ZipEightByteInteger.getBytes(cdOffset));
1832
1833        // no "zip64 extensible data sector" for now
1834
1835        if (isSplitZip) {
1836            // based on the ZIP specification, the End Of Central Directory record and
1837            // the Zip64 End Of Central Directory locator record must be on the same segment
1838            final int zip64EOCDLOCLength = ZipConstants.WORD /* length of ZIP64_EOCD_LOC_SIG */
1839                    + ZipConstants.WORD /* disk number of ZIP64_EOCD_SIG */
1840                    + ZipConstants.DWORD /* offset of ZIP64_EOCD_SIG */
1841                    + ZipConstants.WORD /* total number of disks */;
1842
1843            final long unsplittableContentSize = zip64EOCDLOCLength + eocdLength;
1844            ((ZipSplitOutputStream) this.outputStream).prepareToWriteUnsplittableContent(unsplittableContentSize);
1845        }
1846
1847        // and now the "ZIP64 end of central directory locator"
1848        writeOut(ZIP64_EOCD_LOC_SIG);
1849
1850        // disk number holding the ZIP64 EOCD record
1851        writeOut(ZipLong.getBytes(diskNumberStart));
1852        // relative offset of ZIP64 EOCD record
1853        writeOut(ZipEightByteInteger.getBytes(offset));
1854        // total number of disks
1855        if (isSplitZip) {
1856            // the Zip64 End Of Central Directory Locator and the End Of Central Directory must be
1857            // in the same split disk, it means they must be located in the last disk
1858            final int totalNumberOfDisks = ((ZipSplitOutputStream) this.outputStream).getCurrentSplitSegmentIndex() + 1;
1859            writeOut(ZipLong.getBytes(totalNumberOfDisks));
1860        } else {
1861            writeOut(ONE);
1862        }
1863    }
1864}