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 net.sourceforge.groboutils.codecoverage.v2.ant.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.1 $
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 Vector entries = new Vector();
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 offsets = new Hashtable();
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 #putEntry putEntry}.
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((ZipEntry) 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: public void write(byte[] b, int offset, int length)
455: throws IOException {
456: if (entry.getMethod() == DEFLATED) {
457: if (length > 0) {
458: if (!def.finished()) {
459: def.setInput(b, offset, length);
460: while (!def.needsInput()) {
461: deflate();
462: }
463: }
464: }
465: } else {
466: writeOut(b, offset, length);
467: written += length;
468: }
469: crc.update(b, offset, length);
470: }
471:
472: /**
473: * Writes a single byte to ZIP entry.
474: *
475: * <p>Delegates to the three arg method.</p>
476: *
477: * @since 1.14
478: */
479: public void write(int b) throws IOException {
480: byte[] buf = new byte[1];
481: buf[0] = (byte) (b & 0xff);
482: write(buf, 0, 1);
483: }
484:
485: /**
486: * Closes this output stream and releases any system resources
487: * associated with the stream.
488: *
489: * @exception IOException if an I/O error occurs.
490: * @since 1.14
491: */
492: public void close() throws IOException {
493: finish();
494:
495: if (raf != null) {
496: raf.close();
497: }
498: if (out != null) {
499: out.close();
500: }
501: }
502:
503: /**
504: * Flushes this output stream and forces any buffered output bytes
505: * to be written out to the stream.
506: *
507: * @exception IOException if an I/O error occurs.
508: * @since 1.14
509: */
510: public void flush() throws IOException {
511: if (out == null) {
512: out.flush();
513: }
514: }
515:
516: /*
517: * Various ZIP constants
518: */
519: /**
520: * local file header signature
521: *
522: * @since 1.1
523: */
524: protected static final ZipLong LFH_SIG = new ZipLong(0X04034B50L);
525: /**
526: * data descriptor signature
527: *
528: * @since 1.1
529: */
530: protected static final ZipLong DD_SIG = new ZipLong(0X08074B50L);
531: /**
532: * central file header signature
533: *
534: * @since 1.1
535: */
536: protected static final ZipLong CFH_SIG = new ZipLong(0X02014B50L);
537: /**
538: * end of central dir signature
539: *
540: * @since 1.1
541: */
542: protected static final ZipLong EOCD_SIG = new ZipLong(0X06054B50L);
543:
544: /**
545: * Writes next block of compressed data to the output stream.
546: *
547: * @since 1.14
548: */
549: protected final void deflate() throws IOException {
550: int len = def.deflate(buf, 0, buf.length);
551: if (len > 0) {
552: writeOut(buf, 0, len);
553: }
554: }
555:
556: /**
557: * Writes the local file header entry
558: *
559: * @since 1.1
560: */
561: protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
562: offsets.put(ze, new ZipLong(written));
563:
564: writeOut(LFH_SIG.getBytes());
565: written += 4;
566:
567: // version needed to extract
568: // general purpose bit flag
569: if (ze.getMethod() == DEFLATED && raf == null) {
570: // requires version 2 as we are going to store length info
571: // in the data descriptor
572: writeOut((new ZipShort(20)).getBytes());
573:
574: // bit3 set to signal, we use a data descriptor
575: writeOut((new ZipShort(8)).getBytes());
576: } else {
577: writeOut((new ZipShort(10)).getBytes());
578: writeOut(ZERO);
579: }
580: written += 4;
581:
582: // compression method
583: writeOut((new ZipShort(ze.getMethod())).getBytes());
584: written += 2;
585:
586: // last mod. time and date
587: writeOut(toDosTime(new Date(ze.getTime())).getBytes());
588: written += 4;
589:
590: // CRC
591: // compressed length
592: // uncompressed length
593: localDataStart = written;
594: if (ze.getMethod() == DEFLATED || raf != null) {
595: writeOut(LZERO);
596: writeOut(LZERO);
597: writeOut(LZERO);
598: } else {
599: writeOut((new ZipLong(ze.getCrc())).getBytes());
600: writeOut((new ZipLong(ze.getSize())).getBytes());
601: writeOut((new ZipLong(ze.getSize())).getBytes());
602: }
603: written += 12;
604:
605: // file name length
606: byte[] name = getBytes(ze.getName());
607: writeOut((new ZipShort(name.length)).getBytes());
608: written += 2;
609:
610: // extra field length
611: byte[] extra = ze.getLocalFileDataExtra();
612: writeOut((new ZipShort(extra.length)).getBytes());
613: written += 2;
614:
615: // file name
616: writeOut(name);
617: written += name.length;
618:
619: // extra field
620: writeOut(extra);
621: written += extra.length;
622:
623: dataStart = written;
624: }
625:
626: /**
627: * Writes the data descriptor entry
628: *
629: * @since 1.1
630: */
631: protected void writeDataDescriptor(ZipEntry ze) throws IOException {
632: if (ze.getMethod() != DEFLATED || raf != null) {
633: return;
634: }
635: writeOut(DD_SIG.getBytes());
636: writeOut((new ZipLong(entry.getCrc())).getBytes());
637: writeOut((new ZipLong(entry.getCompressedSize())).getBytes());
638: writeOut((new ZipLong(entry.getSize())).getBytes());
639: written += 16;
640: }
641:
642: /**
643: * Writes the central file header entry
644: *
645: * @since 1.1
646: */
647: protected void writeCentralFileHeader(ZipEntry ze)
648: throws IOException {
649: writeOut(CFH_SIG.getBytes());
650: written += 4;
651:
652: // version made by
653: writeOut((new ZipShort((ze.getPlatform() << 8) | 20))
654: .getBytes());
655: written += 2;
656:
657: // version needed to extract
658: // general purpose bit flag
659: if (ze.getMethod() == DEFLATED && raf == null) {
660: // requires version 2 as we are going to store length info
661: // in the data descriptor
662: writeOut((new ZipShort(20)).getBytes());
663:
664: // bit3 set to signal, we use a data descriptor
665: writeOut((new ZipShort(8)).getBytes());
666: } else {
667: writeOut((new ZipShort(10)).getBytes());
668: writeOut(ZERO);
669: }
670: written += 4;
671:
672: // compression method
673: writeOut((new ZipShort(ze.getMethod())).getBytes());
674: written += 2;
675:
676: // last mod. time and date
677: writeOut(toDosTime(new Date(ze.getTime())).getBytes());
678: written += 4;
679:
680: // CRC
681: // compressed length
682: // uncompressed length
683: writeOut((new ZipLong(ze.getCrc())).getBytes());
684: writeOut((new ZipLong(ze.getCompressedSize())).getBytes());
685: writeOut((new ZipLong(ze.getSize())).getBytes());
686: written += 12;
687:
688: // file name length
689: byte[] name = getBytes(ze.getName());
690: writeOut((new ZipShort(name.length)).getBytes());
691: written += 2;
692:
693: // extra field length
694: byte[] extra = ze.getCentralDirectoryExtra();
695: writeOut((new ZipShort(extra.length)).getBytes());
696: written += 2;
697:
698: // file comment length
699: String comm = ze.getComment();
700: if (comm == null) {
701: comm = "";
702: }
703: byte[] comment = getBytes(comm);
704: writeOut((new ZipShort(comment.length)).getBytes());
705: written += 2;
706:
707: // disk number start
708: writeOut(ZERO);
709: written += 2;
710:
711: // internal file attributes
712: writeOut((new ZipShort(ze.getInternalAttributes())).getBytes());
713: written += 2;
714:
715: // external file attributes
716: writeOut((new ZipLong(ze.getExternalAttributes())).getBytes());
717: written += 4;
718:
719: // relative offset of LFH
720: writeOut(((ZipLong) offsets.get(ze)).getBytes());
721: written += 4;
722:
723: // file name
724: writeOut(name);
725: written += name.length;
726:
727: // extra field
728: writeOut(extra);
729: written += extra.length;
730:
731: // file comment
732: writeOut(comment);
733: written += comment.length;
734: }
735:
736: /**
737: * Writes the "End of central dir record"
738: *
739: * @since 1.1
740: */
741: protected void writeCentralDirectoryEnd() throws IOException {
742: writeOut(EOCD_SIG.getBytes());
743:
744: // disk numbers
745: writeOut(ZERO);
746: writeOut(ZERO);
747:
748: // number of entries
749: byte[] num = (new ZipShort(entries.size())).getBytes();
750: writeOut(num);
751: writeOut(num);
752:
753: // length and location of CD
754: writeOut(cdLength.getBytes());
755: writeOut(cdOffset.getBytes());
756:
757: // ZIP file comment
758: byte[] data = getBytes(comment);
759: writeOut((new ZipShort(data.length)).getBytes());
760: writeOut(data);
761: }
762:
763: /**
764: * Smallest date/time ZIP can handle.
765: *
766: * @since 1.1
767: */
768: private static final ZipLong DOS_TIME_MIN = new ZipLong(0x00002100L);
769:
770: /**
771: * Convert a Date object to a DOS date/time field.
772: *
773: * <p>Stolen from InfoZip's <code>fileio.c</code></p>
774: *
775: * @since 1.1
776: */
777: protected static ZipLong toDosTime(Date time) {
778: int year = time.getYear() + 1900;
779: int month = time.getMonth() + 1;
780: if (year < 1980) {
781: return DOS_TIME_MIN;
782: }
783: long value = ((year - 1980) << 25) | (month << 21)
784: | (time.getDate() << 16) | (time.getHours() << 11)
785: | (time.getMinutes() << 5) | (time.getSeconds() >> 1);
786:
787: byte[] result = new byte[4];
788: result[0] = (byte) ((value & 0xFF));
789: result[1] = (byte) ((value & 0xFF00) >> 8);
790: result[2] = (byte) ((value & 0xFF0000) >> 16);
791: result[3] = (byte) ((value & 0xFF000000L) >> 24);
792: return new ZipLong(result);
793: }
794:
795: /**
796: * Retrieve the bytes for the given String in the encoding set for
797: * this Stream.
798: *
799: * @since 1.3
800: */
801: protected byte[] getBytes(String name) throws ZipException {
802: if (encoding == null) {
803: return name.getBytes();
804: } else {
805: try {
806: return name.getBytes(encoding);
807: } catch (UnsupportedEncodingException uee) {
808: throw new ZipException(uee.getMessage());
809: }
810: }
811: }
812:
813: /**
814: * Write bytes to output or random access file
815: *
816: * @since 1.14
817: */
818: protected final void writeOut(byte[] data) throws IOException {
819: writeOut(data, 0, data.length);
820: }
821:
822: /**
823: * Write bytes to output or random access file
824: *
825: * @since 1.14
826: */
827: protected final void writeOut(byte[] data, int offset, int length)
828: throws IOException {
829: if (raf != null) {
830: raf.write(data, offset, length);
831: } else {
832: out.write(data, offset, length);
833: }
834: }
835: }
|