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