1. /*
  2. * @(#)ZipOutputStream.java 1.27 03/02/07
  3. *
  4. * Copyright 2003 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.27, 02/07/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;
  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.getTotalIn()) {
  177. throw new ZipException(
  178. "invalid entry size (expected " + e.size +
  179. " but got " + def.getTotalIn() + " bytes)");
  180. }
  181. if (e.csize != def.getTotalOut()) {
  182. throw new ZipException(
  183. "invalid entry compressed size (expected " +
  184. e.csize + " but got " + def.getTotalOut() +
  185. " bytes)");
  186. }
  187. if (e.crc != crc.getValue()) {
  188. throw new ZipException(
  189. "invalid entry CRC-32 (expected 0x" +
  190. Long.toHexString(e.crc) + " but got 0x" +
  191. Long.toHexString(crc.getValue()) + ")");
  192. }
  193. } else {
  194. e.size = def.getTotalIn();
  195. e.csize = def.getTotalOut();
  196. e.crc = crc.getValue();
  197. writeEXT(e);
  198. }
  199. def.reset();
  200. written += e.csize;
  201. break;
  202. case STORED:
  203. // we already know that both e.size and e.csize are the same
  204. if (e.size != written - locoff) {
  205. throw new ZipException(
  206. "invalid entry size (expected " + e.size +
  207. " but got " + (written - locoff) + " bytes)");
  208. }
  209. if (e.crc != crc.getValue()) {
  210. throw new ZipException(
  211. "invalid entry crc-32 (expected 0x" +
  212. Long.toHexString(e.crc) + " but got 0x" +
  213. Long.toHexString(crc.getValue()) + ")");
  214. }
  215. break;
  216. default:
  217. throw new InternalError("invalid compression method");
  218. }
  219. crc.reset();
  220. entry = null;
  221. }
  222. }
  223. /**
  224. * Writes an array of bytes to the current ZIP entry data. This method
  225. * will block until all the bytes are written.
  226. * @param b the data to be written
  227. * @param off the start offset in the data
  228. * @param len the number of bytes that are written
  229. * @exception ZipException if a ZIP file error has occurred
  230. * @exception IOException if an I/O error has occurred
  231. */
  232. public synchronized void write(byte[] b, int off, int len)
  233. throws IOException
  234. {
  235. ensureOpen();
  236. if (off < 0 || len < 0 || off > b.length - len) {
  237. throw new IndexOutOfBoundsException();
  238. } else if (len == 0) {
  239. return;
  240. }
  241. if (entry == null) {
  242. throw new ZipException("no current ZIP entry");
  243. }
  244. switch (entry.method) {
  245. case DEFLATED:
  246. super.write(b, off, len);
  247. break;
  248. case STORED:
  249. written += len;
  250. if (written - locoff > entry.size) {
  251. throw new ZipException(
  252. "attempt to write past end of STORED entry");
  253. }
  254. out.write(b, off, len);
  255. break;
  256. default:
  257. throw new InternalError("invalid compression method");
  258. }
  259. crc.update(b, off, len);
  260. }
  261. /**
  262. * Finishes writing the contents of the ZIP output stream without closing
  263. * the underlying stream. Use this method when applying multiple filters
  264. * in succession to the same output stream.
  265. * @exception ZipException if a ZIP file error has occurred
  266. * @exception IOException if an I/O exception has occurred
  267. */
  268. public void finish() throws IOException {
  269. ensureOpen();
  270. if (finished) {
  271. return;
  272. }
  273. if (entry != null) {
  274. closeEntry();
  275. }
  276. if (entries.size() < 1) {
  277. throw new ZipException("ZIP file must have at least one entry");
  278. }
  279. // write central directory
  280. long off = written;
  281. Enumeration e = entries.elements();
  282. while (e.hasMoreElements()) {
  283. writeCEN((ZipEntry)e.nextElement());
  284. }
  285. writeEND(off, written - off);
  286. finished = true;
  287. }
  288. /**
  289. * Closes the ZIP output stream as well as the stream being filtered.
  290. * @exception ZipException if a ZIP file error has occurred
  291. * @exception IOException if an I/O error has occurred
  292. */
  293. public void close() throws IOException {
  294. if (!closed) {
  295. super.close();
  296. closed = true;
  297. }
  298. }
  299. /*
  300. * Writes local file (LOC) header for specified entry.
  301. */
  302. private void writeLOC(ZipEntry e) throws IOException {
  303. writeInt(LOCSIG); // LOC header signature
  304. writeShort(e.version); // version needed to extract
  305. writeShort(e.flag); // general purpose bit flag
  306. writeShort(e.method); // compression method
  307. writeInt(e.time); // last modification time
  308. if ((e.flag & 8) == 8) {
  309. // store size, uncompressed size, and crc-32 in data descriptor
  310. // immediately following compressed entry data
  311. writeInt(0);
  312. writeInt(0);
  313. writeInt(0);
  314. } else {
  315. writeInt(e.crc); // crc-32
  316. writeInt(e.csize); // compressed size
  317. writeInt(e.size); // uncompressed size
  318. }
  319. byte[] nameBytes = getUTF8Bytes(e.name);
  320. writeShort(nameBytes.length);
  321. writeShort(e.extra != null ? e.extra.length : 0);
  322. writeBytes(nameBytes, 0, nameBytes.length);
  323. if (e.extra != null) {
  324. writeBytes(e.extra, 0, e.extra.length);
  325. }
  326. locoff = written;
  327. }
  328. /*
  329. * Writes extra data descriptor (EXT) for specified entry.
  330. */
  331. private void writeEXT(ZipEntry e) throws IOException {
  332. writeInt(EXTSIG); // EXT header signature
  333. writeInt(e.crc); // crc-32
  334. writeInt(e.csize); // compressed size
  335. writeInt(e.size); // uncompressed size
  336. }
  337. /*
  338. * Write central directory (CEN) header for specified entry.
  339. * REMIND: add support for file attributes
  340. */
  341. private void writeCEN(ZipEntry e) throws IOException {
  342. writeInt(CENSIG); // CEN header signature
  343. writeShort(e.version); // version made by
  344. writeShort(e.version); // version needed to extract
  345. writeShort(e.flag); // general purpose bit flag
  346. writeShort(e.method); // compression method
  347. writeInt(e.time); // last modification time
  348. writeInt(e.crc); // crc-32
  349. writeInt(e.csize); // compressed size
  350. writeInt(e.size); // uncompressed size
  351. byte[] nameBytes = getUTF8Bytes(e.name);
  352. writeShort(nameBytes.length);
  353. writeShort(e.extra != null ? e.extra.length : 0);
  354. byte[] commentBytes;
  355. if (e.comment != null) {
  356. commentBytes = getUTF8Bytes(e.comment);
  357. writeShort(commentBytes.length);
  358. } else {
  359. commentBytes = null;
  360. writeShort(0);
  361. }
  362. writeShort(0); // starting disk number
  363. writeShort(0); // internal file attributes (unused)
  364. writeInt(0); // external file attributes (unused)
  365. writeInt(e.offset); // relative offset of local header
  366. writeBytes(nameBytes, 0, nameBytes.length);
  367. if (e.extra != null) {
  368. writeBytes(e.extra, 0, e.extra.length);
  369. }
  370. if (commentBytes != null) {
  371. writeBytes(commentBytes, 0, commentBytes.length);
  372. }
  373. }
  374. /*
  375. * Writes end of central directory (END) header.
  376. */
  377. private void writeEND(long off, long len) throws IOException {
  378. writeInt(ENDSIG); // END record signature
  379. writeShort(0); // number of this disk
  380. writeShort(0); // central directory start disk
  381. writeShort(entries.size()); // number of directory entries on disk
  382. writeShort(entries.size()); // total number of directory entries
  383. writeInt(len); // length of central directory
  384. writeInt(off); // offset of central directory
  385. if (comment != null) { // zip file comment
  386. byte[] b = getUTF8Bytes(comment);
  387. writeShort(b.length);
  388. writeBytes(b, 0, b.length);
  389. } else {
  390. writeShort(0);
  391. }
  392. }
  393. /*
  394. * Writes a 16-bit short to the output stream in little-endian byte order.
  395. */
  396. private void writeShort(int v) throws IOException {
  397. OutputStream out = this.out;
  398. out.write((v >>> 0) & 0xff);
  399. out.write((v >>> 8) & 0xff);
  400. written += 2;
  401. }
  402. /*
  403. * Writes a 32-bit int to the output stream in little-endian byte order.
  404. */
  405. private void writeInt(long v) throws IOException {
  406. OutputStream out = this.out;
  407. out.write((int)((v >>> 0) & 0xff));
  408. out.write((int)((v >>> 8) & 0xff));
  409. out.write((int)((v >>> 16) & 0xff));
  410. out.write((int)((v >>> 24) & 0xff));
  411. written += 4;
  412. }
  413. /*
  414. * Writes an array of bytes to the output stream.
  415. */
  416. private void writeBytes(byte[] b, int off, int len) throws IOException {
  417. super.out.write(b, off, len);
  418. written += len;
  419. }
  420. /*
  421. * Returns the length of String's UTF8 encoding.
  422. */
  423. static int getUTF8Length(String s) {
  424. int count = 0;
  425. for (int i = 0; i < s.length(); i++) {
  426. char ch = s.charAt(i);
  427. if (ch <= 0x7f) {
  428. count++;
  429. } else if (ch <= 0x7ff) {
  430. count += 2;
  431. } else {
  432. count += 3;
  433. }
  434. }
  435. return count;
  436. }
  437. /*
  438. * Returns an array of bytes representing the UTF8 encoding
  439. * of the specified String.
  440. */
  441. private static byte[] getUTF8Bytes(String s) {
  442. char[] c = s.toCharArray();
  443. int len = c.length;
  444. // Count the number of encoded bytes...
  445. int count = 0;
  446. for (int i = 0; i < len; i++) {
  447. int ch = c[i];
  448. if (ch <= 0x7f) {
  449. count++;
  450. } else if (ch <= 0x7ff) {
  451. count += 2;
  452. } else {
  453. count += 3;
  454. }
  455. }
  456. // Now return the encoded bytes...
  457. byte[] b = new byte[count];
  458. int off = 0;
  459. for (int i = 0; i < len; i++) {
  460. int ch = c[i];
  461. if (ch <= 0x7f) {
  462. b[off++] = (byte)ch;
  463. } else if (ch <= 0x7ff) {
  464. b[off++] = (byte)((ch >> 6) | 0xc0);
  465. b[off++] = (byte)((ch & 0x3f) | 0x80);
  466. } else {
  467. b[off++] = (byte)((ch >> 12) | 0xe0);
  468. b[off++] = (byte)(((ch >> 6) & 0x3f) | 0x80);
  469. b[off++] = (byte)((ch & 0x3f) | 0x80);
  470. }
  471. }
  472. return b;
  473. }
  474. }