1. /*
  2. * @(#)ZipOutputStream.java 1.19 01/11/29
  3. *
  4. * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package java.util.zip;
  8. import java.io.OutputStream;
  9. import java.io.IOException;
  10. import java.util.Vector;
  11. import java.util.Hashtable;
  12. import java.util.Enumeration;
  13. /**
  14. * This class implements an output stream filter for writing files in the
  15. * ZIP file format. Includes support for both compressed and uncompressed
  16. * entries.
  17. *
  18. * @author David Connelly
  19. * @version 1.19, 11/29/01
  20. */
  21. public
  22. class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
  23. private ZipEntry entry;
  24. private Vector entries = new Vector();
  25. private Hashtable names = new Hashtable();
  26. private CRC32 crc = new CRC32();
  27. private long written;
  28. private long locoff = 0;
  29. private String comment;
  30. private int method = DEFLATED;
  31. private boolean finished;
  32. private boolean closed = false;
  33. /**
  34. * Check to make sure that this stream has not been closed
  35. */
  36. private void ensureOpen() throws IOException {
  37. if (closed) {
  38. throw new IOException("Stream closed");
  39. }
  40. }
  41. /**
  42. * Compression method for uncompressed (STORED) entries.
  43. */
  44. public static final int STORED = ZipEntry.STORED;
  45. /**
  46. * Compression method for compressed (DEFLATED) entries.
  47. */
  48. public static final int DEFLATED = ZipEntry.DEFLATED;
  49. /**
  50. * Creates a new ZIP output stream.
  51. * @param out the actual output stream
  52. */
  53. public ZipOutputStream(OutputStream out) {
  54. super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true));
  55. }
  56. /**
  57. * Sets the ZIP file comment.
  58. * @param comment the comment string
  59. * @exception IllegalArgumentException if the length of the specified
  60. * ZIP file comment is greater than 0xFFFF bytes
  61. */
  62. public void setComment(String comment) {
  63. if (comment.length() > 0xffff) {
  64. throw new IllegalArgumentException("invalid ZIP file comment");
  65. }
  66. this.comment = comment;
  67. }
  68. /**
  69. * Sets the default compression method for subsequent entries. This
  70. * default will be used whenever the compression method is not specified
  71. * for an individual ZIP file entry, and is initially set to DEFLATED.
  72. * @param method the default compression method
  73. * @exception IllegalArgumentException if the specified compression method
  74. * is invalid
  75. */
  76. public void setMethod(int method) {
  77. if (method != DEFLATED && method != STORED) {
  78. throw new IllegalArgumentException("invalid compression method");
  79. }
  80. this.method = method;
  81. }
  82. /**
  83. * Sets the compression level for subsequent entries which are DEFLATED.
  84. * The default setting is DEFAULT_COMPRESSION.
  85. * @param level the compression level (0-9)
  86. * @exception IllegalArgumentException if the compression level is invalid
  87. */
  88. public void setLevel(int level) {
  89. def.setLevel(level);
  90. }
  91. /**
  92. * Begins writing a new ZIP file entry and positions the stream to the
  93. * start of the entry data. Closes the current entry if still active.
  94. * The default compression method will be used if no compression method
  95. * was specified for the entry, and the current time will be used if
  96. * the entry has no set modification time.
  97. * @param e the ZIP entry to be written
  98. * @exception ZipException if a ZIP format error has occurred
  99. * @exception IOException if an I/O error has occurred
  100. */
  101. public void putNextEntry(ZipEntry e) throws IOException {
  102. ensureOpen();
  103. if (entry != null) {
  104. closeEntry(); // close previous entry
  105. }
  106. if (e.time == -1) {
  107. e.setTime(System.currentTimeMillis());
  108. }
  109. if (e.method == -1) {
  110. e.method = method; // use default method
  111. }
  112. switch (e.method) {
  113. case DEFLATED:
  114. if (e.size == -1 || e.csize == -1 || e.crc == -1) {
  115. // store size, compressed size, and crc-32 in data descriptor
  116. // immediately following the compressed entry data
  117. e.flag = 8;
  118. } else if (e.size != -1 && e.csize != -1 && e.crc != -1) {
  119. // store size, compressed size, and crc-32 in LOC header
  120. e.flag = 0;
  121. } else {
  122. throw new ZipException(
  123. "DEFLATED entry missing size, compressed size, or crc-32");
  124. }
  125. e.version = 20;
  126. break;
  127. case STORED:
  128. // compressed size, uncompressed size, and crc-32 must all be
  129. // set for entries using STORED compression method
  130. if (e.size == -1) {
  131. e.size = e.csize;
  132. } else if (e.csize == -1) {
  133. e.csize = e.size;
  134. } else if (e.size != e.csize) {
  135. throw new ZipException(
  136. "STORED entry where compressed != uncompressed size");
  137. }
  138. if (e.size == -1 || e.crc == -1) {
  139. throw new ZipException(
  140. "STORED entry missing size, compressed size, or crc-32");
  141. }
  142. e.version = 10;
  143. e.flag = 0;
  144. break;
  145. default:
  146. throw new ZipException("unsupported compression method");
  147. }
  148. e.offset = written;
  149. writeLOC(e);
  150. if (names.put(e.name, e) != null) {
  151. throw new ZipException("duplicate entry: " + e.name);
  152. }
  153. entries.addElement(e);
  154. entry = e;
  155. }
  156. /**
  157. * Closes the current ZIP entry and positions the stream for writing
  158. * the next entry.
  159. * @exception ZipException if a ZIP format error has occurred
  160. * @exception IOException if an I/O error has occurred
  161. */
  162. public void closeEntry() throws IOException {
  163. ensureOpen();
  164. ZipEntry e = entry;
  165. if (e != null) {
  166. switch (e.method) {
  167. case DEFLATED:
  168. def.finish();
  169. while (!def.finished()) {
  170. deflate();
  171. }
  172. if ((e.flag & 8) == 0) {
  173. // verify size, compressed size, and crc-32 settings
  174. if (e.size != def.getTotalIn()) {
  175. throw new ZipException(
  176. "invalid entry size (expected " + e.size +
  177. " but got " + def.getTotalIn() + " bytes)");
  178. }
  179. if (e.csize != def.getTotalOut()) {
  180. throw new ZipException(
  181. "invalid entry compressed size (expected " +
  182. e.csize + " but got " + def.getTotalOut() +
  183. " bytes)");
  184. }
  185. if (e.crc != crc.getValue()) {
  186. throw new ZipException(
  187. "invalid entry CRC-32 (expected 0x" +
  188. Long.toHexString(e.crc) + " but got 0x" +
  189. Long.toHexString(crc.getValue()) + ")");
  190. }
  191. } else {
  192. e.size = def.getTotalIn();
  193. e.csize = def.getTotalOut();
  194. e.crc = crc.getValue();
  195. writeEXT(e);
  196. }
  197. def.reset();
  198. written += e.csize;
  199. break;
  200. case STORED:
  201. // we already know that both e.size and e.csize are the same
  202. if (e.size != written - locoff) {
  203. throw new ZipException(
  204. "invalid entry size (expected " + e.size +
  205. " but got " + (written - locoff) + " bytes)");
  206. }
  207. if (e.crc != crc.getValue()) {
  208. throw new ZipException(
  209. "invalid entry crc-32 (expected 0x" +
  210. Long.toHexString(e.size) + " but got 0x" +
  211. Long.toHexString(crc.getValue()) + ")");
  212. }
  213. break;
  214. default:
  215. throw new InternalError("invalid compression method");
  216. }
  217. crc.reset();
  218. entry = null;
  219. }
  220. }
  221. /**
  222. * Writes an array of bytes to the current ZIP entry data. This method
  223. * will block until all the bytes are written.
  224. * @param b the data to be written
  225. * @param off the start offset in the data
  226. * @param len the number of bytes that are written
  227. * @exception ZipException if a ZIP file error has occurred
  228. * @exception IOException if an I/O error has occurred
  229. */
  230. public synchronized void write(byte[] b, int off, int len)
  231. throws IOException
  232. {
  233. ensureOpen();
  234. if (entry == null) {
  235. throw new ZipException("no current ZIP entry");
  236. }
  237. switch (entry.method) {
  238. case DEFLATED:
  239. super.write(b, off, len);
  240. break;
  241. case STORED:
  242. written += len;
  243. if (written - locoff > entry.size) {
  244. throw new ZipException(
  245. "attempt to write past end of STORED entry");
  246. }
  247. out.write(b, off, len);
  248. break;
  249. default:
  250. throw new InternalError("invalid compression method");
  251. }
  252. crc.update(b, off, len);
  253. }
  254. /**
  255. * Finishes writing the contents of the ZIP output stream without closing
  256. * the underlying stream. Use this method when applying multiple filters
  257. * in succession to the same output stream.
  258. * @exception ZipException if a ZIP file error has occurred
  259. * @exception IOException if an I/O exception has occurred
  260. */
  261. public void finish() throws IOException {
  262. ensureOpen();
  263. if (finished) {
  264. return;
  265. }
  266. if (entry != null) {
  267. closeEntry();
  268. }
  269. if (entries.size() < 1) {
  270. throw new ZipException("ZIP file must have at least one entry");
  271. }
  272. // write central directory
  273. long off = written;
  274. Enumeration e = entries.elements();
  275. while (e.hasMoreElements()) {
  276. writeCEN((ZipEntry)e.nextElement());
  277. }
  278. writeEND(off, written - off);
  279. finished = true;
  280. }
  281. /**
  282. * Closes the ZIP output stream as well as the stream being filtered.
  283. * @exception ZipException if a ZIP file error has occurred
  284. * @exception IOException if an I/O error has occurred
  285. */
  286. public void close() throws IOException {
  287. finish();
  288. out.close();
  289. closed = true;
  290. }
  291. /*
  292. * Writes local file (LOC) header for specified entry.
  293. */
  294. private void writeLOC(ZipEntry e) throws IOException {
  295. writeInt(LOCSIG); // LOC header signature
  296. writeShort(e.version); // version needed to extract
  297. writeShort(e.flag); // general purpose bit flag
  298. writeShort(e.method); // compression method
  299. writeInt(e.time); // last modification time
  300. if ((e.flag & 8) == 8) {
  301. // store size, uncompressed size, and crc-32 in data descriptor
  302. // immediately following compressed entry data
  303. writeInt(0);
  304. writeInt(0);
  305. writeInt(0);
  306. } else {
  307. writeInt(e.crc); // crc-32
  308. writeInt(e.csize); // compressed size
  309. writeInt(e.size); // uncompressed size
  310. }
  311. byte[] nameBytes = getUTF8Bytes(e.name);
  312. writeShort(nameBytes.length);
  313. writeShort(e.extra != null ? e.extra.length : 0);
  314. writeBytes(nameBytes, 0, nameBytes.length);
  315. if (e.extra != null) {
  316. writeBytes(e.extra, 0, e.extra.length);
  317. }
  318. locoff = written;
  319. }
  320. /*
  321. * Writes extra data descriptor (EXT) for specified entry.
  322. */
  323. private void writeEXT(ZipEntry e) throws IOException {
  324. writeInt(EXTSIG); // EXT header signature
  325. writeInt(e.crc); // crc-32
  326. writeInt(e.csize); // compressed size
  327. writeInt(e.size); // uncompressed size
  328. }
  329. /*
  330. * Write central directory (CEN) header for specified entry.
  331. * REMIND: add support for file attributes
  332. */
  333. private void writeCEN(ZipEntry e) throws IOException {
  334. writeInt(CENSIG); // CEN header signature
  335. writeShort(e.version); // version made by
  336. writeShort(e.version); // version needed to extract
  337. writeShort(e.flag); // general purpose bit flag
  338. writeShort(e.method); // compression method
  339. writeInt(e.time); // last modification time
  340. writeInt(e.crc); // crc-32
  341. writeInt(e.csize); // compressed size
  342. writeInt(e.size); // uncompressed size
  343. byte[] nameBytes = getUTF8Bytes(e.name);
  344. writeShort(nameBytes.length);
  345. writeShort(e.extra != null ? e.extra.length : 0);
  346. byte[] commentBytes;
  347. if (e.comment != null) {
  348. commentBytes = getUTF8Bytes(e.comment);
  349. writeShort(commentBytes.length);
  350. } else {
  351. commentBytes = null;
  352. writeShort(0);
  353. }
  354. writeShort(0); // starting disk number
  355. writeShort(0); // internal file attributes (unused)
  356. writeInt(0); // external file attributes (unused)
  357. writeInt(e.offset); // relative offset of local header
  358. writeBytes(nameBytes, 0, nameBytes.length);
  359. if (e.extra != null) {
  360. writeBytes(e.extra, 0, e.extra.length);
  361. }
  362. if (commentBytes != null) {
  363. writeBytes(commentBytes, 0, commentBytes.length);
  364. }
  365. }
  366. /*
  367. * Writes end of central directory (END) header.
  368. */
  369. private void writeEND(long off, long len) throws IOException {
  370. writeInt(ENDSIG); // END record signature
  371. writeShort(0); // number of this disk
  372. writeShort(0); // central directory start disk
  373. writeShort(entries.size()); // number of directory entries on disk
  374. writeShort(entries.size()); // total number of directory entries
  375. writeInt(len); // length of central directory
  376. writeInt(off); // offset of central directory
  377. if (comment != null) { // zip file comment
  378. byte[] b = getUTF8Bytes(comment);
  379. writeShort(b.length);
  380. writeBytes(b, 0, b.length);
  381. } else {
  382. writeShort(0);
  383. }
  384. }
  385. /*
  386. * Writes a 16-bit short to the output stream in little-endian byte order.
  387. */
  388. private void writeShort(int v) throws IOException {
  389. OutputStream out = this.out;
  390. out.write((v >>> 0) & 0xff);
  391. out.write((v >>> 8) & 0xff);
  392. written += 2;
  393. }
  394. /*
  395. * Writes a 32-bit int to the output stream in little-endian byte order.
  396. */
  397. private void writeInt(long v) throws IOException {
  398. OutputStream out = this.out;
  399. out.write((int)((v >>> 0) & 0xff));
  400. out.write((int)((v >>> 8) & 0xff));
  401. out.write((int)((v >>> 16) & 0xff));
  402. out.write((int)((v >>> 24) & 0xff));
  403. written += 4;
  404. }
  405. /*
  406. * Writes an array of bytes to the output stream.
  407. */
  408. private void writeBytes(byte[] b, int off, int len) throws IOException {
  409. super.out.write(b, off, len);
  410. written += len;
  411. }
  412. /*
  413. * Returns an array of bytes representing the UTF8 encoding
  414. * of the specified String.
  415. */
  416. private static byte[] getUTF8Bytes(String s) {
  417. char[] c = s.toCharArray();
  418. int len = c.length;
  419. // Count the number of encoded bytes...
  420. int count = 0;
  421. for (int i = 0; i < len; i++) {
  422. int ch = c[i];
  423. if (ch <= 0x7f) {
  424. count++;
  425. } else if (ch <= 0x7ff) {
  426. count += 2;
  427. } else {
  428. count += 3;
  429. }
  430. }
  431. // Now return the encoded bytes...
  432. byte[] b = new byte[count];
  433. int off = 0;
  434. for (int i = 0; i < len; i++) {
  435. int ch = c[i];
  436. if (ch <= 0x7f) {
  437. b[off++] = (byte)ch;
  438. } else if (ch <= 0x7ff) {
  439. b[off++] = (byte)((ch >> 6) | 0xc0);
  440. b[off++] = (byte)((ch & 0x3f) | 0x80);
  441. } else {
  442. b[off++] = (byte)((ch >> 12) | 0xe0);
  443. b[off++] = (byte)(((ch >> 6) & 0x3f) | 0x80);
  444. b[off++] = (byte)((ch & 0x3f) | 0x80);
  445. }
  446. }
  447. return b;
  448. }
  449. }