001: /*
002: * Copyright 2001-2004 The Apache Software Foundation
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: *
016: */
017:
018: package org.apache.tools.zip;
019:
020: import java.io.File;
021: import java.io.FileOutputStream;
022: import java.io.FilterOutputStream;
023: import java.io.IOException;
024: import java.io.OutputStream;
025: import java.io.RandomAccessFile;
026: import java.io.UnsupportedEncodingException;
027: import java.util.Date;
028: import java.util.Hashtable;
029: import java.util.Vector;
030: import java.util.zip.CRC32;
031: import java.util.zip.Deflater;
032: import java.util.zip.ZipException;
033:
034: /**
035: * Reimplementation of {@link java.util.zip.ZipOutputStream
036: * java.util.zip.ZipOutputStream} that does handle the extended
037: * functionality of this package, especially internal/external file
038: * attributes and extra fields with different layouts for local file
039: * data and central directory entries.
040: *
041: * <p>This class will try to use {@link java.io.RandomAccessFile
042: * RandomAccessFile} when you know that the output is going to go to a
043: * file.</p>
044: *
045: * <p>If RandomAccessFile cannot be used, this implementation will use
046: * a Data Descriptor to store size and CRC information for {@link
047: * #DEFLATED DEFLATED} entries, this means, you don't need to
048: * calculate them yourself. Unfortunately this is not possible for
049: * the {@link #STORED STORED} method, here setting the CRC and
050: * uncompressed size information is required before {@link
051: * #putNextEntry putNextEntry} can be called.</p>
052: *
053: * @author Stefan Bodewig
054: * @author Richard Evans
055: * @version $Revision: 1.17.2.3 $
056: */
057: public class ZipOutputStream extends FilterOutputStream {
058:
059: /**
060: * Current entry.
061: *
062: * @since 1.1
063: */
064: private ZipEntry entry;
065:
066: /**
067: * The file comment.
068: *
069: * @since 1.1
070: */
071: private String comment = "";
072:
073: /**
074: * Compression level for next entry.
075: *
076: * @since 1.1
077: */
078: private int level = Deflater.DEFAULT_COMPRESSION;
079:
080: /**
081: * Has the compression level changed when compared to the last
082: * entry?
083: *
084: * @since 1.5
085: */
086: private boolean hasCompressionLevelChanged = false;
087:
088: /**
089: * Default compression method for next entry.
090: *
091: * @since 1.1
092: */
093: private int method = DEFLATED;
094:
095: /**
096: * List of ZipEntries written so far.
097: *
098: * @since 1.1
099: */
100: private final Vector<ZipEntry> entries = new Vector<ZipEntry>();
101:
102: /**
103: * CRC instance to avoid parsing DEFLATED data twice.
104: *
105: * @since 1.1
106: */
107: private CRC32 crc = new CRC32();
108:
109: /**
110: * Count the bytes written to out.
111: *
112: * @since 1.1
113: */
114: private long written = 0;
115:
116: /**
117: * Data for local header data
118: *
119: * @since 1.1
120: */
121: private long dataStart = 0;
122:
123: /**
124: * Offset for CRC entry in the local file header data for the
125: * current entry starts here.
126: *
127: * @since 1.15
128: */
129: private long localDataStart = 0;
130:
131: /**
132: * Start of central directory.
133: *
134: * @since 1.1
135: */
136: private ZipLong cdOffset = new ZipLong(0);
137:
138: /**
139: * Length of central directory.
140: *
141: * @since 1.1
142: */
143: private ZipLong cdLength = new ZipLong(0);
144:
145: /**
146: * Helper, a 0 as ZipShort.
147: *
148: * @since 1.1
149: */
150: private static final byte[] ZERO = { 0, 0 };
151:
152: /**
153: * Helper, a 0 as ZipLong.
154: *
155: * @since 1.1
156: */
157: private static final byte[] LZERO = { 0, 0, 0, 0 };
158:
159: /**
160: * Holds the offsets of the LFH starts for each entry
161: *
162: * @since 1.1
163: */
164: private Hashtable<ZipEntry, ZipLong> offsets = new Hashtable<ZipEntry, ZipLong>();
165:
166: /**
167: * The encoding to use for filenames and the file comment.
168: *
169: * <p>For a list of possible values see <a
170: * href="http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html">http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html</a>.
171: * Defaults to the platform's default character encoding.</p>
172: *
173: * @since 1.3
174: */
175: private String encoding = null;
176:
177: /**
178: * Deflater object for output
179: *
180: * <p>This attribute is only protected to provide a level of API
181: * backwards compatibility. This class used to extend {@link
182: * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
183: * Revision 1.13.</p>
184: *
185: * @since 1.14
186: */
187: protected Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION,
188: true);
189:
190: /**
191: * Deflater buffer
192: *
193: * <p>This attribute is only protected to provide a level of API
194: * backwards compatibility. This class used to extend {@link
195: * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
196: * Revision 1.13.</p>
197: *
198: * @since 1.14
199: */
200: protected byte[] buf = new byte[512];
201:
202: /**
203: * Optional random access output
204: *
205: * @since 1.14
206: */
207: private RandomAccessFile raf = null;
208:
209: /**
210: * Compression method for deflated entries.
211: *
212: * @since 1.1
213: */
214: public static final int DEFLATED = ZipEntry.DEFLATED;
215:
216: /**
217: * Compression method for deflated entries.
218: *
219: * @since 1.1
220: */
221: public static final int STORED = ZipEntry.STORED;
222:
223: /**
224: * Creates a new ZIP OutputStream filtering the underlying stream.
225: *
226: * @since 1.1
227: */
228: public ZipOutputStream(OutputStream out) {
229: super (out);
230: }
231:
232: /**
233: * Creates a new ZIP OutputStream writing to a File. Will use
234: * random access if possible.
235: *
236: * @since 1.14
237: */
238: public ZipOutputStream(File file) throws IOException {
239: super (null);
240:
241: try {
242: raf = new RandomAccessFile(file, "rw");
243: raf.setLength(0);
244: } catch (IOException e) {
245: if (raf != null) {
246: try {
247: raf.close();
248: } catch (IOException inner) {
249: // ignore
250: }
251: raf = null;
252: }
253: out = new FileOutputStream(file);
254: }
255: }
256:
257: /**
258: * Is this archive writing to a seekable stream (i.e. a random
259: * access file)?
260: *
261: * <p>For seekable streams, you don't need to calculate the CRC or
262: * uncompressed size for {@link #STORED STORED} entries before
263: * invoking {@link #putNextEntry putNextEntry}.
264: *
265: * @since 1.17
266: */
267: public boolean isSeekable() {
268: return raf != null;
269: }
270:
271: /**
272: * The encoding to use for filenames and the file comment.
273: *
274: * <p>For a list of possible values see <a
275: * href="http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html">http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html</a>.
276: * Defaults to the platform's default character encoding.</p>
277: *
278: * @since 1.3
279: */
280: public void setEncoding(String encoding) {
281: this .encoding = encoding;
282: }
283:
284: /**
285: * The encoding to use for filenames and the file comment.
286: *
287: * @return null if using the platform's default character encoding.
288: *
289: * @since 1.3
290: */
291: public String getEncoding() {
292: return encoding;
293: }
294:
295: /**
296: * Finishs writing the contents and closes this as well as the
297: * underlying stream.
298: *
299: * @since 1.1
300: */
301: public void finish() throws IOException {
302: closeEntry();
303: cdOffset = new ZipLong(written);
304: for (int i = 0; i < entries.size(); i++) {
305: writeCentralFileHeader(entries.elementAt(i));
306: }
307: cdLength = new ZipLong(written - cdOffset.getValue());
308: writeCentralDirectoryEnd();
309: offsets.clear();
310: entries.removeAllElements();
311: }
312:
313: /**
314: * Writes all necessary data for this entry.
315: *
316: * @since 1.1
317: */
318: public void closeEntry() throws IOException {
319: if (entry == null) {
320: return;
321: }
322:
323: long realCrc = crc.getValue();
324: crc.reset();
325:
326: if (entry.getMethod() == DEFLATED) {
327: def.finish();
328: while (!def.finished()) {
329: deflate();
330: }
331:
332: entry.setSize(def.getTotalIn());
333: entry.setComprSize(def.getTotalOut());
334: entry.setCrc(realCrc);
335:
336: def.reset();
337:
338: written += entry.getCompressedSize();
339: } else if (raf == null) {
340: if (entry.getCrc() != realCrc) {
341: throw new ZipException("bad CRC checksum for entry "
342: + entry.getName() + ": "
343: + Long.toHexString(entry.getCrc())
344: + " instead of " + Long.toHexString(realCrc));
345: }
346:
347: if (entry.getSize() != written - dataStart) {
348: throw new ZipException("bad size for entry "
349: + entry.getName() + ": " + entry.getSize()
350: + " instead of " + (written - dataStart));
351: }
352: } else { /* method is STORED and we used RandomAccessFile */
353: long size = written - dataStart;
354:
355: entry.setSize(size);
356: entry.setComprSize(size);
357: entry.setCrc(realCrc);
358: }
359:
360: // If random access output, write the local file header containing
361: // the correct CRC and compressed/uncompressed sizes
362: if (raf != null) {
363: long save = raf.getFilePointer();
364:
365: raf.seek(localDataStart);
366: writeOut((new ZipLong(entry.getCrc())).getBytes());
367: writeOut((new ZipLong(entry.getCompressedSize()))
368: .getBytes());
369: writeOut((new ZipLong(entry.getSize())).getBytes());
370: raf.seek(save);
371: }
372:
373: writeDataDescriptor(entry);
374: entry = null;
375: }
376:
377: /**
378: * Begin writing next entry.
379: *
380: * @since 1.1
381: */
382: public void putNextEntry(ZipEntry ze) throws IOException {
383: closeEntry();
384:
385: entry = ze;
386: entries.addElement(entry);
387:
388: if (entry.getMethod() == -1) { // not specified
389: entry.setMethod(method);
390: }
391:
392: if (entry.getTime() == -1) { // not specified
393: entry.setTime(System.currentTimeMillis());
394: }
395:
396: // Size/CRC not required if RandomAccessFile is used
397: if (entry.getMethod() == STORED && raf == null) {
398: if (entry.getSize() == -1) {
399: throw new ZipException(
400: "uncompressed size is required for"
401: + " STORED method when not writing to a"
402: + " file");
403: }
404: if (entry.getCrc() == -1) {
405: throw new ZipException(
406: "crc checksum is required for STORED"
407: + " method when not writing to a file");
408: }
409: entry.setComprSize(entry.getSize());
410: }
411:
412: if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
413: def.setLevel(level);
414: hasCompressionLevelChanged = false;
415: }
416: writeLocalFileHeader(entry);
417: }
418:
419: /**
420: * Set the file comment.
421: *
422: * @since 1.1
423: */
424: public void setComment(String comment) {
425: this .comment = comment;
426: }
427:
428: /**
429: * Sets the compression level for subsequent entries.
430: *
431: * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
432: *
433: * @since 1.1
434: */
435: public void setLevel(int level) {
436: hasCompressionLevelChanged = (this .level != level);
437: this .level = level;
438: }
439:
440: /**
441: * Sets the default compression method for subsequent entries.
442: *
443: * <p>Default is DEFLATED.</p>
444: *
445: * @since 1.1
446: */
447: public void setMethod(int method) {
448: this .method = method;
449: }
450:
451: /**
452: * Writes bytes to ZIP entry.
453: */
454: @Override
455: public void write(byte[] b, int offset, int length)
456: throws IOException {
457: if (entry.getMethod() == DEFLATED) {
458: if (length > 0) {
459: if (!def.finished()) {
460: def.setInput(b, offset, length);
461: while (!def.needsInput()) {
462: deflate();
463: }
464: }
465: }
466: } else {
467: writeOut(b, offset, length);
468: written += length;
469: }
470: crc.update(b, offset, length);
471: }
472:
473: /**
474: * Writes a single byte to ZIP entry.
475: *
476: * <p>Delegates to the three arg method.</p>
477: *
478: * @since 1.14
479: */
480: @Override
481: public void write(int b) throws IOException {
482: byte[] buf = new byte[1];
483: buf[0] = (byte) (b & 0xff);
484: write(buf, 0, 1);
485: }
486:
487: /**
488: * Closes this output stream and releases any system resources
489: * associated with the stream.
490: *
491: * @exception IOException if an I/O error occurs.
492: * @since 1.14
493: */
494: @Override
495: public void close() throws IOException {
496: finish();
497:
498: if (raf != null) {
499: raf.close();
500: }
501: if (out != null) {
502: out.close();
503: }
504: }
505:
506: /**
507: * Flushes this output stream and forces any buffered output bytes
508: * to be written out to the stream.
509: *
510: * @exception IOException if an I/O error occurs.
511: * @since 1.14
512: */
513: @Override
514: public void flush() throws IOException {
515: if (out == null) {
516: out.flush();
517: }
518: }
519:
520: /*
521: * Various ZIP constants
522: */
523: /**
524: * local file header signature
525: *
526: * @since 1.1
527: */
528: protected static final ZipLong LFH_SIG = new ZipLong(0X04034B50L);
529: /**
530: * data descriptor signature
531: *
532: * @since 1.1
533: */
534: protected static final ZipLong DD_SIG = new ZipLong(0X08074B50L);
535: /**
536: * central file header signature
537: *
538: * @since 1.1
539: */
540: protected static final ZipLong CFH_SIG = new ZipLong(0X02014B50L);
541: /**
542: * end of central dir signature
543: *
544: * @since 1.1
545: */
546: protected static final ZipLong EOCD_SIG = new ZipLong(0X06054B50L);
547:
548: /**
549: * Writes next block of compressed data to the output stream.
550: *
551: * @since 1.14
552: */
553: protected final void deflate() throws IOException {
554: int len = def.deflate(buf, 0, buf.length);
555: if (len > 0) {
556: writeOut(buf, 0, len);
557: }
558: }
559:
560: /**
561: * Writes the local file header entry
562: *
563: * @since 1.1
564: */
565: protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
566: offsets.put(ze, new ZipLong(written));
567:
568: writeOut(LFH_SIG.getBytes());
569: written += 4;
570:
571: // version needed to extract
572: // general purpose bit flag
573: if (ze.getMethod() == DEFLATED && raf == null) {
574: // requires version 2 as we are going to store length info
575: // in the data descriptor
576: writeOut((new ZipShort(20)).getBytes());
577:
578: // bit3 set to signal, we use a data descriptor
579: writeOut((new ZipShort(8)).getBytes());
580: } else {
581: writeOut((new ZipShort(10)).getBytes());
582: writeOut(ZERO);
583: }
584: written += 4;
585:
586: // compression method
587: writeOut((new ZipShort(ze.getMethod())).getBytes());
588: written += 2;
589:
590: // last mod. time and date
591: writeOut(toDosTime(new Date(ze.getTime())).getBytes());
592: written += 4;
593:
594: // CRC
595: // compressed length
596: // uncompressed length
597: localDataStart = written;
598: if (ze.getMethod() == DEFLATED || raf != null) {
599: writeOut(LZERO);
600: writeOut(LZERO);
601: writeOut(LZERO);
602: } else {
603: writeOut((new ZipLong(ze.getCrc())).getBytes());
604: writeOut((new ZipLong(ze.getSize())).getBytes());
605: writeOut((new ZipLong(ze.getSize())).getBytes());
606: }
607: written += 12;
608:
609: // file name length
610: byte[] name = getBytes(ze.getName());
611: writeOut((new ZipShort(name.length)).getBytes());
612: written += 2;
613:
614: // extra field length
615: byte[] extra = ze.getLocalFileDataExtra();
616: writeOut((new ZipShort(extra.length)).getBytes());
617: written += 2;
618:
619: // file name
620: writeOut(name);
621: written += name.length;
622:
623: // extra field
624: writeOut(extra);
625: written += extra.length;
626:
627: dataStart = written;
628: }
629:
630: /**
631: * Writes the data descriptor entry
632: *
633: * @since 1.1
634: */
635: protected void writeDataDescriptor(ZipEntry ze) throws IOException {
636: if (ze.getMethod() != DEFLATED || raf != null) {
637: return;
638: }
639: writeOut(DD_SIG.getBytes());
640: writeOut((new ZipLong(entry.getCrc())).getBytes());
641: writeOut((new ZipLong(entry.getCompressedSize())).getBytes());
642: writeOut((new ZipLong(entry.getSize())).getBytes());
643: written += 16;
644: }
645:
646: /**
647: * Writes the central file header entry
648: *
649: * @since 1.1
650: */
651: protected void writeCentralFileHeader(ZipEntry ze)
652: throws IOException {
653: writeOut(CFH_SIG.getBytes());
654: written += 4;
655:
656: // version made by
657: writeOut((new ZipShort((ze.getPlatform() << 8) | 20))
658: .getBytes());
659: written += 2;
660:
661: // version needed to extract
662: // general purpose bit flag
663: if (ze.getMethod() == DEFLATED && raf == null) {
664: // requires version 2 as we are going to store length info
665: // in the data descriptor
666: writeOut((new ZipShort(20)).getBytes());
667:
668: // bit3 set to signal, we use a data descriptor
669: writeOut((new ZipShort(8)).getBytes());
670: } else {
671: writeOut((new ZipShort(10)).getBytes());
672: writeOut(ZERO);
673: }
674: written += 4;
675:
676: // compression method
677: writeOut((new ZipShort(ze.getMethod())).getBytes());
678: written += 2;
679:
680: // last mod. time and date
681: writeOut(toDosTime(new Date(ze.getTime())).getBytes());
682: written += 4;
683:
684: // CRC
685: // compressed length
686: // uncompressed length
687: writeOut((new ZipLong(ze.getCrc())).getBytes());
688: writeOut((new ZipLong(ze.getCompressedSize())).getBytes());
689: writeOut((new ZipLong(ze.getSize())).getBytes());
690: written += 12;
691:
692: // file name length
693: byte[] name = getBytes(ze.getName());
694: writeOut((new ZipShort(name.length)).getBytes());
695: written += 2;
696:
697: // extra field length
698: byte[] extra = ze.getCentralDirectoryExtra();
699: writeOut((new ZipShort(extra.length)).getBytes());
700: written += 2;
701:
702: // file comment length
703: String comm = ze.getComment();
704: if (comm == null) {
705: comm = "";
706: }
707: byte[] comment = getBytes(comm);
708: writeOut((new ZipShort(comment.length)).getBytes());
709: written += 2;
710:
711: // disk number start
712: writeOut(ZERO);
713: written += 2;
714:
715: // internal file attributes
716: writeOut((new ZipShort(ze.getInternalAttributes())).getBytes());
717: written += 2;
718:
719: // external file attributes
720: writeOut((new ZipLong(ze.getExternalAttributes())).getBytes());
721: written += 4;
722:
723: // relative offset of LFH
724: writeOut(offsets.get(ze).getBytes());
725: written += 4;
726:
727: // file name
728: writeOut(name);
729: written += name.length;
730:
731: // extra field
732: writeOut(extra);
733: written += extra.length;
734:
735: // file comment
736: writeOut(comment);
737: written += comment.length;
738: }
739:
740: /**
741: * Writes the "End of central dir record"
742: *
743: * @since 1.1
744: */
745: protected void writeCentralDirectoryEnd() throws IOException {
746: writeOut(EOCD_SIG.getBytes());
747:
748: // disk numbers
749: writeOut(ZERO);
750: writeOut(ZERO);
751:
752: // number of entries
753: byte[] num = (new ZipShort(entries.size())).getBytes();
754: writeOut(num);
755: writeOut(num);
756:
757: // length and location of CD
758: writeOut(cdLength.getBytes());
759: writeOut(cdOffset.getBytes());
760:
761: // ZIP file comment
762: byte[] data = getBytes(comment);
763: writeOut((new ZipShort(data.length)).getBytes());
764: writeOut(data);
765: }
766:
767: /**
768: * Smallest date/time ZIP can handle.
769: *
770: * @since 1.1
771: */
772: private static final ZipLong DOS_TIME_MIN = new ZipLong(0x00002100L);
773:
774: /**
775: * Convert a Date object to a DOS date/time field.
776: *
777: * <p>Stolen from InfoZip's <code>fileio.c</code></p>
778: *
779: * @since 1.1
780: */
781: @SuppressWarnings("deprecation")
782: protected static ZipLong toDosTime(Date time) {
783: int year = time.getYear() + 1900;
784: int month = time.getMonth() + 1;
785: if (year < 1980) {
786: return DOS_TIME_MIN;
787: }
788: long value = ((year - 1980) << 25) | (month << 21)
789: | (time.getDate() << 16) | (time.getHours() << 11)
790: | (time.getMinutes() << 5) | (time.getSeconds() >> 1);
791:
792: byte[] result = new byte[4];
793: result[0] = (byte) ((value & 0xFF));
794: result[1] = (byte) ((value & 0xFF00) >> 8);
795: result[2] = (byte) ((value & 0xFF0000) >> 16);
796: result[3] = (byte) ((value & 0xFF000000L) >> 24);
797: return new ZipLong(result);
798: }
799:
800: /**
801: * Retrieve the bytes for the given String in the encoding set for
802: * this Stream.
803: *
804: * @since 1.3
805: */
806: protected byte[] getBytes(String name) throws ZipException {
807: if (encoding == null) {
808: return name.getBytes();
809: } else {
810: try {
811: return name.getBytes(encoding);
812: } catch (UnsupportedEncodingException uee) {
813: throw new ZipException(uee.getMessage());
814: }
815: }
816: }
817:
818: /**
819: * Write bytes to output or random access file
820: *
821: * @since 1.14
822: */
823: protected final void writeOut(byte[] data) throws IOException {
824: writeOut(data, 0, data.length);
825: }
826:
827: /**
828: * Write bytes to output or random access file
829: *
830: * @since 1.14
831: */
832: protected final void writeOut(byte[] data, int offset, int length)
833: throws IOException {
834: if (raf != null) {
835: raf.write(data, offset, length);
836: } else {
837: out.write(data, offset, length);
838: }
839: }
840: }
|