View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.compress.archivers.zip;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.nio.ByteBuffer;
25  import java.nio.channels.SeekableByteChannel;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.LinkOption;
29  import java.nio.file.OpenOption;
30  import java.nio.file.Path;
31  import java.util.HashMap;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.zip.Deflater;
36  import java.util.zip.ZipException;
37  
38  import org.apache.commons.compress.archivers.ArchiveEntry;
39  import org.apache.commons.compress.archivers.ArchiveOutputStream;
40  import org.apache.commons.compress.utils.ByteUtils;
41  
42  /**
43   * Reimplementation of {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} to handle the extended functionality of this package, especially
44   * internal/external file attributes and extra fields with different layouts for local file data and central directory entries.
45   * <p>
46   * 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
47   * created.
48   * </p>
49   * <p>
50   * If SeekableByteChannel cannot be used, this implementation will use a Data Descriptor to store size and CRC information for {@link #DEFLATED DEFLATED}
51   * 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
52   * uncompressed size information is required before {@link #putArchiveEntry(ZipArchiveEntry)} can be called.
53   * </p>
54   * <p>
55   * 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
56   * 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
57   * write a ZipArchiveEntry of unknown size, then Zip64 extensions will be disabled by default.
58   * </p>
59   *
60   * @NotThreadSafe
61   */
62  public class ZipArchiveOutputStream extends ArchiveOutputStream<ZipArchiveEntry> {
63  
64      /**
65       * Structure collecting information for the entry that is currently being written.
66       */
67      private static final class CurrentEntry {
68  
69          /**
70           * Current ZIP entry.
71           */
72          private final ZipArchiveEntry entry;
73  
74          /**
75           * Offset for CRC entry in the local file header data for the current entry starts here.
76           */
77          private long localDataStart;
78  
79          /**
80           * Data for local header data
81           */
82          private long dataStart;
83  
84          /**
85           * Number of bytes read for the current entry (can't rely on Deflater#getBytesRead) when using DEFLATED.
86           */
87          private long bytesRead;
88  
89          /**
90           * Whether current entry was the first one using ZIP64 features.
91           */
92          private boolean causedUseOfZip64;
93  
94          /**
95           * Whether write() has been called at all.
96           *
97           * <p>
98           * In order to create a valid archive {@link #closeArchiveEntry closeArchiveEntry} will write an empty array to get the CRC right if nothing has been
99           * 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 }