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 "End of central dir record".
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 }