You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

ZipFile.java 19 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /*
  2. * Copyright 2003-2005 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. */
  57. public class ZipFile {
  58. /**
  59. * Maps ZipEntrys to Longs, recording the offsets of the local
  60. * file headers.
  61. */
  62. private Hashtable entries = new Hashtable(509);
  63. /**
  64. * Maps String to ZipEntrys, name -> actual entry.
  65. */
  66. private Hashtable nameMap = new Hashtable(509);
  67. private static final class OffsetEntry {
  68. long headerOffset = -1;
  69. long dataOffset = -1;
  70. }
  71. /**
  72. * The encoding to use for filenames and the file comment.
  73. *
  74. * <p>For a list of possible values see <a
  75. * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
  76. * Defaults to the platform's default character encoding.</p>
  77. */
  78. private String encoding = null;
  79. /**
  80. * The actual data source.
  81. */
  82. private RandomAccessFile archive;
  83. /**
  84. * Opens the given file for reading, assuming the platform's
  85. * native encoding for file names.
  86. *
  87. * @param f the archive.
  88. *
  89. * @throws IOException if an error occurs while reading the file.
  90. */
  91. public ZipFile(File f) throws IOException {
  92. this(f, null);
  93. }
  94. /**
  95. * Opens the given file for reading, assuming the platform's
  96. * native encoding for file names.
  97. *
  98. * @param name name of the archive.
  99. *
  100. * @throws IOException if an error occurs while reading the file.
  101. */
  102. public ZipFile(String name) throws IOException {
  103. this(new File(name), null);
  104. }
  105. /**
  106. * Opens the given file for reading, assuming the specified
  107. * encoding for file names.
  108. *
  109. * @param name name of the archive.
  110. * @param encoding the encoding to use for file names
  111. *
  112. * @throws IOException if an error occurs while reading the file.
  113. */
  114. public ZipFile(String name, String encoding) throws IOException {
  115. this(new File(name), encoding);
  116. }
  117. /**
  118. * Opens the given file for reading, assuming the specified
  119. * encoding for file names.
  120. *
  121. * @param f the archive.
  122. * @param encoding the encoding to use for file names
  123. *
  124. * @throws IOException if an error occurs while reading the file.
  125. */
  126. public ZipFile(File f, String encoding) throws IOException {
  127. this.encoding = encoding;
  128. archive = new RandomAccessFile(f, "r");
  129. try {
  130. populateFromCentralDirectory();
  131. resolveLocalFileHeaderData();
  132. } catch (IOException e) {
  133. try {
  134. archive.close();
  135. } catch (IOException e2) {
  136. // swallow, throw the original exception instead
  137. }
  138. throw e;
  139. }
  140. }
  141. /**
  142. * The encoding to use for filenames and the file comment.
  143. *
  144. * @return null if using the platform's default character encoding.
  145. */
  146. public String getEncoding() {
  147. return encoding;
  148. }
  149. /**
  150. * Closes the archive.
  151. * @throws IOException if an error occurs closing the archive.
  152. */
  153. public void close() throws IOException {
  154. archive.close();
  155. }
  156. /**
  157. * close a zipfile quietly; throw no io fault, do nothing
  158. * on a null parameter
  159. * @param zipfile file to close, can be null
  160. */
  161. public static void closeQuietly(ZipFile zipfile) {
  162. if (zipfile != null) {
  163. try {
  164. zipfile.close();
  165. } catch (IOException e) {
  166. //ignore
  167. }
  168. }
  169. }
  170. /**
  171. * Returns all entries.
  172. * @return all entries as {@link ZipEntry} instances
  173. */
  174. public Enumeration getEntries() {
  175. return entries.keys();
  176. }
  177. /**
  178. * Returns a named entry - or <code>null</code> if no entry by
  179. * that name exists.
  180. * @param name name of the entry.
  181. * @return the ZipEntry corresponding to the given name - or
  182. * <code>null</code> if not present.
  183. */
  184. public ZipEntry getEntry(String name) {
  185. return (ZipEntry) nameMap.get(name);
  186. }
  187. /**
  188. * Returns an InputStream for reading the contents of the given entry.
  189. * @param ze the entry to get the stream for.
  190. * @return a stream to read the entry from.
  191. * @throws IOException if unable to create an input stream from the zipenty
  192. * @throws ZipException if the zipentry has an unsupported compression method
  193. */
  194. public InputStream getInputStream(ZipEntry ze)
  195. throws IOException, ZipException {
  196. OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
  197. if (offsetEntry == null) {
  198. return null;
  199. }
  200. long start = offsetEntry.dataOffset;
  201. BoundedInputStream bis =
  202. new BoundedInputStream(start, ze.getCompressedSize());
  203. switch (ze.getMethod()) {
  204. case ZipEntry.STORED:
  205. return bis;
  206. case ZipEntry.DEFLATED:
  207. bis.addDummy();
  208. return new InflaterInputStream(bis, new Inflater(true));
  209. default:
  210. throw new ZipException("Found unsupported compression method "
  211. + ze.getMethod());
  212. }
  213. }
  214. private static final int CFH_LEN =
  215. /* version made by */ 2
  216. /* version needed to extract */ + 2
  217. /* general purpose bit flag */ + 2
  218. /* compression method */ + 2
  219. /* last mod file time */ + 2
  220. /* last mod file date */ + 2
  221. /* crc-32 */ + 4
  222. /* compressed size */ + 4
  223. /* uncompressed size */ + 4
  224. /* filename length */ + 2
  225. /* extra field length */ + 2
  226. /* file comment length */ + 2
  227. /* disk number start */ + 2
  228. /* internal file attributes */ + 2
  229. /* external file attributes */ + 4
  230. /* relative offset of local header */ + 4;
  231. /**
  232. * Reads the central directory of the given archive and populates
  233. * the internal tables with ZipEntry instances.
  234. *
  235. * <p>The ZipEntrys will know all data that can be obtained from
  236. * the central directory alone, but not the data that requires the
  237. * local file header or additional data to be read.</p>
  238. */
  239. private void populateFromCentralDirectory()
  240. throws IOException {
  241. positionAtCentralDirectory();
  242. byte[] cfh = new byte[CFH_LEN];
  243. byte[] signatureBytes = new byte[4];
  244. archive.readFully(signatureBytes);
  245. long sig = ZipLong.getValue(signatureBytes);
  246. final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);
  247. while (sig == cfhSig) {
  248. archive.readFully(cfh);
  249. int off = 0;
  250. ZipEntry ze = new ZipEntry();
  251. int versionMadeBy = ZipShort.getValue(cfh, off);
  252. off += 2;
  253. ze.setPlatform((versionMadeBy >> 8) & 0x0F);
  254. off += 4; // skip version info and general purpose byte
  255. ze.setMethod(ZipShort.getValue(cfh, off));
  256. off += 2;
  257. // FIXME this is actually not very cpu cycles friendly as we are converting from
  258. // dos to java while the underlying Sun implementation will convert
  259. // from java to dos time for internal storage...
  260. long time = dosToJavaTime(ZipLong.getValue(cfh, off));
  261. ze.setTime(time);
  262. off += 4;
  263. ze.setCrc(ZipLong.getValue(cfh, off));
  264. off += 4;
  265. ze.setCompressedSize(ZipLong.getValue(cfh, off));
  266. off += 4;
  267. ze.setSize(ZipLong.getValue(cfh, off));
  268. off += 4;
  269. int fileNameLen = ZipShort.getValue(cfh, off);
  270. off += 2;
  271. int extraLen = ZipShort.getValue(cfh, off);
  272. off += 2;
  273. int commentLen = ZipShort.getValue(cfh, off);
  274. off += 2;
  275. off += 2; // disk number
  276. ze.setInternalAttributes(ZipShort.getValue(cfh, off));
  277. off += 2;
  278. ze.setExternalAttributes(ZipLong.getValue(cfh, off));
  279. off += 4;
  280. byte[] fileName = new byte[fileNameLen];
  281. archive.readFully(fileName);
  282. ze.setName(getString(fileName));
  283. // LFH offset,
  284. OffsetEntry offset = new OffsetEntry();
  285. offset.headerOffset = ZipLong.getValue(cfh, off);
  286. // data offset will be filled later
  287. entries.put(ze, offset);
  288. nameMap.put(ze.getName(), ze);
  289. archive.skipBytes(extraLen);
  290. byte[] comment = new byte[commentLen];
  291. archive.readFully(comment);
  292. ze.setComment(getString(comment));
  293. archive.readFully(signatureBytes);
  294. sig = ZipLong.getValue(signatureBytes);
  295. }
  296. }
  297. private static final int MIN_EOCD_SIZE =
  298. /* end of central dir signature */ 4
  299. /* number of this disk */ + 2
  300. /* number of the disk with the */
  301. /* start of the central directory */ + 2
  302. /* total number of entries in */
  303. /* the central dir on this disk */ + 2
  304. /* total number of entries in */
  305. /* the central dir */ + 2
  306. /* size of the central directory */ + 4
  307. /* offset of start of central */
  308. /* directory with respect to */
  309. /* the starting disk number */ + 4
  310. /* zipfile comment length */ + 2;
  311. private static final int CFD_LOCATOR_OFFSET =
  312. /* end of central dir signature */ 4
  313. /* number of this disk */ + 2
  314. /* number of the disk with the */
  315. /* start of the central directory */ + 2
  316. /* total number of entries in */
  317. /* the central dir on this disk */ + 2
  318. /* total number of entries in */
  319. /* the central dir */ + 2
  320. /* size of the central directory */ + 4;
  321. /**
  322. * Searches for the &quot;End of central dir record&quot;, parses
  323. * it and positions the stream at the first central directory
  324. * record.
  325. */
  326. private void positionAtCentralDirectory()
  327. throws IOException {
  328. boolean found = false;
  329. long off = archive.length() - MIN_EOCD_SIZE;
  330. if (off >= 0) {
  331. archive.seek(off);
  332. byte[] sig = ZipOutputStream.EOCD_SIG;
  333. int curr = archive.read();
  334. while (curr != -1) {
  335. if (curr == sig[0]) {
  336. curr = archive.read();
  337. if (curr == sig[1]) {
  338. curr = archive.read();
  339. if (curr == sig[2]) {
  340. curr = archive.read();
  341. if (curr == sig[3]) {
  342. found = true;
  343. break;
  344. }
  345. }
  346. }
  347. }
  348. archive.seek(--off);
  349. curr = archive.read();
  350. }
  351. }
  352. if (!found) {
  353. throw new ZipException("archive is not a ZIP archive");
  354. }
  355. archive.seek(off + CFD_LOCATOR_OFFSET);
  356. byte[] cfdOffset = new byte[4];
  357. archive.readFully(cfdOffset);
  358. archive.seek(ZipLong.getValue(cfdOffset));
  359. }
  360. /**
  361. * Number of bytes in local file header up to the &quot;length of
  362. * filename&quot; entry.
  363. */
  364. private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
  365. /* local file header signature */ 4
  366. /* version needed to extract */ + 2
  367. /* general purpose bit flag */ + 2
  368. /* compression method */ + 2
  369. /* last mod file time */ + 2
  370. /* last mod file date */ + 2
  371. /* crc-32 */ + 4
  372. /* compressed size */ + 4
  373. /* uncompressed size */ + 4;
  374. /**
  375. * Walks through all recorded entries and adds the data available
  376. * from the local file header.
  377. *
  378. * <p>Also records the offsets for the data to read from the
  379. * entries.</p>
  380. */
  381. private void resolveLocalFileHeaderData()
  382. throws IOException {
  383. Enumeration e = getEntries();
  384. while (e.hasMoreElements()) {
  385. ZipEntry ze = (ZipEntry) e.nextElement();
  386. OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
  387. long offset = offsetEntry.headerOffset;
  388. archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
  389. byte[] b = new byte[2];
  390. archive.readFully(b);
  391. int fileNameLen = ZipShort.getValue(b);
  392. archive.readFully(b);
  393. int extraFieldLen = ZipShort.getValue(b);
  394. archive.skipBytes(fileNameLen);
  395. byte[] localExtraData = new byte[extraFieldLen];
  396. archive.readFully(localExtraData);
  397. ze.setExtra(localExtraData);
  398. /*dataOffsets.put(ze,
  399. new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
  400. + 2 + 2 + fileNameLen + extraFieldLen));
  401. */
  402. offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
  403. + 2 + 2 + fileNameLen + extraFieldLen;
  404. }
  405. }
  406. /**
  407. * Convert a DOS date/time field to a Date object.
  408. *
  409. * @param zipDosTime contains the stored DOS time.
  410. * @return a Date instance corresponding to the given time.
  411. */
  412. protected static Date fromDosTime(ZipLong zipDosTime) {
  413. long dosTime = zipDosTime.getValue();
  414. return new Date(dosToJavaTime(dosTime));
  415. }
  416. /*
  417. * Converts DOS time to Java time (number of milliseconds since epoch).
  418. */
  419. private static long dosToJavaTime(long dosTime) {
  420. Calendar cal = Calendar.getInstance();
  421. cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
  422. cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
  423. cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
  424. cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
  425. cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
  426. cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
  427. return cal.getTime().getTime();
  428. }
  429. /**
  430. * Retrieve a String from the given bytes using the encoding set
  431. * for this ZipFile.
  432. *
  433. * @param bytes the byte array to transform
  434. * @return String obtained by using the given encoding
  435. * @throws ZipException if the encoding cannot be recognized.
  436. */
  437. protected String getString(byte[] bytes) throws ZipException {
  438. if (encoding == null) {
  439. return new String(bytes);
  440. } else {
  441. try {
  442. return new String(bytes, encoding);
  443. } catch (UnsupportedEncodingException uee) {
  444. throw new ZipException(uee.getMessage());
  445. }
  446. }
  447. }
  448. /**
  449. * InputStream that delegates requests to the underlying
  450. * RandomAccessFile, making sure that only bytes from a certain
  451. * range can be read.
  452. */
  453. private class BoundedInputStream extends InputStream {
  454. private long remaining;
  455. private long loc;
  456. private boolean addDummyByte = false;
  457. BoundedInputStream(long start, long remaining) {
  458. this.remaining = remaining;
  459. loc = start;
  460. }
  461. public int read() throws IOException {
  462. if (remaining-- <= 0) {
  463. if (addDummyByte) {
  464. addDummyByte = false;
  465. return 0;
  466. }
  467. return -1;
  468. }
  469. synchronized (archive) {
  470. archive.seek(loc++);
  471. return archive.read();
  472. }
  473. }
  474. public int read(byte[] b, int off, int len) throws IOException {
  475. if (remaining <= 0) {
  476. if (addDummyByte) {
  477. addDummyByte = false;
  478. b[off] = 0;
  479. return 1;
  480. }
  481. return -1;
  482. }
  483. if (len <= 0) {
  484. return 0;
  485. }
  486. if (len > remaining) {
  487. len = (int) remaining;
  488. }
  489. int ret = -1;
  490. synchronized (archive) {
  491. archive.seek(loc);
  492. ret = archive.read(b, off, len);
  493. }
  494. if (ret > 0) {
  495. loc += ret;
  496. remaining -= ret;
  497. }
  498. return ret;
  499. }
  500. /**
  501. * Inflater needs an extra dummy byte for nowrap - see
  502. * Inflater's javadocs.
  503. */
  504. void addDummy() {
  505. addDummyByte = true;
  506. }
  507. }
  508. }