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         */
101        public static final int EFS_FLAG = 1 << 11;
102    
103        /**
104         * Current entry.
105         */
106        private ZipArchiveEntry entry;
107    
108        /**
109         * The file comment.
110         */
111        private String comment = "";
112    
113        /**
114         * Compression level for next entry.
115         */
116        private int level = DEFAULT_COMPRESSION;
117    
118        /**
119         * Has the compression level changed when compared to the last
120         * entry?
121         */
122        private boolean hasCompressionLevelChanged = false;
123    
124        /**
125         * Default compression method for next entry.
126         */
127        private int method = java.util.zip.ZipEntry.DEFLATED;
128    
129        /**
130         * List of ZipArchiveEntries written so far.
131         */
132        private final List entries = new LinkedList();
133    
134        /**
135         * CRC instance to avoid parsing DEFLATED data twice.
136         */
137        private final CRC32 crc = new CRC32();
138    
139        /**
140         * Count the bytes written to out.
141         */
142        private long written = 0;
143    
144        /**
145         * Data for local header data
146         */
147        private long dataStart = 0;
148    
149        /**
150         * Offset for CRC entry in the local file header data for the
151         * current entry starts here.
152         */
153        private long localDataStart = 0;
154    
155        /**
156         * Start of central directory.
157         */
158        private long cdOffset = 0;
159    
160        /**
161         * Length of central directory.
162         */
163        private long cdLength = 0;
164    
165        /**
166         * Helper, a 0 as ZipShort.
167         */
168        private static final byte[] ZERO = {0, 0};
169    
170        /**
171         * Helper, a 0 as ZipLong.
172         */
173        private static final byte[] LZERO = {0, 0, 0, 0};
174    
175        /**
176         * Holds the offsets of the LFH starts for each entry.
177         */
178        private final Map offsets = new HashMap();
179    
180        /**
181         * The encoding to use for filenames and the file comment.
182         *
183         * <p>For a list of possible values see <a
184         * 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>.
185         * Defaults to UTF-8.</p>
186         */
187        private String encoding = DEFAULT_ENCODING;
188    
189        /**
190         * The zip encoding to use for filenames and the file comment.
191         *
192         * This field is of internal use and will be set in {@link
193         * #setEncoding(String)}.
194         */
195        private ZipEncoding zipEncoding =
196            ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING);
197    
198        /**
199         * This Deflater object is used for output.
200         *
201         */
202        protected final Deflater def = new Deflater(level, true);
203    
204        /**
205         * This buffer servers as a Deflater.
206         *
207         */
208        private final byte[] buf = new byte[BUFFER_SIZE];
209    
210        /**
211         * Optional random access output.
212         */
213        private final RandomAccessFile raf;
214    
215        private final OutputStream out;
216    
217        /**
218         * whether to use the EFS flag when writing UTF-8 filenames or not.
219         */
220        private boolean useEFS = true; 
221    
222        /**
223         * Whether to encode non-encodable file names as UTF-8.
224         */
225        private boolean fallbackToUTF8 = false;
226    
227        /**
228         * whether to create UnicodePathExtraField-s for each entry.
229         */
230        private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;
231    
232        /**
233         * Creates a new ZIP OutputStream filtering the underlying stream.
234         * @param out the outputstream to zip
235         */
236        public ZipArchiveOutputStream(OutputStream out) {
237            this.out = out;
238            this.raf = null;
239        }
240    
241        /**
242         * Creates a new ZIP OutputStream writing to a File.  Will use
243         * random access if possible.
244         * @param file the file to zip to
245         * @throws IOException on error
246         */
247        public ZipArchiveOutputStream(File file) throws IOException {
248            OutputStream o = null;
249            RandomAccessFile _raf = null;
250            try {
251                _raf = new RandomAccessFile(file, "rw");
252                _raf.setLength(0);
253            } catch (IOException e) {
254                if (_raf != null) {
255                    try {
256                        _raf.close();
257                    } catch (IOException inner) {
258                        // ignore
259                    }
260                    _raf = null;
261                }
262                o = new FileOutputStream(file);
263            }
264            out = o;
265            raf = _raf;
266        }
267    
268        /**
269         * This method indicates whether this archive is writing to a
270         * seekable stream (i.e., to a random access file).
271         *
272         * <p>For seekable streams, you don't need to calculate the CRC or
273         * uncompressed size for {@link #STORED} entries before
274         * invoking {@link #putArchiveEntry(ArchiveEntry)}.
275         * @return true if seekable
276         */
277        public boolean isSeekable() {
278            return raf != null;
279        }
280    
281        /**
282         * The encoding to use for filenames and the file comment.
283         *
284         * <p>For a list of possible values see <a
285         * 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>.
286         * Defaults to UTF-8.</p>
287         * @param encoding the encoding to use for file names, use null
288         * for the platform's default encoding
289         */
290        public void setEncoding(final String encoding) {
291            this.encoding = encoding;
292            this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
293            useEFS &= ZipEncodingHelper.isUTF8(encoding);
294        }
295    
296        /**
297         * The encoding to use for filenames and the file comment.
298         *
299         * @return null if using the platform's default character encoding.
300         */
301        public String getEncoding() {
302            return encoding;
303        }
304    
305        /**
306         * Whether to set the language encoding flag if the file name
307         * encoding is UTF-8.
308         *
309         * <p>Defaults to true.</p>
310         */
311        public void setUseLanguageEncodingFlag(boolean b) {
312            useEFS = b && ZipEncodingHelper.isUTF8(encoding);
313        }
314    
315        /**
316         * Whether to create Unicode Extra Fields.
317         *
318         * <p>Defaults to NEVER.</p>
319         */
320        public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) {
321            createUnicodeExtraFields = b;
322        }
323    
324        /**
325         * Whether to fall back to UTF and the language encoding flag if
326         * the file name cannot be encoded using the specified encoding.
327         *
328         * <p>Defaults to false.</p>
329         */
330        public void setFallbackToUTF8(boolean b) {
331            fallbackToUTF8 = b;
332        }
333    
334        /* (non-Javadoc)
335         * @see org.apache.commons.compress.archivers.ArchiveOutputStream#finish()
336         */
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        /** {@inheritDoc} */
427     // @throws ClassCastException if entry is not an instance of ZipArchiveEntry
428        public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException {
429            if(finished) {
430                throw new IOException("Stream has already been finished");
431            }
432            
433            if (entry != null) {
434                closeArchiveEntry();
435            }
436    
437            entry = ((ZipArchiveEntry) archiveEntry);
438            entries.add(entry);
439    
440            if (entry.getMethod() == -1) { // not specified
441                entry.setMethod(method);
442            }
443    
444            if (entry.getTime() == -1) { // not specified
445                entry.setTime(System.currentTimeMillis());
446            }
447    
448            // Size/CRC not required if RandomAccessFile is used
449            if (entry.getMethod() == STORED && raf == null) {
450                if (entry.getSize() == -1) {
451                    throw new ZipException("uncompressed size is required for"
452                                           + " STORED method when not writing to a"
453                                           + " file");
454                }
455                if (entry.getCrc() == -1) {
456                    throw new ZipException("crc checksum is required for STORED"
457                                           + " method when not writing to a file");
458                }
459                entry.setCompressedSize(entry.getSize());
460            }
461    
462            if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
463                def.setLevel(level);
464                hasCompressionLevelChanged = false;
465            }
466            writeLocalFileHeader(entry);
467        }
468    
469        /**
470         * Set the file comment.
471         * @param comment the comment
472         */
473        public void setComment(String comment) {
474            this.comment = comment;
475        }
476    
477        /**
478         * Sets the compression level for subsequent entries.
479         *
480         * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
481         * @param level the compression level.
482         * @throws IllegalArgumentException if an invalid compression
483         * level is specified.
484         */
485        public void setLevel(int level) {
486            if (level < Deflater.DEFAULT_COMPRESSION
487                || level > Deflater.BEST_COMPRESSION) {
488                throw new IllegalArgumentException("Invalid compression level: "
489                                                   + level);
490            }
491            hasCompressionLevelChanged = (this.level != level);
492            this.level = level;
493        }
494    
495        /**
496         * Sets the default compression method for subsequent entries.
497         *
498         * <p>Default is DEFLATED.</p>
499         * @param method an <code>int</code> from java.util.zip.ZipEntry
500         */
501        public void setMethod(int method) {
502            this.method = method;
503        }
504    
505        /**
506         * Writes bytes to ZIP entry.
507         * @param b the byte array to write
508         * @param offset the start position to write from
509         * @param length the number of bytes to write
510         * @throws IOException on error
511         */
512        public void write(byte[] b, int offset, int length) throws IOException {
513            if (entry.getMethod() == DEFLATED) {
514                if (length > 0) {
515                    if (!def.finished()) {
516                        if (length <= DEFLATER_BLOCK_SIZE) {
517                            def.setInput(b, offset, length);
518                            deflateUntilInputIsNeeded();
519                        } else {
520                            final int fullblocks = length / DEFLATER_BLOCK_SIZE;
521                            for (int i = 0; i < fullblocks; i++) {
522                                def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE,
523                                             DEFLATER_BLOCK_SIZE);
524                                deflateUntilInputIsNeeded();
525                            }
526                            final int done = fullblocks * DEFLATER_BLOCK_SIZE;
527                            if (done < length) {
528                                def.setInput(b, offset + done, length - done);
529                                deflateUntilInputIsNeeded();
530                            }
531                        }
532                    }
533                }
534            } else {
535                writeOut(b, offset, length);
536                written += length;
537            }
538            crc.update(b, offset, length);
539            count(length);
540        }
541    
542        /**
543         * Closes this output stream and releases any system resources
544         * associated with the stream.
545         *
546         * @exception  IOException  if an I/O error occurs.
547         */
548        public void close() throws IOException {
549            if(!finished) {
550                finish();
551            }
552            
553            if (raf != null) {
554                raf.close();
555            }
556            if (out != null) {
557                out.close();
558            }
559        }
560    
561        /**
562         * Flushes this output stream and forces any buffered output bytes
563         * to be written out to the stream.
564         *
565         * @exception  IOException  if an I/O error occurs.
566         */
567        public void flush() throws IOException {
568            if (out != null) {
569                out.flush();
570            }
571        }
572    
573        /*
574         * Various ZIP constants
575         */
576        /**
577         * local file header signature
578         */
579        static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes();
580        /**
581         * data descriptor signature
582         */
583        static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
584        /**
585         * central file header signature
586         */
587        static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes();
588        /**
589         * end of central dir signature
590         */
591        static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
592    
593        /**
594         * Writes next block of compressed data to the output stream.
595         * @throws IOException on error
596         */
597        protected final void deflate() throws IOException {
598            int len = def.deflate(buf, 0, buf.length);
599            if (len > 0) {
600                writeOut(buf, 0, len);
601            }
602        }
603    
604        /**
605         * Writes the local file header entry
606         * @param ze the entry to write
607         * @throws IOException on error
608         */
609        protected void writeLocalFileHeader(ZipArchiveEntry ze) throws IOException {
610    
611            boolean encodable = zipEncoding.canEncode(ze.getName());
612            
613            final ZipEncoding entryEncoding;
614            
615            if (!encodable && fallbackToUTF8) {
616                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
617            } else {
618                entryEncoding = zipEncoding;
619            }
620            
621            ByteBuffer name = entryEncoding.encode(ze.getName());        
622    
623            if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
624    
625                if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
626                    || !encodable) {
627                    ze.addExtraField(new UnicodePathExtraField(ze.getName(),
628                                                               name.array(),
629                                                               name.arrayOffset(),
630                                                               name.limit()));
631                }
632    
633                String comm = ze.getComment();
634                if (comm != null && !"".equals(comm)) {
635    
636                    boolean commentEncodable = this.zipEncoding.canEncode(comm);
637    
638                    if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
639                        || !commentEncodable) {
640                        ByteBuffer commentB = entryEncoding.encode(comm);
641                        ze.addExtraField(new UnicodeCommentExtraField(comm,
642                                                                      commentB.array(),
643                                                                      commentB.arrayOffset(),
644                                                                      commentB.limit())
645                                         );
646                    }
647                }
648            }
649    
650            offsets.put(ze, ZipLong.getBytes(written));
651    
652            writeOut(LFH_SIG);
653            written += WORD;
654    
655            //store method in local variable to prevent multiple method calls
656            final int zipMethod = ze.getMethod();
657    
658            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
659                                                             !encodable
660                                                             && fallbackToUTF8);
661            written += WORD;
662    
663            // compression method
664            writeOut(ZipShort.getBytes(zipMethod));
665            written += SHORT;
666    
667            // last mod. time and date
668            writeOut(ZipUtil.toDosTime(ze.getTime()));
669            written += WORD;
670    
671            // CRC
672            // compressed length
673            // uncompressed length
674            localDataStart = written;
675            if (zipMethod == DEFLATED || raf != null) {
676                writeOut(LZERO);
677                writeOut(LZERO);
678                writeOut(LZERO);
679            } else {
680                writeOut(ZipLong.getBytes(ze.getCrc()));
681                writeOut(ZipLong.getBytes(ze.getSize()));
682                writeOut(ZipLong.getBytes(ze.getSize()));
683            }
684            // CheckStyle:MagicNumber OFF
685            written += 12;
686            // CheckStyle:MagicNumber ON
687    
688            // file name length
689            writeOut(ZipShort.getBytes(name.limit()));
690            written += SHORT;
691    
692            // extra field length
693            byte[] extra = ze.getLocalFileDataExtra();
694            writeOut(ZipShort.getBytes(extra.length));
695            written += SHORT;
696    
697            // file name
698            writeOut(name.array(), name.arrayOffset(), name.limit());
699            written += name.limit();
700    
701            // extra field
702            writeOut(extra);
703            written += extra.length;
704    
705            dataStart = written;
706        }
707    
708        /**
709         * Writes the data descriptor entry.
710         * @param ze the entry to write
711         * @throws IOException on error
712         */
713        protected void writeDataDescriptor(ZipArchiveEntry ze) throws IOException {
714            if (ze.getMethod() != DEFLATED || raf != null) {
715                return;
716            }
717            writeOut(DD_SIG);
718            writeOut(ZipLong.getBytes(entry.getCrc()));
719            writeOut(ZipLong.getBytes(entry.getCompressedSize()));
720            writeOut(ZipLong.getBytes(entry.getSize()));
721            // CheckStyle:MagicNumber OFF
722            written += 16;
723            // CheckStyle:MagicNumber ON
724        }
725    
726        /**
727         * Writes the central file header entry.
728         * @param ze the entry to write
729         * @throws IOException on error
730         */
731        protected void writeCentralFileHeader(ZipArchiveEntry ze) throws IOException {
732            writeOut(CFH_SIG);
733            written += WORD;
734    
735            // version made by
736            // CheckStyle:MagicNumber OFF
737            writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
738            written += SHORT;
739    
740            final int zipMethod = ze.getMethod();
741            final boolean encodable = zipEncoding.canEncode(ze.getName());
742            writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
743                                                             !encodable
744                                                             && fallbackToUTF8);
745            written += WORD;
746    
747            // compression method
748            writeOut(ZipShort.getBytes(zipMethod));
749            written += SHORT;
750    
751            // last mod. time and date
752            writeOut(ZipUtil.toDosTime(ze.getTime()));
753            written += WORD;
754    
755            // CRC
756            // compressed length
757            // uncompressed length
758            writeOut(ZipLong.getBytes(ze.getCrc()));
759            writeOut(ZipLong.getBytes(ze.getCompressedSize()));
760            writeOut(ZipLong.getBytes(ze.getSize()));
761            // CheckStyle:MagicNumber OFF
762            written += 12;
763            // CheckStyle:MagicNumber ON
764    
765            // file name length
766            final ZipEncoding entryEncoding;
767            
768            if (!encodable && fallbackToUTF8) {
769                entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
770            } else {
771                entryEncoding = zipEncoding;
772            }
773            
774            ByteBuffer name = entryEncoding.encode(ze.getName());        
775    
776            writeOut(ZipShort.getBytes(name.limit()));
777            written += SHORT;
778    
779            // extra field length
780            byte[] extra = ze.getCentralDirectoryExtra();
781            writeOut(ZipShort.getBytes(extra.length));
782            written += SHORT;
783    
784            // file comment length
785            String comm = ze.getComment();
786            if (comm == null) {
787                comm = "";
788            }
789            
790            ByteBuffer commentB = entryEncoding.encode(comm);
791            
792            writeOut(ZipShort.getBytes(commentB.limit()));
793            written += SHORT;
794    
795            // disk number start
796            writeOut(ZERO);
797            written += SHORT;
798    
799            // internal file attributes
800            writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
801            written += SHORT;
802    
803            // external file attributes
804            writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
805            written += WORD;
806    
807            // relative offset of LFH
808            writeOut((byte[]) offsets.get(ze));
809            written += WORD;
810    
811            // file name
812            writeOut(name.array(), name.arrayOffset(), name.limit());
813            written += name.limit();
814    
815            // extra field
816            writeOut(extra);
817            written += extra.length;
818    
819            // file comment
820            writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit());
821            written += commentB.limit();
822        }
823    
824        /**
825         * Writes the &quot;End of central dir record&quot;.
826         * @throws IOException on error
827         */
828        protected void writeCentralDirectoryEnd() throws IOException {
829            writeOut(EOCD_SIG);
830    
831            // disk numbers
832            writeOut(ZERO);
833            writeOut(ZERO);
834    
835            // number of entries
836            byte[] num = ZipShort.getBytes(entries.size());
837            writeOut(num);
838            writeOut(num);
839    
840            // length and location of CD
841            writeOut(ZipLong.getBytes(cdLength));
842            writeOut(ZipLong.getBytes(cdOffset));
843    
844            // ZIP file comment
845            ByteBuffer data = this.zipEncoding.encode(comment);
846            writeOut(ZipShort.getBytes(data.limit()));
847            writeOut(data.array(), data.arrayOffset(), data.limit());
848        }
849    
850        /**
851         * Write bytes to output or random access file.
852         * @param data the byte array to write
853         * @throws IOException on error
854         */
855        protected final void writeOut(byte[] data) throws IOException {
856            writeOut(data, 0, data.length);
857        }
858    
859        /**
860         * Write bytes to output or random access file.
861         * @param data the byte array to write
862         * @param offset the start position to write from
863         * @param length the number of bytes to write
864         * @throws IOException on error
865         */
866        protected final void writeOut(byte[] data, int offset, int length)
867            throws IOException {
868            if (raf != null) {
869                raf.write(data, offset, length);
870            } else {
871                out.write(data, offset, length);
872            }
873        }
874    
875        private void deflateUntilInputIsNeeded() throws IOException {
876            while (!def.needsInput()) {
877                deflate();
878            }
879        }
880    
881        private void writeVersionNeededToExtractAndGeneralPurposeBits(final int
882                                                                      zipMethod,
883                                                                      final boolean
884                                                                      utfFallback)
885            throws IOException {
886    
887            // CheckStyle:MagicNumber OFF
888            int versionNeededToExtract = 10;
889            int generalPurposeFlag = (useEFS || utfFallback) ? EFS_FLAG : 0;
890            if (zipMethod == DEFLATED && raf == null) {
891                // requires version 2 as we are going to store length info
892                // in the data descriptor
893                versionNeededToExtract =  20;
894                // bit3 set to signal, we use a data descriptor
895                generalPurposeFlag |= 8;
896            }
897            // CheckStyle:MagicNumber ON
898    
899            // version needed to extract
900            writeOut(ZipShort.getBytes(versionNeededToExtract));
901            // general purpose bit flag
902            writeOut(ZipShort.getBytes(generalPurposeFlag));
903        }
904    
905        /**
906         * enum that represents the possible policies for creating Unicode
907         * extra fields.
908         */
909        public static final class UnicodeExtraFieldPolicy {
910            /**
911             * Always create Unicode extra fields.
912             */
913            public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always");
914            /**
915             * Never create Unicode extra fields.
916             */
917            public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never");
918            /**
919             * Create Unicode extra fields for filenames that cannot be
920             * encoded using the specified encoding.
921             */
922            public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE =
923                new UnicodeExtraFieldPolicy("not encodeable");
924    
925            private final String name;
926            private UnicodeExtraFieldPolicy(String n) {
927                name = n;
928            }
929            public String toString() {
930                return name;
931            }
932        }
933    
934        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
935                throws IOException {
936            if(finished) {
937                throw new IOException("Stream has already been finished");
938            }
939            return new ZipArchiveEntry(inputFile, entryName);
940        }
941    }