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) {
532                    if (!def.finished()) {
533                        if (length <= DEFLATER_BLOCK_SIZE) {
534                            def.setInput(b, offset, length);
535                            deflateUntilInputIsNeeded();
536                        } else {
537                            final int fullblocks = length / DEFLATER_BLOCK_SIZE;
538                            for (int i = 0; i < fullblocks; i++) {
539                                def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE,
540                                             DEFLATER_BLOCK_SIZE);
541                                deflateUntilInputIsNeeded();
542                            }
543                            final int done = fullblocks * DEFLATER_BLOCK_SIZE;
544                            if (done < length) {
545                                def.setInput(b, offset + done, length - done);
546                                deflateUntilInputIsNeeded();
547                            }
548                        }
549                    }
550                }
551            } else {
552                writeOut(b, offset, length);
553                written += length;
554            }
555            crc.update(b, offset, length);
556            count(length);
557        }
558    
559        /**
560         * Closes this output stream and releases any system resources
561         * associated with the stream.
562         *
563         * @exception  IOException  if an I/O error occurs.
564         */
565        public void close() throws IOException {
566            if (!finished) {
567                finish();
568            }
569    
570            if (raf != null) {
571                raf.close();
572            }
573            if (out != null) {
574                out.close();
575            }
576        }
577    
578        /**
579         * Flushes this output stream and forces any buffered output bytes
580         * to be written out to the stream.
581         *
582         * @exception  IOException  if an I/O error occurs.
583         */
584        public void flush() throws IOException {
585            if (out != null) {
586                out.flush();
587            }
588        }
589    
590        /*
591         * Various ZIP constants
592         */
593        /**
594         * local file header signature
595         */
596        static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes();
597        /**
598         * data descriptor signature
599         */
600        static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes();
601        /**
602         * central file header signature
603         */
604        static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes();
605        /**
606         * end of central dir signature
607         */
608        static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
609    
610        /**
611         * Writes next block of compressed data to the output stream.
612         * @throws IOException on error
613         */
614        protected final void deflate() throws IOException {
615            int len = def.deflate(buf, 0, buf.length);
616            if (len > 0) {
617                writeOut(buf, 0, len);
618            }
619        }
620    
621        /**
622         * Writes the local file header entry
623         * @param ze the entry to write
624         * @throws IOException on error
625         */
626        protected void writeLocalFileHeader(ZipArchiveEntry ze) throws IOException {
627    
628            boolean encodable = zipEncoding.canEncode(ze.getName());
629    
630            final ZipEncoding entryEncoding;
631    
632            if (!encodable && fallbackToUTF8) {
633                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
634            } else {
635                entryEncoding = zipEncoding;
636            }
637    
638            ByteBuffer name = entryEncoding.encode(ze.getName());
639    
640            if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
641    
642                if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
643                    || !encodable) {
644                    ze.addExtraField(new UnicodePathExtraField(ze.getName(),
645                                                               name.array(),
646                                                               name.arrayOffset(),
647                                                               name.limit()));
648                }
649    
650                String comm = ze.getComment();
651                if (comm != null && !"".equals(comm)) {
652    
653                    boolean commentEncodable = this.zipEncoding.canEncode(comm);
654    
655                    if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
656                        || !commentEncodable) {
657                        ByteBuffer commentB = entryEncoding.encode(comm);
658                        ze.addExtraField(new UnicodeCommentExtraField(comm,
659                                                                      commentB.array(),
660                                                                      commentB.arrayOffset(),
661                                                                      commentB.limit())
662                                         );
663                    }
664                }
665            }
666    
667            offsets.put(ze, ZipLong.getBytes(written));
668    
669            writeOut(LFH_SIG);
670            written += WORD;
671    
672            //store method in local variable to prevent multiple method calls
673            final int zipMethod = ze.getMethod();
674    
675            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
676                                                             !encodable
677                                                             && fallbackToUTF8);
678            written += WORD;
679    
680            // compression method
681            writeOut(ZipShort.getBytes(zipMethod));
682            written += SHORT;
683    
684            // last mod. time and date
685            writeOut(ZipUtil.toDosTime(ze.getTime()));
686            written += WORD;
687    
688            // CRC
689            // compressed length
690            // uncompressed length
691            localDataStart = written;
692            if (zipMethod == DEFLATED || raf != null) {
693                writeOut(LZERO);
694                writeOut(LZERO);
695                writeOut(LZERO);
696            } else {
697                writeOut(ZipLong.getBytes(ze.getCrc()));
698                writeOut(ZipLong.getBytes(ze.getSize()));
699                writeOut(ZipLong.getBytes(ze.getSize()));
700            }
701            // CheckStyle:MagicNumber OFF
702            written += 12;
703            // CheckStyle:MagicNumber ON
704    
705            // file name length
706            writeOut(ZipShort.getBytes(name.limit()));
707            written += SHORT;
708    
709            // extra field length
710            byte[] extra = ze.getLocalFileDataExtra();
711            writeOut(ZipShort.getBytes(extra.length));
712            written += SHORT;
713    
714            // file name
715            writeOut(name.array(), name.arrayOffset(), name.limit());
716            written += name.limit();
717    
718            // extra field
719            writeOut(extra);
720            written += extra.length;
721    
722            dataStart = written;
723        }
724    
725        /**
726         * Writes the data descriptor entry.
727         * @param ze the entry to write
728         * @throws IOException on error
729         */
730        protected void writeDataDescriptor(ZipArchiveEntry ze) throws IOException {
731            if (ze.getMethod() != DEFLATED || raf != null) {
732                return;
733            }
734            writeOut(DD_SIG);
735            writeOut(ZipLong.getBytes(entry.getCrc()));
736            writeOut(ZipLong.getBytes(entry.getCompressedSize()));
737            writeOut(ZipLong.getBytes(entry.getSize()));
738            // CheckStyle:MagicNumber OFF
739            written += 16;
740            // CheckStyle:MagicNumber ON
741        }
742    
743        /**
744         * Writes the central file header entry.
745         * @param ze the entry to write
746         * @throws IOException on error
747         */
748        protected void writeCentralFileHeader(ZipArchiveEntry ze) throws IOException {
749            writeOut(CFH_SIG);
750            written += WORD;
751    
752            // version made by
753            // CheckStyle:MagicNumber OFF
754            writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
755            written += SHORT;
756    
757            final int zipMethod = ze.getMethod();
758            final boolean encodable = zipEncoding.canEncode(ze.getName());
759            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
760                                                             !encodable
761                                                             && fallbackToUTF8);
762            written += WORD;
763    
764            // compression method
765            writeOut(ZipShort.getBytes(zipMethod));
766            written += SHORT;
767    
768            // last mod. time and date
769            writeOut(ZipUtil.toDosTime(ze.getTime()));
770            written += WORD;
771    
772            // CRC
773            // compressed length
774            // uncompressed length
775            writeOut(ZipLong.getBytes(ze.getCrc()));
776            writeOut(ZipLong.getBytes(ze.getCompressedSize()));
777            writeOut(ZipLong.getBytes(ze.getSize()));
778            // CheckStyle:MagicNumber OFF
779            written += 12;
780            // CheckStyle:MagicNumber ON
781    
782            // file name length
783            final ZipEncoding entryEncoding;
784    
785            if (!encodable && fallbackToUTF8) {
786                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
787            } else {
788                entryEncoding = zipEncoding;
789            }
790    
791            ByteBuffer name = entryEncoding.encode(ze.getName());
792    
793            writeOut(ZipShort.getBytes(name.limit()));
794            written += SHORT;
795    
796            // extra field length
797            byte[] extra = ze.getCentralDirectoryExtra();
798            writeOut(ZipShort.getBytes(extra.length));
799            written += SHORT;
800    
801            // file comment length
802            String comm = ze.getComment();
803            if (comm == null) {
804                comm = "";
805            }
806    
807            ByteBuffer commentB = entryEncoding.encode(comm);
808    
809            writeOut(ZipShort.getBytes(commentB.limit()));
810            written += SHORT;
811    
812            // disk number start
813            writeOut(ZERO);
814            written += SHORT;
815    
816            // internal file attributes
817            writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
818            written += SHORT;
819    
820            // external file attributes
821            writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
822            written += WORD;
823    
824            // relative offset of LFH
825            writeOut((byte[]) offsets.get(ze));
826            written += WORD;
827    
828            // file name
829            writeOut(name.array(), name.arrayOffset(), name.limit());
830            written += name.limit();
831    
832            // extra field
833            writeOut(extra);
834            written += extra.length;
835    
836            // file comment
837            writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit());
838            written += commentB.limit();
839        }
840    
841        /**
842         * Writes the &quot;End of central dir record&quot;.
843         * @throws IOException on error
844         */
845        protected void writeCentralDirectoryEnd() throws IOException {
846            writeOut(EOCD_SIG);
847    
848            // disk numbers
849            writeOut(ZERO);
850            writeOut(ZERO);
851    
852            // number of entries
853            byte[] num = ZipShort.getBytes(entries.size());
854            writeOut(num);
855            writeOut(num);
856    
857            // length and location of CD
858            writeOut(ZipLong.getBytes(cdLength));
859            writeOut(ZipLong.getBytes(cdOffset));
860    
861            // ZIP file comment
862            ByteBuffer data = this.zipEncoding.encode(comment);
863            writeOut(ZipShort.getBytes(data.limit()));
864            writeOut(data.array(), data.arrayOffset(), data.limit());
865        }
866    
867        /**
868         * Write bytes to output or random access file.
869         * @param data the byte array to write
870         * @throws IOException on error
871         */
872        protected final void writeOut(byte[] data) throws IOException {
873            writeOut(data, 0, data.length);
874        }
875    
876        /**
877         * Write bytes to output or random access file.
878         * @param data the byte array to write
879         * @param offset the start position to write from
880         * @param length the number of bytes to write
881         * @throws IOException on error
882         */
883        protected final void writeOut(byte[] data, int offset, int length)
884            throws IOException {
885            if (raf != null) {
886                raf.write(data, offset, length);
887            } else {
888                out.write(data, offset, length);
889            }
890        }
891    
892        private void deflateUntilInputIsNeeded() throws IOException {
893            while (!def.needsInput()) {
894                deflate();
895            }
896        }
897    
898        private void writeVersionNeededToExtractAndGeneralPurposeBits(final int
899                                                                      zipMethod,
900                                                                      final boolean
901                                                                      utfFallback)
902            throws IOException {
903    
904            // CheckStyle:MagicNumber OFF
905            int versionNeededToExtract = 10;
906            GeneralPurposeBit b = new GeneralPurposeBit();
907            b.useUTF8ForNames(useUTF8Flag || utfFallback);
908            if (zipMethod == DEFLATED && raf == null) {
909                // requires version 2 as we are going to store length info
910                // in the data descriptor
911                versionNeededToExtract =  20;
912                b.useDataDescriptor(true);
913            }
914            // CheckStyle:MagicNumber ON
915    
916            // version needed to extract
917            writeOut(ZipShort.getBytes(versionNeededToExtract));
918            // general purpose bit flag
919            writeOut(b.encode());
920        }
921    
922        /**
923         * enum that represents the possible policies for creating Unicode
924         * extra fields.
925         */
926        public static final class UnicodeExtraFieldPolicy {
927            /**
928             * Always create Unicode extra fields.
929             */
930            public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always");
931            /**
932             * Never create Unicode extra fields.
933             */
934            public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never");
935            /**
936             * Create Unicode extra fields for filenames that cannot be
937             * encoded using the specified encoding.
938             */
939            public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE =
940                new UnicodeExtraFieldPolicy("not encodeable");
941    
942            private final String name;
943            private UnicodeExtraFieldPolicy(String n) {
944                name = n;
945            }
946            public String toString() {
947                return name;
948            }
949        }
950    
951        /**
952         * Creates a new zip entry taking some information from the given
953         * file and using the provided name.
954         *
955         * <p>The name will be adjusted to end with a forward slash "/" if
956         * the file is a directory.  If the file is not a directory a
957         * potential trailing forward slash will be stripped from the
958         * entry name.</p>
959         *
960         * <p>Must not be used if the stream has already been closed.</p>
961         */
962        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
963                throws IOException {
964            if (finished) {
965                throw new IOException("Stream has already been finished");
966            }
967            return new ZipArchiveEntry(inputFile, entryName);
968        }
969    }