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     *
017     */
018    package org.apache.commons.compress.archivers.zip;
019    
020    import java.io.File;
021    import java.io.FileOutputStream;
022    import java.io.IOException;
023    import java.io.OutputStream;
024    import java.io.RandomAccessFile;
025    import java.nio.ByteBuffer;
026    import java.util.HashMap;
027    import java.util.Iterator;
028    import java.util.LinkedList;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.zip.CRC32;
032    import java.util.zip.Deflater;
033    import java.util.zip.ZipException;
034    
035    import org.apache.commons.compress.archivers.ArchiveEntry;
036    import org.apache.commons.compress.archivers.ArchiveOutputStream;
037    
038    /**
039     * Reimplementation of {@link java.util.zip.ZipOutputStream
040     * java.util.zip.ZipOutputStream} that does handle the extended
041     * functionality of this package, especially internal/external file
042     * attributes and extra fields with different layouts for local file
043     * data and central directory entries.
044     *
045     * <p>This class will try to use {@link java.io.RandomAccessFile
046     * RandomAccessFile} when you know that the output is going to go to a
047     * file.</p>
048     *
049     * <p>If RandomAccessFile cannot be used, this implementation will use
050     * a Data Descriptor to store size and CRC information for {@link
051     * #DEFLATED DEFLATED} entries, this means, you don't need to
052     * calculate them yourself.  Unfortunately this is not possible for
053     * the {@link #STORED STORED} method, here setting the CRC and
054     * uncompressed size information is required before {@link
055     * #putArchiveEntry(ArchiveEntry)} can be called.</p>
056     * @NotThreadSafe
057     */
058    public class ZipArchiveOutputStream extends ArchiveOutputStream {
059    
060        static final int BYTE_MASK = 0xFF;
061        private static final int SHORT = 2;
062        private static final int WORD = 4;
063        static final int BUFFER_SIZE = 512;
064    
065        /** indicates if this archive is finished. protected for use in Jar implementation */
066        protected boolean finished = false;
067    
068        /* 
069         * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs
070         * when it gets handed a really big buffer.  See
071         * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396
072         *
073         * Using a buffer size of 8 kB proved to be a good compromise
074         */
075        private static final int DEFLATER_BLOCK_SIZE = 8192;
076    
077        /**
078         * Compression method for deflated entries.
079         */
080        public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
081    
082        /**
083         * Default compression level for deflated entries.
084         */
085        public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
086    
087        /**
088         * Compression method for stored entries.
089         */
090        public static final int STORED = java.util.zip.ZipEntry.STORED;
091    
092        /**
093         * default encoding for file names and comment.
094         */
095        static final String DEFAULT_ENCODING = ZipEncodingHelper.UTF8;
096    
097        /**
098         * General purpose flag, which indicates that filenames are
099         * written in utf-8.
100         * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead
101         */
102        public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG;
103    
104        /**
105         * Current entry.
106         */
107        private ZipArchiveEntry entry;
108    
109        /**
110         * The file comment.
111         */
112        private String comment = "";
113    
114        /**
115         * Compression level for next entry.
116         */
117        private int level = DEFAULT_COMPRESSION;
118    
119        /**
120         * Has the compression level changed when compared to the last
121         * entry?
122         */
123        private boolean hasCompressionLevelChanged = false;
124    
125        /**
126         * Default compression method for next entry.
127         */
128        private int method = java.util.zip.ZipEntry.DEFLATED;
129    
130        /**
131         * List of ZipArchiveEntries written so far.
132         */
133        private final List entries = new LinkedList();
134    
135        /**
136         * CRC instance to avoid parsing DEFLATED data twice.
137         */
138        private final CRC32 crc = new CRC32();
139    
140        /**
141         * Count the bytes written to out.
142         */
143        private long written = 0;
144    
145        /**
146         * Data for local header data
147         */
148        private long dataStart = 0;
149    
150        /**
151         * Offset for CRC entry in the local file header data for the
152         * current entry starts here.
153         */
154        private long localDataStart = 0;
155    
156        /**
157         * Start of central directory.
158         */
159        private long cdOffset = 0;
160    
161        /**
162         * Length of central directory.
163         */
164        private long cdLength = 0;
165    
166        /**
167         * Helper, a 0 as ZipShort.
168         */
169        private static final byte[] ZERO = {0, 0};
170    
171        /**
172         * Helper, a 0 as ZipLong.
173         */
174        private static final byte[] LZERO = {0, 0, 0, 0};
175    
176        /**
177         * Holds the offsets of the LFH starts for each entry.
178         */
179        private final Map offsets = new HashMap();
180    
181        /**
182         * The encoding to use for filenames and the file comment.
183         *
184         * <p>For a list of possible values see <a
185         * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
186         * Defaults to UTF-8.</p>
187         */
188        private String encoding = DEFAULT_ENCODING;
189    
190        /**
191         * The zip encoding to use for filenames and the file comment.
192         *
193         * This field is of internal use and will be set in {@link
194         * #setEncoding(String)}.
195         */
196        private ZipEncoding zipEncoding =
197            ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING);
198    
199        /**
200         * This Deflater object is used for output.
201         *
202         */
203        protected final Deflater def = new Deflater(level, true);
204    
205        /**
206         * This buffer servers as a Deflater.
207         *
208         */
209        private final byte[] buf = new byte[BUFFER_SIZE];
210    
211        /**
212         * Optional random access output.
213         */
214        private final RandomAccessFile raf;
215    
216        private final OutputStream out;
217    
218        /**
219         * whether to use the general purpose bit flag when writing UTF-8
220         * filenames or not.
221         */
222        private boolean useUTF8Flag = true; 
223    
224        /**
225         * Whether to encode non-encodable file names as UTF-8.
226         */
227        private boolean fallbackToUTF8 = false;
228    
229        /**
230         * whether to create UnicodePathExtraField-s for each entry.
231         */
232        private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;
233    
234        /**
235         * Creates a new ZIP OutputStream filtering the underlying stream.
236         * @param out the outputstream to zip
237         */
238        public ZipArchiveOutputStream(OutputStream out) {
239            this.out = out;
240            this.raf = null;
241        }
242    
243        /**
244         * Creates a new ZIP OutputStream writing to a File.  Will use
245         * random access if possible.
246         * @param file the file to zip to
247         * @throws IOException on error
248         */
249        public ZipArchiveOutputStream(File file) throws IOException {
250            OutputStream o = null;
251            RandomAccessFile _raf = null;
252            try {
253                _raf = new RandomAccessFile(file, "rw");
254                _raf.setLength(0);
255            } catch (IOException e) {
256                if (_raf != null) {
257                    try {
258                        _raf.close();
259                    } catch (IOException inner) {
260                        // ignore
261                    }
262                    _raf = null;
263                }
264                o = new FileOutputStream(file);
265            }
266            out = o;
267            raf = _raf;
268        }
269    
270        /**
271         * This method indicates whether this archive is writing to a
272         * seekable stream (i.e., to a random access file).
273         *
274         * <p>For seekable streams, you don't need to calculate the CRC or
275         * uncompressed size for {@link #STORED} entries before
276         * invoking {@link #putArchiveEntry(ArchiveEntry)}.
277         * @return true if seekable
278         */
279        public boolean isSeekable() {
280            return raf != null;
281        }
282    
283        /**
284         * The encoding to use for filenames and the file comment.
285         *
286         * <p>For a list of possible values see <a
287         * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
288         * Defaults to UTF-8.</p>
289         * @param encoding the encoding to use for file names, use null
290         * for the platform's default encoding
291         */
292        public void setEncoding(final String encoding) {
293            this.encoding = encoding;
294            this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
295            useUTF8Flag &= ZipEncodingHelper.isUTF8(encoding);
296        }
297    
298        /**
299         * The encoding to use for filenames and the file comment.
300         *
301         * @return null if using the platform's default character encoding.
302         */
303        public String getEncoding() {
304            return encoding;
305        }
306    
307        /**
308         * Whether to set the language encoding flag if the file name
309         * encoding is UTF-8.
310         *
311         * <p>Defaults to true.</p>
312         */
313        public void setUseLanguageEncodingFlag(boolean b) {
314            useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding);
315        }
316    
317        /**
318         * Whether to create Unicode Extra Fields.
319         *
320         * <p>Defaults to NEVER.</p>
321         */
322        public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) {
323            createUnicodeExtraFields = b;
324        }
325    
326        /**
327         * Whether to fall back to UTF and the language encoding flag if
328         * the file name cannot be encoded using the specified encoding.
329         *
330         * <p>Defaults to false.</p>
331         */
332        public void setFallbackToUTF8(boolean b) {
333            fallbackToUTF8 = b;
334        }
335    
336        /** {@inheritDoc} */
337        public void finish() throws IOException {
338            if (finished) {
339                throw new IOException("This archive has already been finished");
340            }
341    
342            if (entry != null) {
343                throw new IOException("This archives contains unclosed entries.");
344            }
345    
346            cdOffset = written;
347            for (Iterator i = entries.iterator(); i.hasNext(); ) {
348                writeCentralFileHeader((ZipArchiveEntry) i.next());
349            }
350            cdLength = written - cdOffset;
351            writeCentralDirectoryEnd();
352            offsets.clear();
353            entries.clear();
354            finished = true;
355        }
356    
357        /**
358         * Writes all necessary data for this entry.
359         * @throws IOException on error
360         */
361        public void closeArchiveEntry() throws IOException {
362            if (finished) {
363                throw new IOException("Stream has already been finished");
364            }
365    
366            if (entry == null) {
367                throw new IOException("No current entry to close");
368            }
369    
370            long realCrc = crc.getValue();
371            crc.reset();
372    
373            if (entry.getMethod() == DEFLATED) {
374                def.finish();
375                while (!def.finished()) {
376                    deflate();
377                }
378    
379                entry.setSize(ZipUtil.adjustToLong(def.getTotalIn()));
380                entry.setCompressedSize(ZipUtil.adjustToLong(def.getTotalOut()));
381                entry.setCrc(realCrc);
382    
383                def.reset();
384    
385                written += entry.getCompressedSize();
386            } else if (raf == null) {
387                if (entry.getCrc() != realCrc) {
388                    throw new ZipException("bad CRC checksum for entry "
389                                           + entry.getName() + ": "
390                                           + Long.toHexString(entry.getCrc())
391                                           + " instead of "
392                                           + Long.toHexString(realCrc));
393                }
394    
395                if (entry.getSize() != written - dataStart) {
396                    throw new ZipException("bad size for entry "
397                                           + entry.getName() + ": "
398                                           + entry.getSize()
399                                           + " instead of "
400                                           + (written - dataStart));
401                }
402            } else { /* method is STORED and we used RandomAccessFile */
403                long size = written - dataStart;
404    
405                entry.setSize(size);
406                entry.setCompressedSize(size);
407                entry.setCrc(realCrc);
408            }
409    
410            // If random access output, write the local file header containing
411            // the correct CRC and compressed/uncompressed sizes
412            if (raf != null) {
413                long save = raf.getFilePointer();
414    
415                raf.seek(localDataStart);
416                writeOut(ZipLong.getBytes(entry.getCrc()));
417                writeOut(ZipLong.getBytes(entry.getCompressedSize()));
418                writeOut(ZipLong.getBytes(entry.getSize()));
419                raf.seek(save);
420            }
421    
422            writeDataDescriptor(entry);
423            entry = null;
424        }
425    
426        /**
427         * {@inheritDoc} 
428         * @throws ClassCastException if entry is not an instance of ZipArchiveEntry
429         */
430        public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException {
431            if (finished) {
432                throw new IOException("Stream has already been finished");
433            }
434    
435            if (entry != null) {
436                closeArchiveEntry();
437            }
438    
439            entry = ((ZipArchiveEntry) archiveEntry);
440            entries.add(entry);
441    
442            if (entry.getMethod() == -1) { // not specified
443                entry.setMethod(method);
444            }
445    
446            if (entry.getTime() == -1) { // not specified
447                entry.setTime(System.currentTimeMillis());
448            }
449    
450            // Size/CRC not required if RandomAccessFile is used
451            if (entry.getMethod() == STORED && raf == null) {
452                if (entry.getSize() == -1) {
453                    throw new ZipException("uncompressed size is required for"
454                                           + " STORED method when not writing to a"
455                                           + " file");
456                }
457                if (entry.getCrc() == -1) {
458                    throw new ZipException("crc checksum is required for STORED"
459                                           + " method when not writing to a file");
460                }
461                entry.setCompressedSize(entry.getSize());
462            }
463    
464            if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
465                def.setLevel(level);
466                hasCompressionLevelChanged = false;
467            }
468            writeLocalFileHeader(entry);
469        }
470    
471        /**
472         * Set the file comment.
473         * @param comment the comment
474         */
475        public void setComment(String comment) {
476            this.comment = comment;
477        }
478    
479        /**
480         * Sets the compression level for subsequent entries.
481         *
482         * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
483         * @param level the compression level.
484         * @throws IllegalArgumentException if an invalid compression
485         * level is specified.
486         */
487        public void setLevel(int level) {
488            if (level < Deflater.DEFAULT_COMPRESSION
489                || level > Deflater.BEST_COMPRESSION) {
490                throw new IllegalArgumentException("Invalid compression level: "
491                                                   + level);
492            }
493            hasCompressionLevelChanged = (this.level != level);
494            this.level = level;
495        }
496    
497        /**
498         * Sets the default compression method for subsequent entries.
499         *
500         * <p>Default is DEFLATED.</p>
501         * @param method an <code>int</code> from java.util.zip.ZipEntry
502         */
503        public void setMethod(int method) {
504            this.method = method;
505        }
506    
507        /**
508         * Whether this stream is able to write the given entry.
509         *
510         * <p>May return false if it is set up to use encryption or a
511         * compression method that hasn't been implemented yet.</p>
512         * @since Apache Commons Compress 1.1
513         */
514        public boolean canWriteEntryData(ArchiveEntry ae) {
515            if (ae instanceof ZipArchiveEntry) {
516                return ZipUtil.canHandleEntryData((ZipArchiveEntry) ae);
517            }
518            return false;
519        }
520    
521        /**
522         * Writes bytes to ZIP entry.
523         * @param b the byte array to write
524         * @param offset the start position to write from
525         * @param length the number of bytes to write
526         * @throws IOException on error
527         */
528        public void write(byte[] b, int offset, int length) throws IOException {
529            ZipUtil.checkRequestedFeatures(entry);
530            if (entry.getMethod() == DEFLATED) {
531                if (length > 0 && !def.finished()) {
532                    if (length <= DEFLATER_BLOCK_SIZE) {
533                        def.setInput(b, offset, length);
534                        deflateUntilInputIsNeeded();
535                    } else {
536                        final int fullblocks = length / DEFLATER_BLOCK_SIZE;
537                        for (int i = 0; i < fullblocks; i++) {
538                            def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE,
539                                         DEFLATER_BLOCK_SIZE);
540                            deflateUntilInputIsNeeded();
541                        }
542                        final int done = fullblocks * DEFLATER_BLOCK_SIZE;
543                        if (done < length) {
544                            def.setInput(b, offset + done, length - done);
545                            deflateUntilInputIsNeeded();
546                        }
547                    }
548                }
549            } else {
550                writeOut(b, offset, length);
551                written += length;
552            }
553            crc.update(b, offset, length);
554            count(length);
555        }
556    
557        /**
558         * Closes this output stream and releases any system resources
559         * associated with the stream.
560         *
561         * @exception  IOException  if an I/O error occurs.
562         */
563        public void close() throws IOException {
564            if (!finished) {
565                finish();
566            }
567    
568            if (raf != null) {
569                raf.close();
570            }
571            if (out != null) {
572                out.close();
573            }
574        }
575    
576        /**
577         * Flushes this output stream and forces any buffered output bytes
578         * to be written out to the stream.
579         *
580         * @exception  IOException  if an I/O error occurs.
581         */
582        public void flush() throws IOException {
583            if (out != null) {
584                out.flush();
585            }
586        }
587    
588        /*
589         * Various ZIP constants
590         */
591        /**
592         * local file header signature
593         */
594        static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes();
595        /**
596         * data descriptor signature
597         */
598        static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes();
599        /**
600         * central file header signature
601         */
602        static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes();
603        /**
604         * end of central dir signature
605         */
606        static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
607    
608        /**
609         * Writes next block of compressed data to the output stream.
610         * @throws IOException on error
611         */
612        protected final void deflate() throws IOException {
613            int len = def.deflate(buf, 0, buf.length);
614            if (len > 0) {
615                writeOut(buf, 0, len);
616            }
617        }
618    
619        /**
620         * Writes the local file header entry
621         * @param ze the entry to write
622         * @throws IOException on error
623         */
624        protected void writeLocalFileHeader(ZipArchiveEntry ze) throws IOException {
625    
626            boolean encodable = zipEncoding.canEncode(ze.getName());
627    
628            final ZipEncoding entryEncoding;
629    
630            if (!encodable && fallbackToUTF8) {
631                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
632            } else {
633                entryEncoding = zipEncoding;
634            }
635    
636            ByteBuffer name = entryEncoding.encode(ze.getName());
637    
638            if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
639    
640                if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
641                    || !encodable) {
642                    ze.addExtraField(new UnicodePathExtraField(ze.getName(),
643                                                               name.array(),
644                                                               name.arrayOffset(),
645                                                               name.limit()));
646                }
647    
648                String comm = ze.getComment();
649                if (comm != null && !"".equals(comm)) {
650    
651                    boolean commentEncodable = this.zipEncoding.canEncode(comm);
652    
653                    if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
654                        || !commentEncodable) {
655                        ByteBuffer commentB = entryEncoding.encode(comm);
656                        ze.addExtraField(new UnicodeCommentExtraField(comm,
657                                                                      commentB.array(),
658                                                                      commentB.arrayOffset(),
659                                                                      commentB.limit())
660                                         );
661                    }
662                }
663            }
664    
665            offsets.put(ze, ZipLong.getBytes(written));
666    
667            writeOut(LFH_SIG);
668            written += WORD;
669    
670            //store method in local variable to prevent multiple method calls
671            final int zipMethod = ze.getMethod();
672    
673            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
674                                                             !encodable
675                                                             && fallbackToUTF8);
676            written += WORD;
677    
678            // compression method
679            writeOut(ZipShort.getBytes(zipMethod));
680            written += SHORT;
681    
682            // last mod. time and date
683            writeOut(ZipUtil.toDosTime(ze.getTime()));
684            written += WORD;
685    
686            // CRC
687            // compressed length
688            // uncompressed length
689            localDataStart = written;
690            if (zipMethod == DEFLATED || raf != null) {
691                writeOut(LZERO);
692                writeOut(LZERO);
693                writeOut(LZERO);
694            } else {
695                writeOut(ZipLong.getBytes(ze.getCrc()));
696                writeOut(ZipLong.getBytes(ze.getSize()));
697                writeOut(ZipLong.getBytes(ze.getSize()));
698            }
699            // CheckStyle:MagicNumber OFF
700            written += 12;
701            // CheckStyle:MagicNumber ON
702    
703            // file name length
704            writeOut(ZipShort.getBytes(name.limit()));
705            written += SHORT;
706    
707            // extra field length
708            byte[] extra = ze.getLocalFileDataExtra();
709            writeOut(ZipShort.getBytes(extra.length));
710            written += SHORT;
711    
712            // file name
713            writeOut(name.array(), name.arrayOffset(), name.limit());
714            written += name.limit();
715    
716            // extra field
717            writeOut(extra);
718            written += extra.length;
719    
720            dataStart = written;
721        }
722    
723        /**
724         * Writes the data descriptor entry.
725         * @param ze the entry to write
726         * @throws IOException on error
727         */
728        protected void writeDataDescriptor(ZipArchiveEntry ze) throws IOException {
729            if (ze.getMethod() != DEFLATED || raf != null) {
730                return;
731            }
732            writeOut(DD_SIG);
733            writeOut(ZipLong.getBytes(entry.getCrc()));
734            writeOut(ZipLong.getBytes(entry.getCompressedSize()));
735            writeOut(ZipLong.getBytes(entry.getSize()));
736            // CheckStyle:MagicNumber OFF
737            written += 16;
738            // CheckStyle:MagicNumber ON
739        }
740    
741        /**
742         * Writes the central file header entry.
743         * @param ze the entry to write
744         * @throws IOException on error
745         */
746        protected void writeCentralFileHeader(ZipArchiveEntry ze) throws IOException {
747            writeOut(CFH_SIG);
748            written += WORD;
749    
750            // version made by
751            // CheckStyle:MagicNumber OFF
752            writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
753            written += SHORT;
754    
755            final int zipMethod = ze.getMethod();
756            final boolean encodable = zipEncoding.canEncode(ze.getName());
757            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
758                                                             !encodable
759                                                             && fallbackToUTF8);
760            written += WORD;
761    
762            // compression method
763            writeOut(ZipShort.getBytes(zipMethod));
764            written += SHORT;
765    
766            // last mod. time and date
767            writeOut(ZipUtil.toDosTime(ze.getTime()));
768            written += WORD;
769    
770            // CRC
771            // compressed length
772            // uncompressed length
773            writeOut(ZipLong.getBytes(ze.getCrc()));
774            writeOut(ZipLong.getBytes(ze.getCompressedSize()));
775            writeOut(ZipLong.getBytes(ze.getSize()));
776            // CheckStyle:MagicNumber OFF
777            written += 12;
778            // CheckStyle:MagicNumber ON
779    
780            // file name length
781            final ZipEncoding entryEncoding;
782    
783            if (!encodable && fallbackToUTF8) {
784                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
785            } else {
786                entryEncoding = zipEncoding;
787            }
788    
789            ByteBuffer name = entryEncoding.encode(ze.getName());
790    
791            writeOut(ZipShort.getBytes(name.limit()));
792            written += SHORT;
793    
794            // extra field length
795            byte[] extra = ze.getCentralDirectoryExtra();
796            writeOut(ZipShort.getBytes(extra.length));
797            written += SHORT;
798    
799            // file comment length
800            String comm = ze.getComment();
801            if (comm == null) {
802                comm = "";
803            }
804    
805            ByteBuffer commentB = entryEncoding.encode(comm);
806    
807            writeOut(ZipShort.getBytes(commentB.limit()));
808            written += SHORT;
809    
810            // disk number start
811            writeOut(ZERO);
812            written += SHORT;
813    
814            // internal file attributes
815            writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
816            written += SHORT;
817    
818            // external file attributes
819            writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
820            written += WORD;
821    
822            // relative offset of LFH
823            writeOut((byte[]) offsets.get(ze));
824            written += WORD;
825    
826            // file name
827            writeOut(name.array(), name.arrayOffset(), name.limit());
828            written += name.limit();
829    
830            // extra field
831            writeOut(extra);
832            written += extra.length;
833    
834            // file comment
835            writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit());
836            written += commentB.limit();
837        }
838    
839        /**
840         * Writes the &quot;End of central dir record&quot;.
841         * @throws IOException on error
842         */
843        protected void writeCentralDirectoryEnd() throws IOException {
844            writeOut(EOCD_SIG);
845    
846            // disk numbers
847            writeOut(ZERO);
848            writeOut(ZERO);
849    
850            // number of entries
851            byte[] num = ZipShort.getBytes(entries.size());
852            writeOut(num);
853            writeOut(num);
854    
855            // length and location of CD
856            writeOut(ZipLong.getBytes(cdLength));
857            writeOut(ZipLong.getBytes(cdOffset));
858    
859            // ZIP file comment
860            ByteBuffer data = this.zipEncoding.encode(comment);
861            writeOut(ZipShort.getBytes(data.limit()));
862            writeOut(data.array(), data.arrayOffset(), data.limit());
863        }
864    
865        /**
866         * Write bytes to output or random access file.
867         * @param data the byte array to write
868         * @throws IOException on error
869         */
870        protected final void writeOut(byte[] data) throws IOException {
871            writeOut(data, 0, data.length);
872        }
873    
874        /**
875         * Write bytes to output or random access file.
876         * @param data the byte array to write
877         * @param offset the start position to write from
878         * @param length the number of bytes to write
879         * @throws IOException on error
880         */
881        protected final void writeOut(byte[] data, int offset, int length)
882            throws IOException {
883            if (raf != null) {
884                raf.write(data, offset, length);
885            } else {
886                out.write(data, offset, length);
887            }
888        }
889    
890        private void deflateUntilInputIsNeeded() throws IOException {
891            while (!def.needsInput()) {
892                deflate();
893            }
894        }
895    
896        private void writeVersionNeededToExtractAndGeneralPurposeBits(final int
897                                                                      zipMethod,
898                                                                      final boolean
899                                                                      utfFallback)
900            throws IOException {
901    
902            // CheckStyle:MagicNumber OFF
903            int versionNeededToExtract = 10;
904            GeneralPurposeBit b = new GeneralPurposeBit();
905            b.useUTF8ForNames(useUTF8Flag || utfFallback);
906            if (zipMethod == DEFLATED && raf == null) {
907                // requires version 2 as we are going to store length info
908                // in the data descriptor
909                versionNeededToExtract =  20;
910                b.useDataDescriptor(true);
911            }
912            // CheckStyle:MagicNumber ON
913    
914            // version needed to extract
915            writeOut(ZipShort.getBytes(versionNeededToExtract));
916            // general purpose bit flag
917            writeOut(b.encode());
918        }
919    
920        /**
921         * enum that represents the possible policies for creating Unicode
922         * extra fields.
923         */
924        public static final class UnicodeExtraFieldPolicy {
925            /**
926             * Always create Unicode extra fields.
927             */
928            public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always");
929            /**
930             * Never create Unicode extra fields.
931             */
932            public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never");
933            /**
934             * Create Unicode extra fields for filenames that cannot be
935             * encoded using the specified encoding.
936             */
937            public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE =
938                new UnicodeExtraFieldPolicy("not encodeable");
939    
940            private final String name;
941            private UnicodeExtraFieldPolicy(String n) {
942                name = n;
943            }
944            public String toString() {
945                return name;
946            }
947        }
948    
949        /**
950         * Creates a new zip entry taking some information from the given
951         * file and using the provided name.
952         *
953         * <p>The name will be adjusted to end with a forward slash "/" if
954         * the file is a directory.  If the file is not a directory a
955         * potential trailing forward slash will be stripped from the
956         * entry name.</p>
957         *
958         * <p>Must not be used if the stream has already been closed.</p>
959         */
960        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
961                throws IOException {
962            if (finished) {
963                throw new IOException("Stream has already been finished");
964            }
965            return new ZipArchiveEntry(inputFile, entryName);
966        }
967    }