001: /*
002: * Copyright 2003-2004 The Apache Software Foundation
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: *
016: */
017:
018: package net.sourceforge.groboutils.codecoverage.v2.ant.zip;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.io.RandomAccessFile;
024: import java.io.UnsupportedEncodingException;
025: import java.util.Calendar;
026: import java.util.Date;
027: import java.util.Enumeration;
028: import java.util.Hashtable;
029: import java.util.zip.Inflater;
030: import java.util.zip.InflaterInputStream;
031: import java.util.zip.ZipException;
032:
033: /**
034: * Replacement for <code>java.util.ZipFile</code>.
035: *
036: * <p>This class adds support for file name encodings other than UTF-8
037: * (which is required to work on ZIP files created by native zip tools
038: * and is able to skip a preamble like the one found in self
039: * extracting archives. Furthermore it returns instances of
040: * <code>org.apache.tools.zip.ZipEntry</code> instead of
041: * <code>java.util.zip.ZipEntry</code>.</p>
042: *
043: * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
044: * have to reimplement all methods anyway. Like
045: * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
046: * covers and supports compressed and uncompressed entries.</p>
047: *
048: * <p>The method signatures mimic the ones of
049: * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
050: *
051: * <ul>
052: * <li>There is no getName method.</li>
053: * <li>entries has been renamed to getEntries.</li>
054: * <li>getEntries and getEntry return
055: * <code>org.apache.tools.zip.ZipEntry</code> instances.</li>
056: * <li>close is allowed to throw IOException.</li>
057: * </ul>
058: *
059: * @author Stefan Bodewig
060: * @version $Revision: 1.1 $
061: */
062: public class ZipFile {
063:
064: /**
065: * Maps ZipEntrys to Longs, recording the offsets of the local
066: * file headers.
067: */
068: private Hashtable entries = new Hashtable();
069:
070: /**
071: * Maps String to ZipEntrys, name -> actual entry.
072: */
073: private Hashtable nameMap = new Hashtable();
074:
075: /**
076: * Maps ZipEntrys to Longs, recording the offsets of the actual file data.
077: */
078: private Hashtable dataOffsets = new Hashtable();
079:
080: /**
081: * The encoding to use for filenames and the file comment.
082: *
083: * <p>For a list of possible values see <a
084: * 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>.
085: * Defaults to the platform's default character encoding.</p>
086: */
087: private String encoding = null;
088:
089: /**
090: * The actual data source.
091: */
092: private RandomAccessFile archive;
093:
094: /**
095: * Opens the given file for reading, assuming the platform's
096: * native encoding for file names.
097: *
098: * @param f the archive.
099: *
100: * @throws IOException if an error occurs while reading the file.
101: */
102: public ZipFile(File f) throws IOException {
103: this (f, null);
104: }
105:
106: /**
107: * Opens the given file for reading, assuming the platform's
108: * native encoding for file names.
109: *
110: * @param name name of the archive.
111: *
112: * @throws IOException if an error occurs while reading the file.
113: */
114: public ZipFile(String name) throws IOException {
115: this (new File(name), null);
116: }
117:
118: /**
119: * Opens the given file for reading, assuming the specified
120: * encoding for file names.
121: *
122: * @param name name of 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(String name, String encoding) throws IOException {
128: this (new File(name), encoding);
129: }
130:
131: /**
132: * Opens the given file for reading, assuming the specified
133: * encoding for file names.
134: *
135: * @param f the archive.
136: * @param encoding the encoding to use for file names
137: *
138: * @throws IOException if an error occurs while reading the file.
139: */
140: public ZipFile(File f, String encoding) throws IOException {
141: this .encoding = encoding;
142: archive = new RandomAccessFile(f, "r");
143: populateFromCentralDirectory();
144: resolveLocalFileHeaderData();
145: }
146:
147: /**
148: * The encoding to use for filenames and the file comment.
149: *
150: * @return null if using the platform's default character encoding.
151: */
152: public String getEncoding() {
153: return encoding;
154: }
155:
156: /**
157: * Closes the archive.
158: * @throws IOException if an error occurs closing the archive.
159: */
160: public void close() throws IOException {
161: archive.close();
162: }
163:
164: /**
165: * Returns all entries as {@link org.apache.tools.ant.ZipEntry
166: * ZipEntry} instances.
167: * @return all entries as ZipEntry instances.
168: */
169: public Enumeration getEntries() {
170: return entries.keys();
171: }
172:
173: /**
174: * Returns a named entry - or <code>null</code> if no entry by
175: * that name exists.
176: * @param name name of the entry.
177: * @return the ZipEntry corresponding to the given name - or
178: * <code>null</code> if not present.
179: */
180: public ZipEntry getEntry(String name) {
181: return (ZipEntry) nameMap.get(name);
182: }
183:
184: /**
185: * Returns an InputStream for reading the contents of the given entry.
186: * @param ze the entry to get the stream for.
187: * @return a stream to read the entry from.
188: */
189: public InputStream getInputStream(ZipEntry ze) throws IOException,
190: ZipException {
191: Long start = (Long) dataOffsets.get(ze);
192: if (start == null) {
193: return null;
194: }
195: BoundedInputStream bis = new BoundedInputStream(start
196: .longValue(), ze.getCompressedSize());
197: switch (ze.getMethod()) {
198: case ZipEntry.STORED:
199: return bis;
200: case ZipEntry.DEFLATED:
201: bis.addDummy();
202: return new InflaterInputStream(bis, new Inflater(true));
203: default:
204: throw new ZipException(
205: "Found unsupported compression method "
206: + ze.getMethod());
207: }
208: }
209:
210: private static final int CFH_LEN =
211: /* version made by */2 +
212: /* version needed to extract */2 +
213: /* general purpose bit flag */2 +
214: /* compression method */2 +
215: /* last mod file time */2 +
216: /* last mod file date */2 +
217: /* crc-32 */4 +
218: /* compressed size */4 +
219: /* uncompressed size */4 +
220: /* filename length */2 +
221: /* extra field length */2 +
222: /* file comment length */2 +
223: /* disk number start */2 +
224: /* internal file attributes */2 +
225: /* external file attributes */4 +
226: /* relative offset of local header */4;
227:
228: /**
229: * Reads the central directory of the given archive and populates
230: * the internal tables with ZipEntry instances.
231: *
232: * <p>The ZipEntrys will know all data that can be obtained from
233: * the central directory alone, but not the data that requires the
234: * local file header or additional data to be read.</p>
235: */
236: private void populateFromCentralDirectory() throws IOException {
237: positionAtCentralDirectory();
238:
239: byte[] cfh = new byte[CFH_LEN];
240:
241: byte[] signatureBytes = new byte[4];
242: archive.readFully(signatureBytes);
243: ZipLong sig = new ZipLong(signatureBytes);
244: while (sig.equals(ZipOutputStream.CFH_SIG)) {
245: archive.readFully(cfh);
246: int off = 0;
247: ZipEntry ze = new ZipEntry();
248:
249: ZipShort versionMadeBy = new ZipShort(cfh, off);
250: off += 2;
251: ze.setPlatform((versionMadeBy.getValue() >> 8) & 0x0F);
252:
253: off += 4; // skip version info and general purpose byte
254:
255: ze.setMethod((new ZipShort(cfh, off)).getValue());
256: off += 2;
257:
258: ze.setTime(fromDosTime(new ZipLong(cfh, off)).getTime());
259: off += 4;
260:
261: ze.setCrc((new ZipLong(cfh, off)).getValue());
262: off += 4;
263:
264: ze.setCompressedSize((new ZipLong(cfh, off)).getValue());
265: off += 4;
266:
267: ze.setSize((new ZipLong(cfh, off)).getValue());
268: off += 4;
269:
270: int fileNameLen = (new ZipShort(cfh, off)).getValue();
271: off += 2;
272:
273: int extraLen = (new ZipShort(cfh, off)).getValue();
274: off += 2;
275:
276: int commentLen = (new ZipShort(cfh, off)).getValue();
277: off += 2;
278:
279: off += 2; // disk number
280:
281: ze.setInternalAttributes((new ZipShort(cfh, off))
282: .getValue());
283: off += 2;
284:
285: ze
286: .setExternalAttributes((new ZipLong(cfh, off))
287: .getValue());
288: off += 4;
289:
290: // LFH offset
291: entries.put(ze,
292: new Long((new ZipLong(cfh, off)).getValue()));
293:
294: byte[] fileName = new byte[fileNameLen];
295: archive.readFully(fileName);
296: ze.setName(getString(fileName));
297:
298: nameMap.put(ze.getName(), ze);
299:
300: archive.skipBytes(extraLen);
301:
302: byte[] comment = new byte[commentLen];
303: archive.readFully(comment);
304: ze.setComment(getString(comment));
305:
306: archive.readFully(signatureBytes);
307: sig = new ZipLong(signatureBytes);
308: }
309: }
310:
311: private static final int MIN_EOCD_SIZE =
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: /* offset of start of central */+
322: /* directory with respect to */+
323: /* the starting disk number */4 +
324: /* zipfile comment length */2;
325:
326: private static final int CFD_LOCATOR_OFFSET =
327: /* end of central dir signature */4 +
328: /* number of this disk */2 +
329: /* number of the disk with the */+
330: /* start of the central directory */2 +
331: /* total number of entries in */+
332: /* the central dir on this disk */2 +
333: /* total number of entries in */+
334: /* the central dir */2 +
335: /* size of the central directory */4;
336:
337: /**
338: * Searches for the "End of central dir record", parses
339: * it and positions the stream at the first central directory
340: * record.
341: */
342: private void positionAtCentralDirectory() throws IOException {
343: long off = archive.length() - MIN_EOCD_SIZE;
344: archive.seek(off);
345: byte[] sig = ZipOutputStream.EOCD_SIG.getBytes();
346: int curr = archive.read();
347: boolean found = false;
348: while (curr != -1) {
349: if (curr == sig[0]) {
350: curr = archive.read();
351: if (curr == sig[1]) {
352: curr = archive.read();
353: if (curr == sig[2]) {
354: curr = archive.read();
355: if (curr == sig[3]) {
356: found = true;
357: break;
358: }
359: }
360: }
361: }
362: archive.seek(--off);
363: curr = archive.read();
364: }
365: if (!found) {
366: throw new ZipException("archive is not a ZIP archive");
367: }
368: archive.seek(off + CFD_LOCATOR_OFFSET);
369: byte[] cfdOffset = new byte[4];
370: archive.readFully(cfdOffset);
371: archive.seek((new ZipLong(cfdOffset)).getValue());
372: }
373:
374: /**
375: * Number of bytes in local file header up to the "length of
376: * filename" entry.
377: */
378: private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
379: /* local file header signature */4 +
380: /* version needed to extract */2 +
381: /* general purpose bit flag */2 +
382: /* compression method */2 +
383: /* last mod file time */2 +
384: /* last mod file date */2 +
385: /* crc-32 */4 +
386: /* compressed size */4 +
387: /* uncompressed size */4;
388:
389: /**
390: * Walks through all recorded entries and adds the data available
391: * from the local file header.
392: *
393: * <p>Also records the offsets for the data to read from the
394: * entries.</p>
395: */
396: private void resolveLocalFileHeaderData() throws IOException {
397: Enumeration e = getEntries();
398: while (e.hasMoreElements()) {
399: ZipEntry ze = (ZipEntry) e.nextElement();
400: long offset = ((Long) entries.get(ze)).longValue();
401: archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
402: byte[] b = new byte[2];
403: archive.readFully(b);
404: int fileNameLen = (new ZipShort(b)).getValue();
405: archive.readFully(b);
406: int extraFieldLen = (new ZipShort(b)).getValue();
407: archive.skipBytes(fileNameLen);
408: byte[] localExtraData = new byte[extraFieldLen];
409: archive.readFully(localExtraData);
410: ze.setExtra(localExtraData);
411: dataOffsets.put(ze, new Long(offset
412: + LFH_OFFSET_FOR_FILENAME_LENGTH + 2 + 2
413: + fileNameLen + extraFieldLen));
414: }
415: }
416:
417: /**
418: * Convert a DOS date/time field to a Date object.
419: *
420: * @param l contains the stored DOS time.
421: * @return a Date instance corresponding to the given time.
422: */
423: protected static Date fromDosTime(ZipLong l) {
424: long dosTime = l.getValue();
425: Calendar cal = Calendar.getInstance();
426: cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
427: cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
428: cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
429: cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
430: cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
431: cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
432: return cal.getTime();
433: }
434:
435: /**
436: * Retrieve a String from the given bytes using the encoding set
437: * for this ZipFile.
438: *
439: * @param bytes the byte array to transform
440: * @return String obtained by using the given encoding
441: * @throws ZipException if the encoding cannot be recognized.
442: */
443: protected String getString(byte[] bytes) throws ZipException {
444: if (encoding == null) {
445: return new String(bytes);
446: } else {
447: try {
448: return new String(bytes, encoding);
449: } catch (UnsupportedEncodingException uee) {
450: throw new ZipException(uee.getMessage());
451: }
452: }
453: }
454:
455: /**
456: * InputStream that delegates requests to the underlying
457: * RandomAccessFile, making sure that only bytes from a certain
458: * range can be read.
459: */
460: private class BoundedInputStream extends InputStream {
461: private long remaining;
462: private long loc;
463: private boolean addDummyByte = false;
464:
465: BoundedInputStream(long start, long remaining) {
466: this .remaining = remaining;
467: loc = start;
468: }
469:
470: public int read() throws IOException {
471: if (remaining-- <= 0) {
472: if (addDummyByte) {
473: addDummyByte = false;
474: return 0;
475: }
476: return -1;
477: }
478: synchronized (archive) {
479: archive.seek(loc++);
480: return archive.read();
481: }
482: }
483:
484: public int read(byte[] b, int off, int len) throws IOException {
485: if (remaining <= 0) {
486: if (addDummyByte) {
487: addDummyByte = false;
488: b[off] = 0;
489: return 1;
490: }
491: return -1;
492: }
493:
494: if (len <= 0) {
495: return 0;
496: }
497:
498: if (len > remaining) {
499: len = (int) remaining;
500: }
501: int ret = -1;
502: synchronized (archive) {
503: archive.seek(loc);
504: ret = archive.read(b, off, len);
505: }
506: if (ret > 0) {
507: loc += ret;
508: remaining -= ret;
509: }
510: return ret;
511: }
512:
513: /**
514: * Inflater needs an extra dummy byte for nowrap - see
515: * Inflater's javadocs.
516: */
517: void addDummy() {
518: addDummyByte = true;
519: }
520: }
521:
522: }
|