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