1. /*
  2. * Copyright 2003-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.zip;
  18. import java.io.File;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.RandomAccessFile;
  22. import java.io.UnsupportedEncodingException;
  23. import java.util.Calendar;
  24. import java.util.Date;
  25. import java.util.Enumeration;
  26. import java.util.Hashtable;
  27. import java.util.zip.Inflater;
  28. import java.util.zip.InflaterInputStream;
  29. import java.util.zip.ZipException;
  30. /**
  31. * Replacement for <code>java.util.ZipFile</code>.
  32. *
  33. * <p>This class adds support for file name encodings other than UTF-8
  34. * (which is required to work on ZIP files created by native zip tools
  35. * and is able to skip a preamble like the one found in self
  36. * extracting archives. Furthermore it returns instances of
  37. * <code>org.apache.tools.zip.ZipEntry</code> instead of
  38. * <code>java.util.zip.ZipEntry</code>.</p>
  39. *
  40. * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
  41. * have to reimplement all methods anyway. Like
  42. * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
  43. * covers and supports compressed and uncompressed entries.</p>
  44. *
  45. * <p>The method signatures mimic the ones of
  46. * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
  47. *
  48. * <ul>
  49. * <li>There is no getName method.</li>
  50. * <li>entries has been renamed to getEntries.</li>
  51. * <li>getEntries and getEntry return
  52. * <code>org.apache.tools.zip.ZipEntry</code> instances.</li>
  53. * <li>close is allowed to throw IOException.</li>
  54. * </ul>
  55. *
  56. * @version $Revision: 1.8.2.5 $
  57. */
  58. public class ZipFile {
  59. /**
  60. * Maps ZipEntrys to Longs, recording the offsets of the local
  61. * file headers.
  62. */
  63. private Hashtable entries = new Hashtable();
  64. /**
  65. * Maps String to ZipEntrys, name -> actual entry.
  66. */
  67. private Hashtable nameMap = new Hashtable();
  68. /**
  69. * Maps ZipEntrys to Longs, recording the offsets of the actual file data.
  70. */
  71. private Hashtable dataOffsets = new Hashtable();
  72. /**
  73. * The encoding to use for filenames and the file comment.
  74. *
  75. * <p>For a list of possible values see <a
  76. * 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>.
  77. * Defaults to the platform's default character encoding.</p>
  78. */
  79. private String encoding = null;
  80. /**
  81. * The actual data source.
  82. */
  83. private RandomAccessFile archive;
  84. /**
  85. * Opens the given file for reading, assuming the platform's
  86. * native encoding for file names.
  87. *
  88. * @param f the archive.
  89. *
  90. * @throws IOException if an error occurs while reading the file.
  91. */
  92. public ZipFile(File f) throws IOException {
  93. this(f, null);
  94. }
  95. /**
  96. * Opens the given file for reading, assuming the platform's
  97. * native encoding for file names.
  98. *
  99. * @param name name of the archive.
  100. *
  101. * @throws IOException if an error occurs while reading the file.
  102. */
  103. public ZipFile(String name) throws IOException {
  104. this(new File(name), null);
  105. }
  106. /**
  107. * Opens the given file for reading, assuming the specified
  108. * encoding for file names.
  109. *
  110. * @param name name of the archive.
  111. * @param encoding the encoding to use for file names
  112. *
  113. * @throws IOException if an error occurs while reading the file.
  114. */
  115. public ZipFile(String name, String encoding) throws IOException {
  116. this(new File(name), encoding);
  117. }
  118. /**
  119. * Opens the given file for reading, assuming the specified
  120. * encoding for file names.
  121. *
  122. * @param f the archive.
  123. * @param encoding the encoding to use for file names
  124. *
  125. * @throws IOException if an error occurs while reading the file.
  126. */
  127. public ZipFile(File f, String encoding) throws IOException {
  128. this.encoding = encoding;
  129. archive = new RandomAccessFile(f, "r");
  130. populateFromCentralDirectory();
  131. resolveLocalFileHeaderData();
  132. }
  133. /**
  134. * The encoding to use for filenames and the file comment.
  135. *
  136. * @return null if using the platform's default character encoding.
  137. */
  138. public String getEncoding() {
  139. return encoding;
  140. }
  141. /**
  142. * Closes the archive.
  143. * @throws IOException if an error occurs closing the archive.
  144. */
  145. public void close() throws IOException {
  146. archive.close();
  147. }
  148. /**
  149. * Returns all entries.
  150. * @return all entries as {@link ZipEntry} instances
  151. */
  152. public Enumeration getEntries() {
  153. return entries.keys();
  154. }
  155. /**
  156. * Returns a named entry - or <code>null</code> if no entry by
  157. * that name exists.
  158. * @param name name of the entry.
  159. * @return the ZipEntry corresponding to the given name - or
  160. * <code>null</code> if not present.
  161. */
  162. public ZipEntry getEntry(String name) {
  163. return (ZipEntry) nameMap.get(name);
  164. }
  165. /**
  166. * Returns an InputStream for reading the contents of the given entry.
  167. * @param ze the entry to get the stream for.
  168. * @return a stream to read the entry from.
  169. */
  170. public InputStream getInputStream(ZipEntry ze)
  171. throws IOException, ZipException {
  172. Long start = (Long) dataOffsets.get(ze);
  173. if (start == null) {
  174. return null;
  175. }
  176. BoundedInputStream bis =
  177. new BoundedInputStream(start.longValue(), ze.getCompressedSize());
  178. switch (ze.getMethod()) {
  179. case ZipEntry.STORED:
  180. return bis;
  181. case ZipEntry.DEFLATED:
  182. bis.addDummy();
  183. return new InflaterInputStream(bis, new Inflater(true));
  184. default:
  185. throw new ZipException("Found unsupported compression method "
  186. + ze.getMethod());
  187. }
  188. }
  189. private static final int CFH_LEN =
  190. /* version made by */ 2 +
  191. /* version needed to extract */ 2 +
  192. /* general purpose bit flag */ 2 +
  193. /* compression method */ 2 +
  194. /* last mod file time */ 2 +
  195. /* last mod file date */ 2 +
  196. /* crc-32 */ 4 +
  197. /* compressed size */ 4 +
  198. /* uncompressed size */ 4 +
  199. /* filename length */ 2 +
  200. /* extra field length */ 2 +
  201. /* file comment length */ 2 +
  202. /* disk number start */ 2 +
  203. /* internal file attributes */ 2 +
  204. /* external file attributes */ 4 +
  205. /* relative offset of local header */ 4;
  206. /**
  207. * Reads the central directory of the given archive and populates
  208. * the internal tables with ZipEntry instances.
  209. *
  210. * <p>The ZipEntrys will know all data that can be obtained from
  211. * the central directory alone, but not the data that requires the
  212. * local file header or additional data to be read.</p>
  213. */
  214. private void populateFromCentralDirectory()
  215. throws IOException {
  216. positionAtCentralDirectory();
  217. byte[] cfh = new byte[CFH_LEN];
  218. byte[] signatureBytes = new byte[4];
  219. archive.readFully(signatureBytes);
  220. ZipLong sig = new ZipLong(signatureBytes);
  221. while (sig.equals(ZipOutputStream.CFH_SIG)) {
  222. archive.readFully(cfh);
  223. int off = 0;
  224. ZipEntry ze = new ZipEntry();
  225. ZipShort versionMadeBy = new ZipShort(cfh, off);
  226. off += 2;
  227. ze.setPlatform((versionMadeBy.getValue() >> 8) & 0x0F);
  228. off += 4; // skip version info and general purpose byte
  229. ze.setMethod((new ZipShort(cfh, off)).getValue());
  230. off += 2;
  231. ze.setTime(fromDosTime(new ZipLong(cfh, off)).getTime());
  232. off += 4;
  233. ze.setCrc((new ZipLong(cfh, off)).getValue());
  234. off += 4;
  235. ze.setCompressedSize((new ZipLong(cfh, off)).getValue());
  236. off += 4;
  237. ze.setSize((new ZipLong(cfh, off)).getValue());
  238. off += 4;
  239. int fileNameLen = (new ZipShort(cfh, off)).getValue();
  240. off += 2;
  241. int extraLen = (new ZipShort(cfh, off)).getValue();
  242. off += 2;
  243. int commentLen = (new ZipShort(cfh, off)).getValue();
  244. off += 2;
  245. off += 2; // disk number
  246. ze.setInternalAttributes((new ZipShort(cfh, off)).getValue());
  247. off += 2;
  248. ze.setExternalAttributes((new ZipLong(cfh, off)).getValue());
  249. off += 4;
  250. // LFH offset
  251. entries.put(ze, new Long((new ZipLong(cfh, off)).getValue()));
  252. byte[] fileName = new byte[fileNameLen];
  253. archive.readFully(fileName);
  254. ze.setName(getString(fileName));
  255. nameMap.put(ze.getName(), ze);
  256. archive.skipBytes(extraLen);
  257. byte[] comment = new byte[commentLen];
  258. archive.readFully(comment);
  259. ze.setComment(getString(comment));
  260. archive.readFully(signatureBytes);
  261. sig = new ZipLong(signatureBytes);
  262. }
  263. }
  264. private static final int MIN_EOCD_SIZE =
  265. /* end of central dir signature */ 4 +
  266. /* number of this disk */ 2 +
  267. /* number of the disk with the */ +
  268. /* start of the central directory */ 2 +
  269. /* total number of entries in */ +
  270. /* the central dir on this disk */ 2 +
  271. /* total number of entries in */ +
  272. /* the central dir */ 2 +
  273. /* size of the central directory */ 4 +
  274. /* offset of start of central */ +
  275. /* directory with respect to */ +
  276. /* the starting disk number */ 4 +
  277. /* zipfile comment length */ 2;
  278. private static final int CFD_LOCATOR_OFFSET =
  279. /* end of central dir signature */ 4 +
  280. /* number of this disk */ 2 +
  281. /* number of the disk with the */ +
  282. /* start of the central directory */ 2 +
  283. /* total number of entries in */ +
  284. /* the central dir on this disk */ 2 +
  285. /* total number of entries in */ +
  286. /* the central dir */ 2 +
  287. /* size of the central directory */ 4;
  288. /**
  289. * Searches for the "End of central dir record", parses
  290. * it and positions the stream at the first central directory
  291. * record.
  292. */
  293. private void positionAtCentralDirectory()
  294. throws IOException {
  295. long off = archive.length() - MIN_EOCD_SIZE;
  296. archive.seek(off);
  297. byte[] sig = ZipOutputStream.EOCD_SIG.getBytes();
  298. int curr = archive.read();
  299. boolean found = false;
  300. while (curr != -1) {
  301. if (curr == sig[0]) {
  302. curr = archive.read();
  303. if (curr == sig[1]) {
  304. curr = archive.read();
  305. if (curr == sig[2]) {
  306. curr = archive.read();
  307. if (curr == sig[3]) {
  308. found = true;
  309. break;
  310. }
  311. }
  312. }
  313. }
  314. archive.seek(--off);
  315. curr = archive.read();
  316. }
  317. if (!found) {
  318. throw new ZipException("archive is not a ZIP archive");
  319. }
  320. archive.seek(off + CFD_LOCATOR_OFFSET);
  321. byte[] cfdOffset = new byte[4];
  322. archive.readFully(cfdOffset);
  323. archive.seek((new ZipLong(cfdOffset)).getValue());
  324. }
  325. /**
  326. * Number of bytes in local file header up to the "length of
  327. * filename" entry.
  328. */
  329. private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
  330. /* local file header signature */ 4 +
  331. /* version needed to extract */ 2 +
  332. /* general purpose bit flag */ 2 +
  333. /* compression method */ 2 +
  334. /* last mod file time */ 2 +
  335. /* last mod file date */ 2 +
  336. /* crc-32 */ 4 +
  337. /* compressed size */ 4 +
  338. /* uncompressed size */ 4;
  339. /**
  340. * Walks through all recorded entries and adds the data available
  341. * from the local file header.
  342. *
  343. * <p>Also records the offsets for the data to read from the
  344. * entries.</p>
  345. */
  346. private void resolveLocalFileHeaderData()
  347. throws IOException {
  348. Enumeration e = getEntries();
  349. while (e.hasMoreElements()) {
  350. ZipEntry ze = (ZipEntry) e.nextElement();
  351. long offset = ((Long) entries.get(ze)).longValue();
  352. archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
  353. byte[] b = new byte[2];
  354. archive.readFully(b);
  355. int fileNameLen = (new ZipShort(b)).getValue();
  356. archive.readFully(b);
  357. int extraFieldLen = (new ZipShort(b)).getValue();
  358. archive.skipBytes(fileNameLen);
  359. byte[] localExtraData = new byte[extraFieldLen];
  360. archive.readFully(localExtraData);
  361. ze.setExtra(localExtraData);
  362. dataOffsets.put(ze,
  363. new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
  364. + 2 + 2 + fileNameLen + extraFieldLen));
  365. }
  366. }
  367. /**
  368. * Convert a DOS date/time field to a Date object.
  369. *
  370. * @param l contains the stored DOS time.
  371. * @return a Date instance corresponding to the given time.
  372. */
  373. protected static Date fromDosTime(ZipLong l) {
  374. long dosTime = l.getValue();
  375. Calendar cal = Calendar.getInstance();
  376. cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
  377. cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
  378. cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
  379. cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
  380. cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
  381. cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
  382. return cal.getTime();
  383. }
  384. /**
  385. * Retrieve a String from the given bytes using the encoding set
  386. * for this ZipFile.
  387. *
  388. * @param bytes the byte array to transform
  389. * @return String obtained by using the given encoding
  390. * @throws ZipException if the encoding cannot be recognized.
  391. */
  392. protected String getString(byte[] bytes) throws ZipException {
  393. if (encoding == null) {
  394. return new String(bytes);
  395. } else {
  396. try {
  397. return new String(bytes, encoding);
  398. } catch (UnsupportedEncodingException uee) {
  399. throw new ZipException(uee.getMessage());
  400. }
  401. }
  402. }
  403. /**
  404. * InputStream that delegates requests to the underlying
  405. * RandomAccessFile, making sure that only bytes from a certain
  406. * range can be read.
  407. */
  408. private class BoundedInputStream extends InputStream {
  409. private long remaining;
  410. private long loc;
  411. private boolean addDummyByte = false;
  412. BoundedInputStream(long start, long remaining) {
  413. this.remaining = remaining;
  414. loc = start;
  415. }
  416. public int read() throws IOException {
  417. if (remaining-- <= 0) {
  418. if (addDummyByte) {
  419. addDummyByte = false;
  420. return 0;
  421. }
  422. return -1;
  423. }
  424. synchronized (archive) {
  425. archive.seek(loc++);
  426. return archive.read();
  427. }
  428. }
  429. public int read(byte[] b, int off, int len) throws IOException {
  430. if (remaining <= 0) {
  431. if (addDummyByte) {
  432. addDummyByte = false;
  433. b[off] = 0;
  434. return 1;
  435. }
  436. return -1;
  437. }
  438. if (len <= 0) {
  439. return 0;
  440. }
  441. if (len > remaining) {
  442. len = (int) remaining;
  443. }
  444. int ret = -1;
  445. synchronized (archive) {
  446. archive.seek(loc);
  447. ret = archive.read(b, off, len);
  448. }
  449. if (ret > 0) {
  450. loc += ret;
  451. remaining -= ret;
  452. }
  453. return ret;
  454. }
  455. /**
  456. * Inflater needs an extra dummy byte for nowrap - see
  457. * Inflater's javadocs.
  458. */
  459. void addDummy() {
  460. addDummyByte = true;
  461. }
  462. }
  463. }