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