Author: tcurdt Date: Mon Jan 12 03:29:53 2009 New Revision: 733692 URL: http://svn.apache.org/viewvc?rev=733692&view=rev Log: applied patch by Christian Grobmeier
updated zip support https://issues.apache.org/jira/browse/SANDBOX-248 Added: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java (with props) commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java (with props) Added: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java URL: http://svn.apache.org/viewvc/commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java?rev=733692&view=auto ============================================================================== --- commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java (added) +++ commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java Mon Jan 12 03:29:53 2009 @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.apache.commons.compress.archivers.zip; + +import java.util.zip.ZipException; + +/** + * If this extra field is added as the very first extra field of the + * archive, Solaris will consider it an executable jar file. + * + * @since Ant 1.6.3 + */ +public final class JarMarker implements ZipExtraField { + + private static final ZipShort ID = new ZipShort(0xCAFE); + private static final ZipShort NULL = new ZipShort(0); + private static final byte[] NO_BYTES = new byte[0]; + private static final JarMarker DEFAULT = new JarMarker(); + + /** No-arg constructor */ + public JarMarker() { + // empty + } + + /** + * Since JarMarker is stateless we can always use the same instance. + * @return the DEFAULT jarmaker. + */ + public static JarMarker getInstance() { + return DEFAULT; + } + + /** + * The Header-ID. + * @return the header id + */ + public ZipShort getHeaderId() { + return ID; + } + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * @return 0 + */ + public ZipShort getLocalFileDataLength() { + return NULL; + } + + /** + * Length of the extra field in the central directory - without + * Header-ID or length specifier. + * @return 0 + */ + public ZipShort getCentralDirectoryLength() { + return NULL; + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * @return the data + * @since 1.1 + */ + public byte[] getLocalFileDataData() { + return NO_BYTES; + } + + /** + * The actual data to put central directory - without Header-ID or + * length specifier. + * @return the data + */ + public byte[] getCentralDirectoryData() { + return NO_BYTES; + } + + /** + * Populate data from this array as if it was in local file data. + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * + * @throws ZipException on error + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException { + if (length != 0) { + throw new ZipException("JarMarker doesn't expect any data"); + } + } +} Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Author HeadURL Id Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/JarMarker.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java URL: http://svn.apache.org/viewvc/commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java?rev=733692&view=auto ============================================================================== --- commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java (added) +++ commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java Mon Jan 12 03:29:53 2009 @@ -0,0 +1,602 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.apache.commons.compress.archivers.zip; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipException; + +/** + * Replacement for <code>java.util.ZipFile</code>. + * + * <p>This class adds support for file name encodings other than UTF-8 + * (which is required to work on ZIP files created by native zip tools + * and is able to skip a preamble like the one found in self + * extracting archives. Furthermore it returns instances of + * <code>org.apache.tools.zip.ZipEntry</code> instead of + * <code>java.util.zip.ZipEntry</code>.</p> + * + * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would + * have to reimplement all methods anyway. Like + * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the + * covers and supports compressed and uncompressed entries.</p> + * + * <p>The method signatures mimic the ones of + * <code>java.util.zip.ZipFile</code>, with a couple of exceptions: + * + * <ul> + * <li>There is no getName method.</li> + * <li>entries has been renamed to getEntries.</li> + * <li>getEntries and getEntry return + * <code>org.apache.tools.zip.ZipEntry</code> instances.</li> + * <li>close is allowed to throw IOException.</li> + * </ul> + * + */ +public class ZipFile { + private static final int HASH_SIZE = 509; + private static final int SHORT = 2; + private static final int WORD = 4; + private static final int NIBLET_MASK = 0x0f; + private static final int BYTE_SHIFT = 8; + private static final int POS_0 = 0; + private static final int POS_1 = 1; + private static final int POS_2 = 2; + private static final int POS_3 = 3; + + /** + * Maps ZipEntrys to Longs, recording the offsets of the local + * file headers. + */ + private Hashtable entries = new Hashtable(HASH_SIZE); + + /** + * Maps String to ZipEntrys, name -> actual entry. + */ + private Hashtable nameMap = new Hashtable(HASH_SIZE); + + private static final class OffsetEntry { + private long headerOffset = -1; + private long dataOffset = -1; + } + + /** + * The encoding to use for filenames and the file comment. + * + * <p>For a list of possible values see <a + * 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>. + * Defaults to the platform's default character encoding.</p> + */ + private String encoding = null; + + /** + * The actual data source. + */ + private RandomAccessFile archive; + + /** + * Opens the given file for reading, assuming the platform's + * native encoding for file names. + * + * @param f the archive. + * + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(File f) throws IOException { + this(f, null); + } + + /** + * Opens the given file for reading, assuming the platform's + * native encoding for file names. + * + * @param name name of the archive. + * + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(String name) throws IOException { + this(new File(name), null); + } + + /** + * Opens the given file for reading, assuming the specified + * encoding for file names. + * + * @param name name of the archive. + * @param encoding the encoding to use for file names + * + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(String name, String encoding) throws IOException { + this(new File(name), encoding); + } + + /** + * Opens the given file for reading, assuming the specified + * encoding for file names. + * + * @param f the archive. + * @param encoding the encoding to use for file names + * + * @throws IOException if an error occurs while reading the file. + */ + public ZipFile(File f, String encoding) throws IOException { + this.encoding = encoding; + archive = new RandomAccessFile(f, "r"); + try { + populateFromCentralDirectory(); + resolveLocalFileHeaderData(); + } catch (IOException e) { + try { + archive.close(); + } catch (IOException e2) { + // swallow, throw the original exception instead + } + throw e; + } + } + + /** + * The encoding to use for filenames and the file comment. + * + * @return null if using the platform's default character encoding. + */ + public String getEncoding() { + return encoding; + } + + /** + * Closes the archive. + * @throws IOException if an error occurs closing the archive. + */ + public void close() throws IOException { + archive.close(); + } + + /** + * close a zipfile quietly; throw no io fault, do nothing + * on a null parameter + * @param zipfile file to close, can be null + */ + public static void closeQuietly(ZipFile zipfile) { + if (zipfile != null) { + try { + zipfile.close(); + } catch (IOException e) { + //ignore + } + } + } + + /** + * Returns all entries. + * @return all entries as {...@link ZipEntry} instances + */ + public Enumeration getEntries() { + return entries.keys(); + } + + /** + * Returns a named entry - or <code>null</code> if no entry by + * that name exists. + * @param name name of the entry. + * @return the ZipEntry corresponding to the given name - or + * <code>null</code> if not present. + */ + public ZipEntry getEntry(String name) { + return (ZipEntry) nameMap.get(name); + } + + /** + * Returns an InputStream for reading the contents of the given entry. + * @param ze the entry to get the stream for. + * @return a stream to read the entry from. + * @throws IOException if unable to create an input stream from the zipenty + * @throws ZipException if the zipentry has an unsupported compression method + */ + public InputStream getInputStream(ZipEntry ze) + throws IOException, ZipException { + OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); + if (offsetEntry == null) { + return null; + } + long start = offsetEntry.dataOffset; + BoundedInputStream bis = + new BoundedInputStream(start, ze.getCompressedSize()); + switch (ze.getMethod()) { + case ZipEntry.STORED: + return bis; + case ZipEntry.DEFLATED: + bis.addDummy(); + return new InflaterInputStream(bis, new Inflater(true)); + default: + throw new ZipException("Found unsupported compression method " + + ze.getMethod()); + } + } + + private static final int CFH_LEN = + /* version made by */ SHORT + /* version needed to extract */ + SHORT + /* general purpose bit flag */ + SHORT + /* compression method */ + SHORT + /* last mod file time */ + SHORT + /* last mod file date */ + SHORT + /* crc-32 */ + WORD + /* compressed size */ + WORD + /* uncompressed size */ + WORD + /* filename length */ + SHORT + /* extra field length */ + SHORT + /* file comment length */ + SHORT + /* disk number start */ + SHORT + /* internal file attributes */ + SHORT + /* external file attributes */ + WORD + /* relative offset of local header */ + WORD; + + /** + * Reads the central directory of the given archive and populates + * the internal tables with ZipEntry instances. + * + * <p>The ZipEntrys will know all data that can be obtained from + * the central directory alone, but not the data that requires the + * local file header or additional data to be read.</p> + */ + private void populateFromCentralDirectory() + throws IOException { + positionAtCentralDirectory(); + + byte[] cfh = new byte[CFH_LEN]; + + byte[] signatureBytes = new byte[WORD]; + archive.readFully(signatureBytes); + long sig = ZipLong.getValue(signatureBytes); + final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG); + if (sig != cfhSig && startsWithLocalFileHeader()) { + throw new IOException("central directory is empty, can't expand" + + " corrupt archive."); + } + while (sig == cfhSig) { + archive.readFully(cfh); + int off = 0; + ZipEntry ze = new ZipEntry(); + + int versionMadeBy = ZipShort.getValue(cfh, off); + off += SHORT; + ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); + + off += WORD; // skip version info and general purpose byte + + ze.setMethod(ZipShort.getValue(cfh, off)); + off += SHORT; + + // FIXME this is actually not very cpu cycles friendly as we are converting from + // dos to java while the underlying Sun implementation will convert + // from java to dos time for internal storage... + long time = dosToJavaTime(ZipLong.getValue(cfh, off)); + ze.setTime(time); + off += WORD; + + ze.setCrc(ZipLong.getValue(cfh, off)); + off += WORD; + + ze.setCompressedSize(ZipLong.getValue(cfh, off)); + off += WORD; + + ze.setSize(ZipLong.getValue(cfh, off)); + off += WORD; + + int fileNameLen = ZipShort.getValue(cfh, off); + off += SHORT; + + int extraLen = ZipShort.getValue(cfh, off); + off += SHORT; + + int commentLen = ZipShort.getValue(cfh, off); + off += SHORT; + + off += SHORT; // disk number + + ze.setInternalAttributes(ZipShort.getValue(cfh, off)); + off += SHORT; + + ze.setExternalAttributes(ZipLong.getValue(cfh, off)); + off += WORD; + + byte[] fileName = new byte[fileNameLen]; + archive.readFully(fileName); + ze.setName(getString(fileName)); + + + // LFH offset, + OffsetEntry offset = new OffsetEntry(); + offset.headerOffset = ZipLong.getValue(cfh, off); + // data offset will be filled later + entries.put(ze, offset); + + nameMap.put(ze.getName(), ze); + + archive.skipBytes(extraLen); + + byte[] comment = new byte[commentLen]; + archive.readFully(comment); + ze.setComment(getString(comment)); + + archive.readFully(signatureBytes); + sig = ZipLong.getValue(signatureBytes); + } + } + + private static final int MIN_EOCD_SIZE = + /* end of central dir signature */ WORD + /* number of this disk */ + SHORT + /* number of the disk with the */ + /* start of the central directory */ + SHORT + /* total number of entries in */ + /* the central dir on this disk */ + SHORT + /* total number of entries in */ + /* the central dir */ + SHORT + /* size of the central directory */ + WORD + /* offset of start of central */ + /* directory with respect to */ + /* the starting disk number */ + WORD + /* zipfile comment length */ + SHORT; + + private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE + /* maximum length of zipfile comment */ + 0xFFFF; + + private static final int CFD_LOCATOR_OFFSET = + /* end of central dir signature */ WORD + /* number of this disk */ + SHORT + /* number of the disk with the */ + /* start of the central directory */ + SHORT + /* total number of entries in */ + /* the central dir on this disk */ + SHORT + /* total number of entries in */ + /* the central dir */ + SHORT + /* size of the central directory */ + WORD; + + /** + * Searches for the "End of central dir record", parses + * it and positions the stream at the first central directory + * record. + */ + private void positionAtCentralDirectory() + throws IOException { + boolean found = false; + long off = archive.length() - MIN_EOCD_SIZE; + long stopSearching = Math.max(0L, archive.length() - MAX_EOCD_SIZE); + if (off >= 0) { + archive.seek(off); + byte[] sig = ZipOutputStream.EOCD_SIG; + int curr = archive.read(); + while (off >= stopSearching && curr != -1) { + if (curr == sig[POS_0]) { + curr = archive.read(); + if (curr == sig[POS_1]) { + curr = archive.read(); + if (curr == sig[POS_2]) { + curr = archive.read(); + if (curr == sig[POS_3]) { + found = true; + break; + } + } + } + } + archive.seek(--off); + curr = archive.read(); + } + } + if (!found) { + throw new ZipException("archive is not a ZIP archive"); + } + archive.seek(off + CFD_LOCATOR_OFFSET); + byte[] cfdOffset = new byte[WORD]; + archive.readFully(cfdOffset); + archive.seek(ZipLong.getValue(cfdOffset)); + } + + /** + * Number of bytes in local file header up to the "length of + * filename" entry. + */ + private static final long LFH_OFFSET_FOR_FILENAME_LENGTH = + /* local file header signature */ WORD + /* version needed to extract */ + SHORT + /* general purpose bit flag */ + SHORT + /* compression method */ + SHORT + /* last mod file time */ + SHORT + /* last mod file date */ + SHORT + /* crc-32 */ + WORD + /* compressed size */ + WORD + /* uncompressed size */ + WORD; + + /** + * Walks through all recorded entries and adds the data available + * from the local file header. + * + * <p>Also records the offsets for the data to read from the + * entries.</p> + */ + private void resolveLocalFileHeaderData() + throws IOException { + Enumeration e = getEntries(); + while (e.hasMoreElements()) { + ZipEntry ze = (ZipEntry) e.nextElement(); + OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); + long offset = offsetEntry.headerOffset; + archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + byte[] b = new byte[SHORT]; + archive.readFully(b); + int fileNameLen = ZipShort.getValue(b); + archive.readFully(b); + int extraFieldLen = ZipShort.getValue(b); + archive.skipBytes(fileNameLen); + byte[] localExtraData = new byte[extraFieldLen]; + archive.readFully(localExtraData); + ze.setExtra(localExtraData); + /*dataOffsets.put(ze, + new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH + + SHORT + SHORT + fileNameLen + extraFieldLen)); + */ + offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH + + SHORT + SHORT + fileNameLen + extraFieldLen; + } + } + + /** + * Convert a DOS date/time field to a Date object. + * + * @param zipDosTime contains the stored DOS time. + * @return a Date instance corresponding to the given time. + */ + protected static Date fromDosTime(ZipLong zipDosTime) { + long dosTime = zipDosTime.getValue(); + return new Date(dosToJavaTime(dosTime)); + } + + /* + * Converts DOS time to Java time (number of milliseconds since epoch). + */ + private static long dosToJavaTime(long dosTime) { + Calendar cal = Calendar.getInstance(); + // CheckStyle:MagicNumberCheck OFF - no point + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + // CheckStyle:MagicNumberCheck ON + return cal.getTime().getTime(); + } + + + /** + * Retrieve a String from the given bytes using the encoding set + * for this ZipFile. + * + * @param bytes the byte array to transform + * @return String obtained by using the given encoding + * @throws ZipException if the encoding cannot be recognized. + */ + protected String getString(byte[] bytes) throws ZipException { + if (encoding == null) { + return new String(bytes); + } else { + try { + return new String(bytes, encoding); + } catch (UnsupportedEncodingException uee) { + throw new ZipException(uee.getMessage()); + } + } + } + + /** + * Checks whether the archive starts with a LFH. If it doesn't, + * it may be an empty archive. + */ + private boolean startsWithLocalFileHeader() throws IOException { + archive.seek(0); + final byte[] start = new byte[WORD]; + archive.readFully(start); + for (int i = 0; i < start.length; i++) { + if (start[i] != ZipOutputStream.LFH_SIG[i]) { + return false; + } + } + return true; + } + + /** + * InputStream that delegates requests to the underlying + * RandomAccessFile, making sure that only bytes from a certain + * range can be read. + */ + private class BoundedInputStream extends InputStream { + private long remaining; + private long loc; + private boolean addDummyByte = false; + + BoundedInputStream(long start, long remaining) { + this.remaining = remaining; + loc = start; + } + + public int read() throws IOException { + if (remaining-- <= 0) { + if (addDummyByte) { + addDummyByte = false; + return 0; + } + return -1; + } + synchronized (archive) { + archive.seek(loc++); + return archive.read(); + } + } + + public int read(byte[] b, int off, int len) throws IOException { + if (remaining <= 0) { + if (addDummyByte) { + addDummyByte = false; + b[off] = 0; + return 1; + } + return -1; + } + + if (len <= 0) { + return 0; + } + + if (len > remaining) { + len = (int) remaining; + } + int ret = -1; + synchronized (archive) { + archive.seek(loc); + ret = archive.read(b, off, len); + } + if (ret > 0) { + loc += ret; + remaining -= ret; + } + return ret; + } + + /** + * Inflater needs an extra dummy byte for nowrap - see + * Inflater's javadocs. + */ + void addDummy() { + addDummyByte = true; + } + } + +} Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Author HeadURL Id Propchange: commons/sandbox/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java ------------------------------------------------------------------------------ svn:mime-type = text/plain
