imported from ant 1.7 source distribution
authorjprocter <jprocter@compbio.dundee.ac.uk>
Thu, 27 Sep 2007 13:02:31 +0000 (13:02 +0000)
committerjprocter <jprocter@compbio.dundee.ac.uk>
Thu, 27 Sep 2007 13:02:31 +0000 (13:02 +0000)
git-svn-id: https://svn.lifesci.dundee.ac.uk/svn/repository/trunk@478 be28352e-c001-0410-b1a7-c7978e42abec

src/org/apache/tools/zip/AsiExtraField.java [new file with mode: 0644]
src/org/apache/tools/zip/ExtraFieldUtils.java [new file with mode: 0644]
src/org/apache/tools/zip/JarMarker.java [new file with mode: 0644]
src/org/apache/tools/zip/UnixStat.java [new file with mode: 0644]
src/org/apache/tools/zip/UnrecognizedExtraField.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipEntry.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipExtraField.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipFile.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipLong.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipOutputStream.java [new file with mode: 0644]
src/org/apache/tools/zip/ZipShort.java [new file with mode: 0644]

diff --git a/src/org/apache/tools/zip/AsiExtraField.java b/src/org/apache/tools/zip/AsiExtraField.java
new file mode 100644 (file)
index 0000000..8d59670
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ *  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.tools.zip;
+
+import java.util.zip.CRC32;
+import java.util.zip.ZipException;
+
+/**
+ * Adds Unix file permission and UID/GID fields as well as symbolic
+ * link handling.
+ *
+ * <p>This class uses the ASi extra field in the format:
+ * <pre>
+ *         Value         Size            Description
+ *         -----         ----            -----------
+ * (Unix3) 0x756e        Short           tag for this extra block type
+ *         TSize         Short           total data size for this block
+ *         CRC           Long            CRC-32 of the remaining data
+ *         Mode          Short           file permissions
+ *         SizDev        Long            symlink'd size OR major/minor dev num
+ *         UID           Short           user ID
+ *         GID           Short           group ID
+ *         (var.)        variable        symbolic link filename
+ * </pre>
+ * taken from appnote.iz (Info-ZIP note, 981119) found at <a
+ * href="ftp://ftp.uu.net/pub/archiving/zip/doc/">ftp://ftp.uu.net/pub/archiving/zip/doc/</a></p>
+
+ *
+ * <p>Short is two bytes and Long is four bytes in big endian byte and
+ * word order, device numbers are currently not supported.</p>
+ *
+ */
+public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable {
+
+    private static final ZipShort HEADER_ID = new ZipShort(0x756E);
+
+    /**
+     * Standard Unix stat(2) file mode.
+     *
+     * @since 1.1
+     */
+    private int mode = 0;
+    /**
+     * User ID.
+     *
+     * @since 1.1
+     */
+    private int uid = 0;
+    /**
+     * Group ID.
+     *
+     * @since 1.1
+     */
+    private int gid = 0;
+    /**
+     * File this entry points to, if it is a symbolic link.
+     *
+     * <p>empty string - if entry is not a symbolic link.</p>
+     *
+     * @since 1.1
+     */
+    private String link = "";
+    /**
+     * Is this an entry for a directory?
+     *
+     * @since 1.1
+     */
+    private boolean dirFlag = false;
+
+    /**
+     * Instance used to calculate checksums.
+     *
+     * @since 1.1
+     */
+    private CRC32 crc = new CRC32();
+
+    /** Constructor for AsiExtraField. */
+    public AsiExtraField() {
+    }
+
+    /**
+     * The Header-ID.
+     * @return the value for the header id for this extrafield
+     * @since 1.1
+     */
+    public ZipShort getHeaderId() {
+        return HEADER_ID;
+    }
+
+    /**
+     * Length of the extra field in the local file data - without
+     * Header-ID or length specifier.
+     * @return a <code>ZipShort</code> for the length of the data of this extra field
+     * @since 1.1
+     */
+    public ZipShort getLocalFileDataLength() {
+        return new ZipShort(4         // CRC
+                          + 2         // Mode
+                          + 4         // SizDev
+                          + 2         // UID
+                          + 2         // GID
+                          + getLinkedFile().getBytes().length);
+    }
+
+    /**
+     * Delegate to local file data.
+     * @return the centralDirectory length
+     * @since 1.1
+     */
+    public ZipShort getCentralDirectoryLength() {
+        return getLocalFileDataLength();
+    }
+
+    /**
+     * The actual data to put into local file data - without Header-ID
+     * or length specifier.
+     * @return get the data
+     * @since 1.1
+     */
+    public byte[] getLocalFileDataData() {
+        // CRC will be added later
+        byte[] data = new byte[getLocalFileDataLength().getValue() - 4];
+        System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2);
+
+        byte[] linkArray = getLinkedFile().getBytes();
+        System.arraycopy(ZipLong.getBytes(linkArray.length),
+                         0, data, 2, 4);
+
+        System.arraycopy(ZipShort.getBytes(getUserId()),
+                         0, data, 6, 2);
+        System.arraycopy(ZipShort.getBytes(getGroupId()),
+                         0, data, 8, 2);
+
+        System.arraycopy(linkArray, 0, data, 10, linkArray.length);
+
+        crc.reset();
+        crc.update(data);
+        long checksum = crc.getValue();
+
+        byte[] result = new byte[data.length + 4];
+        System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, 4);
+        System.arraycopy(data, 0, result, 4, data.length);
+        return result;
+    }
+
+    /**
+     * Delegate to local file data.
+     * @return the local file data
+     * @since 1.1
+     */
+    public byte[] getCentralDirectoryData() {
+        return getLocalFileDataData();
+    }
+
+    /**
+     * Set the user id.
+     * @param uid the user id
+     * @since 1.1
+     */
+    public void setUserId(int uid) {
+        this.uid = uid;
+    }
+
+    /**
+     * Get the user id.
+     * @return the user id
+     * @since 1.1
+     */
+    public int getUserId() {
+        return uid;
+    }
+
+    /**
+     * Set the group id.
+     * @param gid the group id
+     * @since 1.1
+     */
+    public void setGroupId(int gid) {
+        this.gid = gid;
+    }
+
+    /**
+     * Get the group id.
+     * @return the group id
+     * @since 1.1
+     */
+    public int getGroupId() {
+        return gid;
+    }
+
+    /**
+     * Indicate that this entry is a symbolic link to the given filename.
+     *
+     * @param name Name of the file this entry links to, empty String
+     *             if it is not a symbolic link.
+     *
+     * @since 1.1
+     */
+    public void setLinkedFile(String name) {
+        link = name;
+        mode = getMode(mode);
+    }
+
+    /**
+     * Name of linked file
+     *
+     * @return name of the file this entry links to if it is a
+     *         symbolic link, the empty string otherwise.
+     *
+     * @since 1.1
+     */
+    public String getLinkedFile() {
+        return link;
+    }
+
+    /**
+     * Is this entry a symbolic link?
+     * @return true if this is a symbolic link
+     * @since 1.1
+     */
+    public boolean isLink() {
+        return getLinkedFile().length() != 0;
+    }
+
+    /**
+     * File mode of this file.
+     * @param mode the file mode
+     * @since 1.1
+     */
+    public void setMode(int mode) {
+        this.mode = getMode(mode);
+    }
+
+    /**
+     * File mode of this file.
+     * @return the file mode
+     * @since 1.1
+     */
+    public int getMode() {
+        return mode;
+    }
+
+    /**
+     * Indicate whether this entry is a directory.
+     * @param dirFlag if true, this entry is a directory
+     * @since 1.1
+     */
+    public void setDirectory(boolean dirFlag) {
+        this.dirFlag = dirFlag;
+        mode = getMode(mode);
+    }
+
+    /**
+     * Is this entry a directory?
+     * @return true if this entry is a directory
+     * @since 1.1
+     */
+    public boolean isDirectory() {
+        return dirFlag && !isLink();
+    }
+
+    /**
+     * 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
+     * @since 1.1
+     * @throws ZipException on error
+     */
+    public void parseFromLocalFileData(byte[] data, int offset, int length)
+        throws ZipException {
+
+        long givenChecksum = ZipLong.getValue(data, offset);
+        byte[] tmp = new byte[length - 4];
+        System.arraycopy(data, offset + 4, tmp, 0, length - 4);
+        crc.reset();
+        crc.update(tmp);
+        long realChecksum = crc.getValue();
+        if (givenChecksum != realChecksum) {
+            throw new ZipException("bad CRC checksum "
+                                   + Long.toHexString(givenChecksum)
+                                   + " instead of "
+                                   + Long.toHexString(realChecksum));
+        }
+
+        int newMode = ZipShort.getValue(tmp, 0);
+        byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)];
+        uid = ZipShort.getValue(tmp, 6);
+        gid = ZipShort.getValue(tmp, 8);
+
+        if (linkArray.length == 0) {
+            link = "";
+        } else {
+            System.arraycopy(tmp, 10, linkArray, 0, linkArray.length);
+            link = new String(linkArray);
+        }
+        setDirectory((newMode & DIR_FLAG) != 0);
+        setMode(newMode);
+    }
+
+    /**
+     * Get the file mode for given permissions with the correct file type.
+     * @param mode the mode
+     * @return the type with the mode
+     * @since 1.1
+     */
+    protected int getMode(int mode) {
+        int type = FILE_FLAG;
+        if (isLink()) {
+            type = LINK_FLAG;
+        } else if (isDirectory()) {
+            type = DIR_FLAG;
+        }
+        return type | (mode & PERM_MASK);
+    }
+
+}
diff --git a/src/org/apache/tools/zip/ExtraFieldUtils.java b/src/org/apache/tools/zip/ExtraFieldUtils.java
new file mode 100644 (file)
index 0000000..e9811f4
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ *  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.tools.zip;
+
+import java.util.Hashtable;
+import java.util.Vector;
+import java.util.zip.ZipException;
+
+/**
+ * ZipExtraField related methods
+ *
+ */
+public class ExtraFieldUtils {
+
+    /**
+     * Static registry of known extra fields.
+     *
+     * @since 1.1
+     */
+    private static Hashtable implementations;
+
+    static {
+        implementations = new Hashtable();
+        register(AsiExtraField.class);
+        register(JarMarker.class);
+    }
+
+    /**
+     * Register a ZipExtraField implementation.
+     *
+     * <p>The given class must have a no-arg constructor and implement
+     * the {@link ZipExtraField ZipExtraField interface}.</p>
+     * @param c the class to register
+     *
+     * @since 1.1
+     */
+    public static void register(Class c) {
+        try {
+            ZipExtraField ze = (ZipExtraField) c.newInstance();
+            implementations.put(ze.getHeaderId(), c);
+        } catch (ClassCastException cc) {
+            throw new RuntimeException(c + " doesn\'t implement ZipExtraField");
+        } catch (InstantiationException ie) {
+            throw new RuntimeException(c + " is not a concrete class");
+        } catch (IllegalAccessException ie) {
+            throw new RuntimeException(c + "\'s no-arg constructor is not public");
+        }
+    }
+
+    /**
+     * Create an instance of the approriate ExtraField, falls back to
+     * {@link UnrecognizedExtraField UnrecognizedExtraField}.
+     * @param headerId the header identifier
+     * @return an instance of the appropiate ExtraField
+     * @exception InstantiationException if unable to instantiate the class
+     * @exception IllegalAccessException if not allowed to instatiate the class
+     * @since 1.1
+     */
+    public static ZipExtraField createExtraField(ZipShort headerId)
+        throws InstantiationException, IllegalAccessException {
+        Class c = (Class) implementations.get(headerId);
+        if (c != null) {
+            return (ZipExtraField) c.newInstance();
+        }
+        UnrecognizedExtraField u = new UnrecognizedExtraField();
+        u.setHeaderId(headerId);
+        return u;
+    }
+
+    /**
+     * Split the array into ExtraFields and populate them with the
+     * give data.
+     * @param data an array of bytes
+     * @return an array of ExtraFields
+     * @since 1.1
+     * @throws ZipException on error
+     */
+    public static ZipExtraField[] parse(byte[] data) throws ZipException {
+        Vector v = new Vector();
+        int start = 0;
+        while (start <= data.length - 4) {
+            ZipShort headerId = new ZipShort(data, start);
+            int length = (new ZipShort(data, start + 2)).getValue();
+            if (start + 4 + length > data.length) {
+                throw new ZipException("data starting at " + start
+                    + " is in unknown format");
+            }
+            try {
+                ZipExtraField ze = createExtraField(headerId);
+                ze.parseFromLocalFileData(data, start + 4, length);
+                v.addElement(ze);
+            } catch (InstantiationException ie) {
+                throw new ZipException(ie.getMessage());
+            } catch (IllegalAccessException iae) {
+                throw new ZipException(iae.getMessage());
+            }
+            start += (length + 4);
+        }
+        if (start != data.length) { // array not exhausted
+            throw new ZipException("data starting at " + start
+                + " is in unknown format");
+        }
+
+        ZipExtraField[] result = new ZipExtraField[v.size()];
+        v.copyInto(result);
+        return result;
+    }
+
+    /**
+     * Merges the local file data fields of the given ZipExtraFields.
+     * @param data an array of ExtraFiles
+     * @return an array of bytes
+     * @since 1.1
+     */
+    public static byte[] mergeLocalFileDataData(ZipExtraField[] data) {
+        int sum = 4 * data.length;
+        for (int i = 0; i < data.length; i++) {
+            sum += data[i].getLocalFileDataLength().getValue();
+        }
+        byte[] result = new byte[sum];
+        int start = 0;
+        for (int i = 0; i < data.length; i++) {
+            System.arraycopy(data[i].getHeaderId().getBytes(),
+                             0, result, start, 2);
+            System.arraycopy(data[i].getLocalFileDataLength().getBytes(),
+                             0, result, start + 2, 2);
+            byte[] local = data[i].getLocalFileDataData();
+            System.arraycopy(local, 0, result, start + 4, local.length);
+            start += (local.length + 4);
+        }
+        return result;
+    }
+
+    /**
+     * Merges the central directory fields of the given ZipExtraFields.
+     * @param data an array of ExtraFields
+     * @return an array of bytes
+     * @since 1.1
+     */
+    public static byte[] mergeCentralDirectoryData(ZipExtraField[] data) {
+        int sum = 4 * data.length;
+        for (int i = 0; i < data.length; i++) {
+            sum += data[i].getCentralDirectoryLength().getValue();
+        }
+        byte[] result = new byte[sum];
+        int start = 0;
+        for (int i = 0; i < data.length; i++) {
+            System.arraycopy(data[i].getHeaderId().getBytes(),
+                             0, result, start, 2);
+            System.arraycopy(data[i].getCentralDirectoryLength().getBytes(),
+                             0, result, start + 2, 2);
+            byte[] local = data[i].getCentralDirectoryData();
+            System.arraycopy(local, 0, result, start + 4, local.length);
+            start += (local.length + 4);
+        }
+        return result;
+    }
+}
diff --git a/src/org/apache/tools/zip/JarMarker.java b/src/org/apache/tools/zip/JarMarker.java
new file mode 100644 (file)
index 0000000..c063353
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ *  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.tools.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");
+        }
+    }
+}
diff --git a/src/org/apache/tools/zip/UnixStat.java b/src/org/apache/tools/zip/UnixStat.java
new file mode 100644 (file)
index 0000000..3509ec4
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ *  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.tools.zip;
+
+/**
+ * Constants from stat.h on Unix systems.
+ *
+ */
+public interface UnixStat {
+
+    /**
+     * Bits used for permissions (and sticky bit)
+     *
+     * @since 1.1
+     */
+    int PERM_MASK =           07777;
+    /**
+     * Indicates symbolic links.
+     *
+     * @since 1.1
+     */
+    int LINK_FLAG =         0120000;
+    /**
+     * Indicates plain files.
+     *
+     * @since 1.1
+     */
+    int FILE_FLAG =         0100000;
+    /**
+     * Indicates directories.
+     *
+     * @since 1.1
+     */
+    int DIR_FLAG =           040000;
+
+    // ----------------------------------------------------------
+    // somewhat arbitrary choices that are quite common for shared
+    // installations
+    // -----------------------------------------------------------
+
+    /**
+     * Default permissions for symbolic links.
+     *
+     * @since 1.1
+     */
+    int DEFAULT_LINK_PERM =    0777;
+    /**
+     * Default permissions for directories.
+     *
+     * @since 1.1
+     */
+    int DEFAULT_DIR_PERM =     0755;
+    /**
+     * Default permissions for plain files.
+     *
+     * @since 1.1
+     */
+    int DEFAULT_FILE_PERM =    0644;
+}
diff --git a/src/org/apache/tools/zip/UnrecognizedExtraField.java b/src/org/apache/tools/zip/UnrecognizedExtraField.java
new file mode 100644 (file)
index 0000000..79f2e6e
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ *  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.tools.zip;
+
+/**
+ * Simple placeholder for all those extra fields we don't want to deal
+ * with.
+ *
+ * <p>Assumes local file data and central directory entries are
+ * identical - unless told the opposite.</p>
+ *
+ */
+public class UnrecognizedExtraField implements ZipExtraField {
+
+    /**
+     * The Header-ID.
+     *
+     * @since 1.1
+     */
+    private ZipShort headerId;
+
+    /**
+     * Set the header id.
+     * @param headerId the header id to use
+     */
+    public void setHeaderId(ZipShort headerId) {
+        this.headerId = headerId;
+    }
+
+    /**
+     * Get the header id.
+     * @return the header id
+     */
+    public ZipShort getHeaderId() {
+        return headerId;
+    }
+
+    /**
+     * Extra field data in local file data - without
+     * Header-ID or length specifier.
+     *
+     * @since 1.1
+     */
+    private byte[] localData;
+
+    /**
+     * Set the extra field data in the local file data -
+     * without Header-ID or length specifier.
+     * @param data the field data to use
+     */
+    public void setLocalFileDataData(byte[] data) {
+        localData = data;
+    }
+
+    /**
+     * Get the length of the local data.
+     * @return the length of the local data
+     */
+    public ZipShort getLocalFileDataLength() {
+        return new ZipShort(localData.length);
+    }
+
+    /**
+     * Get the local data.
+     * @return the local data
+     */
+    public byte[] getLocalFileDataData() {
+        return localData;
+    }
+
+    /**
+     * Extra field data in central directory - without
+     * Header-ID or length specifier.
+     *
+     * @since 1.1
+     */
+    private byte[] centralData;
+
+    /**
+     * Set the extra field data in central directory.
+     * @param data the data to use
+     */
+    public void setCentralDirectoryData(byte[] data) {
+        centralData = data;
+    }
+
+    /**
+     * Get the central data length.
+     * If there is no central data, get the local file data length.
+     * @return the central data length
+     */
+    public ZipShort getCentralDirectoryLength() {
+        if (centralData != null) {
+            return new ZipShort(centralData.length);
+        }
+        return getLocalFileDataLength();
+    }
+
+    /**
+     * Get the central data.
+     * @return the central data if present, else return the local file data
+     */
+    public byte[] getCentralDirectoryData() {
+        if (centralData != null) {
+            return centralData;
+        }
+        return getLocalFileDataData();
+    }
+
+    /**
+     * @param data the array of bytes.
+     * @param offset the source location in the data array.
+     * @param length the number of bytes to use in the data array.
+     * @see ZipExtraField#parseFromLocalFileData(byte[], int, int)
+     */
+    public void parseFromLocalFileData(byte[] data, int offset, int length) {
+        byte[] tmp = new byte[length];
+        System.arraycopy(data, offset, tmp, 0, length);
+        setLocalFileDataData(tmp);
+    }
+}
diff --git a/src/org/apache/tools/zip/ZipEntry.java b/src/org/apache/tools/zip/ZipEntry.java
new file mode 100644 (file)
index 0000000..fc43e02
--- /dev/null
@@ -0,0 +1,368 @@
+/*
+ *  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.tools.zip;
+
+import java.util.Vector;
+import java.util.zip.ZipException;
+
+/**
+ * Extension that adds better handling of extra fields and provides
+ * access to the internal and external file attributes.
+ *
+ */
+public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable {
+
+    private static final int PLATFORM_UNIX = 3;
+    private static final int PLATFORM_FAT  = 0;
+
+    private int internalAttributes = 0;
+    private int platform = PLATFORM_FAT;
+    private long externalAttributes = 0;
+    private Vector/*<ZipExtraField>*/ extraFields = null;
+    private String name = null;
+
+    /**
+     * Creates a new zip entry with the specified name.
+     * @param name the name of the entry
+     * @since 1.1
+     */
+    public ZipEntry(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates a new zip entry with fields taken from the specified zip entry.
+     * @param entry the entry to get fields from
+     * @since 1.1
+     * @throws ZipException on error
+     */
+    public ZipEntry(java.util.zip.ZipEntry entry) throws ZipException {
+        super(entry);
+        byte[] extra = entry.getExtra();
+        if (extra != null) {
+            setExtraFields(ExtraFieldUtils.parse(extra));
+        } else {
+            // initializes extra data to an empty byte array
+            setExtra();
+        }
+    }
+
+    /**
+     * Creates a new zip entry with fields taken from the specified zip entry.
+     * @param entry the entry to get fields from
+     * @throws ZipException on error
+     * @since 1.1
+     */
+    public ZipEntry(ZipEntry entry) throws ZipException {
+        this((java.util.zip.ZipEntry) entry);
+        setInternalAttributes(entry.getInternalAttributes());
+        setExternalAttributes(entry.getExternalAttributes());
+        setExtraFields(entry.getExtraFields());
+    }
+
+    /**
+     * @since 1.9
+     */
+    protected ZipEntry() {
+        super("");
+    }
+
+    /**
+     * Overwrite clone.
+     * @return a cloned copy of this ZipEntry
+     * @since 1.1
+     */
+    public Object clone() {
+        ZipEntry e = (ZipEntry) super.clone();
+
+        e.extraFields = extraFields != null ? (Vector) extraFields.clone() : null;
+        e.setInternalAttributes(getInternalAttributes());
+        e.setExternalAttributes(getExternalAttributes());
+        e.setExtraFields(getExtraFields());
+        return e;
+    }
+
+    /**
+     * Retrieves the internal file attributes.
+     *
+     * @return the internal file attributes
+     * @since 1.1
+     */
+    public int getInternalAttributes() {
+        return internalAttributes;
+    }
+
+    /**
+     * Sets the internal file attributes.
+     * @param value an <code>int</code> value
+     * @since 1.1
+     */
+    public void setInternalAttributes(int value) {
+        internalAttributes = value;
+    }
+
+    /**
+     * Retrieves the external file attributes.
+     * @return the external file attributes
+     * @since 1.1
+     */
+    public long getExternalAttributes() {
+        return externalAttributes;
+    }
+
+    /**
+     * Sets the external file attributes.
+     * @param value an <code>long</code> value
+     * @since 1.1
+     */
+    public void setExternalAttributes(long value) {
+        externalAttributes = value;
+    }
+
+    /**
+     * Sets Unix permissions in a way that is understood by Info-Zip's
+     * unzip command.
+     * @param mode an <code>int</code> value
+     * @since Ant 1.5.2
+     */
+    public void setUnixMode(int mode) {
+        setExternalAttributes((mode << 16)
+                              // MS-DOS read-only attribute
+                              | ((mode & 0200) == 0 ? 1 : 0)
+                              // MS-DOS directory flag
+                              | (isDirectory() ? 0x10 : 0));
+        platform = PLATFORM_UNIX;
+    }
+
+    /**
+     * Unix permission.
+     * @return the unix permissions
+     * @since Ant 1.6
+     */
+    public int getUnixMode() {
+        return (int) ((getExternalAttributes() >> 16) & 0xFFFF);
+    }
+
+    /**
+     * Platform specification to put into the &quot;version made
+     * by&quot; part of the central file header.
+     *
+     * @return 0 (MS-DOS FAT) unless {@link #setUnixMode setUnixMode}
+     * has been called, in which case 3 (Unix) will be returned.
+     *
+     * @since Ant 1.5.2
+     */
+    public int getPlatform() {
+        return platform;
+    }
+
+    /**
+     * Set the platform (UNIX or FAT).
+     * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
+     * @since 1.9
+     */
+    protected void setPlatform(int platform) {
+        this.platform = platform;
+    }
+
+    /**
+     * Replaces all currently attached extra fields with the new array.
+     * @param fields an array of extra fields
+     * @since 1.1
+     */
+    public void setExtraFields(ZipExtraField[] fields) {
+        extraFields = new Vector();
+        for (int i = 0; i < fields.length; i++) {
+            extraFields.addElement(fields[i]);
+        }
+        setExtra();
+    }
+
+    /**
+     * Retrieves extra fields.
+     * @return an array of the extra fields
+     * @since 1.1
+     */
+    public ZipExtraField[] getExtraFields() {
+        if (extraFields == null) {
+            return new ZipExtraField[0];
+        }
+        ZipExtraField[] result = new ZipExtraField[extraFields.size()];
+        extraFields.copyInto(result);
+        return result;
+    }
+
+    /**
+     * Adds an extra fields - replacing an already present extra field
+     * of the same type.
+     * @param ze an extra field
+     * @since 1.1
+     */
+    public void addExtraField(ZipExtraField ze) {
+        if (extraFields == null) {
+            extraFields = new Vector();
+        }
+        ZipShort type = ze.getHeaderId();
+        boolean done = false;
+        for (int i = 0, fieldsSize = extraFields.size(); !done && i < fieldsSize; i++) {
+            if (((ZipExtraField) extraFields.elementAt(i)).getHeaderId().equals(type)) {
+                extraFields.setElementAt(ze, i);
+                done = true;
+            }
+        }
+        if (!done) {
+            extraFields.addElement(ze);
+        }
+        setExtra();
+    }
+
+    /**
+     * Remove an extra fields.
+     * @param type the type of extra field to remove
+     * @since 1.1
+     */
+    public void removeExtraField(ZipShort type) {
+        if (extraFields == null) {
+            extraFields = new Vector();
+        }
+        boolean done = false;
+        for (int i = 0, fieldsSize = extraFields.size(); !done && i < fieldsSize; i++) {
+            if (((ZipExtraField) extraFields.elementAt(i)).getHeaderId().equals(type)) {
+                extraFields.removeElementAt(i);
+                done = true;
+            }
+        }
+        if (!done) {
+            throw new java.util.NoSuchElementException();
+        }
+        setExtra();
+    }
+
+    /**
+     * Throws an Exception if extra data cannot be parsed into extra fields.
+     * @param extra an array of bytes to be parsed into extra fields
+     * @throws RuntimeException if the bytes cannot be parsed
+     * @since 1.1
+     * @throws RuntimeException on error
+     */
+    public void setExtra(byte[] extra) throws RuntimeException {
+        try {
+            setExtraFields(ExtraFieldUtils.parse(extra));
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    /**
+     * Unfortunately {@link java.util.zip.ZipOutputStream
+     * java.util.zip.ZipOutputStream} seems to access the extra data
+     * directly, so overriding getExtra doesn't help - we need to
+     * modify super's data directly.
+     *
+     * @since 1.1
+     */
+    protected void setExtra() {
+        super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields()));
+    }
+
+    /**
+     * Retrieves the extra data for the local file data.
+     * @return the extra data for local file
+     * @since 1.1
+     */
+    public byte[] getLocalFileDataExtra() {
+        byte[] extra = getExtra();
+        return extra != null ? extra : new byte[0];
+    }
+
+    /**
+     * Retrieves the extra data for the central directory.
+     * @return the central directory extra data
+     * @since 1.1
+     */
+    public byte[] getCentralDirectoryExtra() {
+        return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields());
+    }
+
+    /**
+     * Make this class work in JDK 1.1 like a 1.2 class.
+     *
+     * <p>This either stores the size for later usage or invokes
+     * setCompressedSize via reflection.</p>
+     * @param size the size to use
+     * @deprecated since 1.7.
+     *             Use setCompressedSize directly.
+     * @since 1.2
+     */
+    public void setComprSize(long size) {
+        setCompressedSize(size);
+    }
+
+    /**
+     * Get the name of the entry.
+     * @return the entry name
+     * @since 1.9
+     */
+    public String getName() {
+        return name == null ? super.getName() : name;
+    }
+
+    /**
+     * Is this entry a directory?
+     * @return true if the entry is a directory
+     * @since 1.10
+     */
+    public boolean isDirectory() {
+        return getName().endsWith("/");
+    }
+
+    /**
+     * Set the name of the entry.
+     * @param name the name to use
+     */
+    protected void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Get the hashCode of the entry.
+     * This uses the name as the hashcode.
+     * @return a hashcode.
+     * @since Ant 1.7
+     */
+    public int hashCode() {
+        // this method has severe consequences on performance. We cannot rely
+        // on the super.hashCode() method since super.getName() always return
+        // the empty string in the current implemention (there's no setter)
+        // so it is basically draining the performance of a hashmap lookup
+        return getName().hashCode();
+    }
+
+    /**
+     * The equality method. In this case, the implementation returns 'this == o'
+     * which is basically the equals method of the Object class.
+     * @param o the object to compare to
+     * @return true if this object is the same as <code>o</code>
+     * @since Ant 1.7
+     */
+    public boolean equals(Object o) {
+        return (this == o);
+    }
+
+}
diff --git a/src/org/apache/tools/zip/ZipExtraField.java b/src/org/apache/tools/zip/ZipExtraField.java
new file mode 100644 (file)
index 0000000..622ff17
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ *  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.tools.zip;
+
+import java.util.zip.ZipException;
+
+/**
+ * General format of extra field data.
+ *
+ * <p>Extra fields usually appear twice per file, once in the local
+ * file data and once in the central directory.  Usually they are the
+ * same, but they don't have to be.  {@link
+ * java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will
+ * only use the local file data in both places.</p>
+ *
+ */
+public interface ZipExtraField {
+
+    /**
+     * The Header-ID.
+     * @return the header id
+     * @since 1.1
+     */
+    ZipShort getHeaderId();
+
+    /**
+     * Length of the extra field in the local file data - without
+     * Header-ID or length specifier.
+     * @return the length of the field in the local file data
+     * @since 1.1
+     */
+    ZipShort getLocalFileDataLength();
+
+    /**
+     * Length of the extra field in the central directory - without
+     * Header-ID or length specifier.
+     * @return the length of the field in the central directory
+     * @since 1.1
+     */
+    ZipShort getCentralDirectoryLength();
+
+    /**
+     * The actual data to put into local file data - without Header-ID
+     * or length specifier.
+     * @return the data
+     * @since 1.1
+     */
+    byte[] getLocalFileDataData();
+
+    /**
+     * The actual data to put central directory - without Header-ID or
+     * length specifier.
+     * @return the data
+     * @since 1.1
+     */
+    byte[] getCentralDirectoryData();
+
+    /**
+     * 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
+     *
+     * @since 1.1
+     * @throws ZipException on error
+     */
+    void parseFromLocalFileData(byte[] data, int offset, int length)
+        throws ZipException;
+}
diff --git a/src/org/apache/tools/zip/ZipFile.java b/src/org/apache/tools/zip/ZipFile.java
new file mode 100644 (file)
index 0000000..2de51aa
--- /dev/null
@@ -0,0 +1,593 @@
+/*
+ *  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.tools.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>For the VAMSAS library, a new constructor was added to pass in
+ * existing RandomAccessFile directly. This allows the ZIPped data input
+ * from a file locked under the <code>java.nio.FileChannel.Lock</code>
+ * mechanism.</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 {
+
+    /**
+     * Maps ZipEntrys to Longs, recording the offsets of the local
+     * file headers.
+     */
+    private Hashtable entries = new Hashtable(509);
+
+    /**
+     * Maps String to ZipEntrys, name -> actual entry.
+     */
+    private Hashtable nameMap = new Hashtable(509);
+
+    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(new RandomAccessFile(f, "r"), encoding);
+    }
+    /**
+     * Read an archive from the given random access file
+     * 
+     * @param rafile the archive as a readable random access file
+     * @throws IOException
+     */
+    public ZipFile(RandomAccessFile rafile) throws IOException
+    {
+      this (rafile, null);
+    }
+    /**
+     * Read an archive from the given random access file, assuming the specified
+     * encoding for file names.
+     *
+     * @param readablearchive the archive opened as a readable random access file
+     * @param encoding the encoding to use for file names
+     * 
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(RandomAccessFile readablearchive, String encoding) throws IOException
+    {
+      this.encoding = encoding;
+      archive = readablearchive;
+        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                 */ 2
+        /* version needed to extract       */ + 2
+        /* general purpose bit flag        */ + 2
+        /* compression method              */ + 2
+        /* last mod file time              */ + 2
+        /* last mod file date              */ + 2
+        /* crc-32                          */ + 4
+        /* compressed size                 */ + 4
+        /* uncompressed size               */ + 4
+        /* filename length                 */ + 2
+        /* extra field length              */ + 2
+        /* file comment length             */ + 2
+        /* disk number start               */ + 2
+        /* internal file attributes        */ + 2
+        /* external file attributes        */ + 4
+        /* relative offset of local header */ + 4;
+
+    /**
+     * 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[4];
+        archive.readFully(signatureBytes);
+        long sig = ZipLong.getValue(signatureBytes);
+        final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);
+        while (sig == cfhSig) {
+            archive.readFully(cfh);
+            int off = 0;
+            ZipEntry ze = new ZipEntry();
+
+            int versionMadeBy = ZipShort.getValue(cfh, off);
+            off += 2;
+            ze.setPlatform((versionMadeBy >> 8) & 0x0F);
+
+            off += 4; // skip version info and general purpose byte
+
+            ze.setMethod(ZipShort.getValue(cfh, off));
+            off += 2;
+
+            // 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 += 4;
+
+            ze.setCrc(ZipLong.getValue(cfh, off));
+            off += 4;
+
+            ze.setCompressedSize(ZipLong.getValue(cfh, off));
+            off += 4;
+
+            ze.setSize(ZipLong.getValue(cfh, off));
+            off += 4;
+
+            int fileNameLen = ZipShort.getValue(cfh, off);
+            off += 2;
+
+            int extraLen = ZipShort.getValue(cfh, off);
+            off += 2;
+
+            int commentLen = ZipShort.getValue(cfh, off);
+            off += 2;
+
+            off += 2; // disk number
+
+            ze.setInternalAttributes(ZipShort.getValue(cfh, off));
+            off += 2;
+
+            ze.setExternalAttributes(ZipLong.getValue(cfh, off));
+            off += 4;
+
+            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    */ 4
+        /* number of this disk             */ + 2
+        /* number of the disk with the     */
+        /* start of the central directory  */ + 2
+        /* total number of entries in      */
+        /* the central dir on this disk    */ + 2
+        /* total number of entries in      */
+        /* the central dir                 */ + 2
+        /* size of the central directory   */ + 4
+        /* offset of start of central      */
+        /* directory with respect to       */
+        /* the starting disk number        */ + 4
+        /* zipfile comment length          */ + 2;
+
+    private static final int CFD_LOCATOR_OFFSET =
+        /* end of central dir signature    */ 4
+        /* number of this disk             */ + 2
+        /* number of the disk with the     */
+        /* start of the central directory  */ + 2
+        /* total number of entries in      */
+        /* the central dir on this disk    */ + 2
+        /* total number of entries in      */
+        /* the central dir                 */ + 2
+        /* size of the central directory   */ + 4;
+
+    /**
+     * Searches for the &quot;End of central dir record&quot;, 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;
+        if (off >= 0) {
+            archive.seek(off);
+            byte[] sig = ZipOutputStream.EOCD_SIG;
+            int curr = archive.read();
+            while (curr != -1) {
+                if (curr == sig[0]) {
+                    curr = archive.read();
+                    if (curr == sig[1]) {
+                        curr = archive.read();
+                        if (curr == sig[2]) {
+                            curr = archive.read();
+                            if (curr == sig[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[4];
+        archive.readFully(cfdOffset);
+        archive.seek(ZipLong.getValue(cfdOffset));
+    }
+
+    /**
+     * Number of bytes in local file header up to the &quot;length of
+     * filename&quot; entry.
+     */
+    private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
+        /* local file header signature     */ 4
+        /* version needed to extract       */ + 2
+        /* general purpose bit flag        */ + 2
+        /* compression method              */ + 2
+        /* last mod file time              */ + 2
+        /* last mod file date              */ + 2
+        /* crc-32                          */ + 4
+        /* compressed size                 */ + 4
+        /* uncompressed size               */ + 4;
+
+    /**
+     * 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[2];
+            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
+                                     + 2 + 2 + fileNameLen + extraFieldLen));
+            */
+            offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
+                                     + 2 + 2 + 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();
+        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);
+        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());
+            }
+        }
+    }
+
+    /**
+     * 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;
+        }
+    }
+
+}
diff --git a/src/org/apache/tools/zip/ZipLong.java b/src/org/apache/tools/zip/ZipLong.java
new file mode 100644 (file)
index 0000000..5804798
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ *  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.tools.zip;
+
+/**
+ * Utility class that represents a four byte integer with conversion
+ * rules for the big endian byte order of ZIP files.
+ *
+ */
+public final class ZipLong implements Cloneable {
+
+    private long value;
+
+    /**
+     * Create instance from a number.
+     * @param value the long to store as a ZipLong
+     * @since 1.1
+     */
+    public ZipLong(long value) {
+        this.value = value;
+    }
+
+    /**
+     * Create instance from bytes.
+     * @param bytes the bytes to store as a ZipLong
+     * @since 1.1
+     */
+    public ZipLong (byte[] bytes) {
+        this(bytes, 0);
+    }
+
+    /**
+     * Create instance from the four bytes starting at offset.
+     * @param bytes the bytes to store as a ZipLong
+     * @param offset the offset to start
+     * @since 1.1
+     */
+    public ZipLong (byte[] bytes, int offset) {
+        value = ZipLong.getValue(bytes, offset);
+    }
+
+    /**
+     * Get value as four bytes in big endian byte order.
+     * @since 1.1
+     * @return value as four bytes in big endian order
+     */
+    public byte[] getBytes() {
+        return ZipLong.getBytes(value);
+    }
+
+    /**
+     * Get value as Java long.
+     * @since 1.1
+     * @return value as a long
+     */
+    public long getValue() {
+        return value;
+    }
+
+    /**
+     * Get value as four bytes in big endian byte order.
+     * @param value the value to convert
+     * @return value as four bytes in big endian byte order
+     */
+    public static byte[] getBytes(long value) {
+        byte[] result = new byte[4];
+        result[0] = (byte) ((value & 0xFF));
+        result[1] = (byte) ((value & 0xFF00) >> 8);
+        result[2] = (byte) ((value & 0xFF0000) >> 16);
+        result[3] = (byte) ((value & 0xFF000000L) >> 24);
+        return result;
+    }
+
+    /**
+     * Helper method to get the value as a Java long from four bytes starting at given array offset
+     * @param bytes the array of bytes
+     * @param offset the offset to start
+     * @return the correspondanding Java long value
+     */
+    public static long getValue(byte[] bytes, int offset) {
+        long value = (bytes[offset + 3] << 24) & 0xFF000000L;
+        value += (bytes[offset + 2] << 16) & 0xFF0000;
+        value += (bytes[offset + 1] << 8) & 0xFF00;
+        value += (bytes[offset] & 0xFF);
+        return value;
+    }
+
+    /**
+     * Helper method to get the value as a Java long from a four-byte array
+     * @param bytes the array of bytes
+     * @return the correspondanding Java long value
+     */
+    public static long getValue(byte[] bytes) {
+        return getValue(bytes, 0);
+    }
+
+    /**
+     * Override to make two instances with same value equal.
+     * @param o an object to compare
+     * @return true if the objects are equal
+     * @since 1.1
+     */
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof ZipLong)) {
+            return false;
+        }
+        return value == ((ZipLong) o).getValue();
+    }
+
+    /**
+     * Override to make two instances with same value equal.
+     * @return the value stored in the ZipLong
+     * @since 1.1
+     */
+    public int hashCode() {
+        return (int) value;
+    }
+}
diff --git a/src/org/apache/tools/zip/ZipOutputStream.java b/src/org/apache/tools/zip/ZipOutputStream.java
new file mode 100644 (file)
index 0000000..2c4c4a9
--- /dev/null
@@ -0,0 +1,900 @@
+/*
+ *  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.tools.zip;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.Vector;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+import java.util.zip.ZipException;
+
+/**
+ * Reimplementation of {@link java.util.zip.ZipOutputStream
+ * java.util.zip.ZipOutputStream} that does handle the extended
+ * functionality of this package, especially internal/external file
+ * attributes and extra fields with different layouts for local file
+ * data and central directory entries.
+ *
+ * <p>This class will try to use {@link java.io.RandomAccessFile
+ * RandomAccessFile} when you know that the output is going to go to a
+ * file.</p>
+ *
+ * <p>If RandomAccessFile cannot be used, this implementation will use
+ * a Data Descriptor to store size and CRC information for {@link
+ * #DEFLATED DEFLATED} entries, this means, you don't need to
+ * calculate them yourself.  Unfortunately this is not possible for
+ * the {@link #STORED STORED} method, here setting the CRC and
+ * uncompressed size information is required before {@link
+ * #putNextEntry putNextEntry} can be called.</p>
+ *
+ */
+public class ZipOutputStream extends FilterOutputStream {
+
+    /**
+     * Compression method for deflated entries.
+     *
+     * @since 1.1
+     */
+    public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
+
+    /**
+     * Default compression level for deflated entries.
+     *
+     * @since Ant 1.7
+     */
+    public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
+
+    /**
+     * Compression method for stored entries.
+     *
+     * @since 1.1
+     */
+    public static final int STORED = java.util.zip.ZipEntry.STORED;
+
+    /**
+     * Current entry.
+     *
+     * @since 1.1
+     */
+    private ZipEntry entry;
+
+    /**
+     * The file comment.
+     *
+     * @since 1.1
+     */
+    private String comment = "";
+
+    /**
+     * Compression level for next entry.
+     *
+     * @since 1.1
+     */
+    private int level = DEFAULT_COMPRESSION;
+
+    /**
+     * Has the compression level changed when compared to the last
+     * entry?
+     *
+     * @since 1.5
+     */
+    private boolean hasCompressionLevelChanged = false;
+
+    /**
+     * Default compression method for next entry.
+     *
+     * @since 1.1
+     */
+    private int method = java.util.zip.ZipEntry.DEFLATED;
+
+    /**
+     * List of ZipEntries written so far.
+     *
+     * @since 1.1
+     */
+    private Vector entries = new Vector();
+
+    /**
+     * CRC instance to avoid parsing DEFLATED data twice.
+     *
+     * @since 1.1
+     */
+    private CRC32 crc = new CRC32();
+
+    /**
+     * Count the bytes written to out.
+     *
+     * @since 1.1
+     */
+    private long written = 0;
+
+    /**
+     * Data for local header data
+     *
+     * @since 1.1
+     */
+    private long dataStart = 0;
+
+    /**
+     * Offset for CRC entry in the local file header data for the
+     * current entry starts here.
+     *
+     * @since 1.15
+     */
+    private long localDataStart = 0;
+
+    /**
+     * Start of central directory.
+     *
+     * @since 1.1
+     */
+    private long cdOffset = 0;
+
+    /**
+     * Length of central directory.
+     *
+     * @since 1.1
+     */
+    private long cdLength = 0;
+
+    /**
+     * Helper, a 0 as ZipShort.
+     *
+     * @since 1.1
+     */
+    private static final byte[] ZERO = {0, 0};
+
+    /**
+     * Helper, a 0 as ZipLong.
+     *
+     * @since 1.1
+     */
+    private static final byte[] LZERO = {0, 0, 0, 0};
+
+    /**
+     * Holds the offsets of the LFH starts for each entry.
+     *
+     * @since 1.1
+     */
+    private Hashtable offsets = new Hashtable();
+
+    /**
+     * 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>
+     *
+     * @since 1.3
+     */
+    private String encoding = null;
+
+    // CheckStyle:VisibilityModifier OFF - bc
+
+    /**
+     * This Deflater object is used for output.
+     *
+     * <p>This attribute is only protected to provide a level of API
+     * backwards compatibility.  This class used to extend {@link
+     * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
+     * Revision 1.13.</p>
+     *
+     * @since 1.14
+     */
+    protected Deflater def = new Deflater(level, true);
+
+    /**
+     * This buffer servers as a Deflater.
+     *
+     * <p>This attribute is only protected to provide a level of API
+     * backwards compatibility.  This class used to extend {@link
+     * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
+     * Revision 1.13.</p>
+     *
+     * @since 1.14
+     */
+    protected byte[] buf = new byte[512];
+
+    // CheckStyle:VisibilityModifier ON
+
+    /**
+     * Optional random access output.
+     *
+     * @since 1.14
+     */
+    private RandomAccessFile raf = null;
+
+    /**
+     * Creates a new ZIP OutputStream filtering the underlying stream.
+     * @param out the outputstream to zip
+     * @since 1.1
+     */
+    public ZipOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    /**
+     * Creates a new ZIP OutputStream writing to a File.  Will use
+     * random access if possible.
+     * @param file the file to zip to
+     * @since 1.14
+     * @throws IOException on error
+     */
+    public ZipOutputStream(File file) throws IOException {
+        super(null);
+
+        try {
+            raf = new RandomAccessFile(file, "rw");
+            raf.setLength(0);
+        } catch (IOException e) {
+            if (raf != null) {
+                try {
+                    raf.close();
+                } catch (IOException inner) {
+                    // ignore
+                }
+                raf = null;
+            }
+            out = new FileOutputStream(file);
+        }
+    }
+
+    /**
+     * This method indicates whether this archive is writing to a seekable stream (i.e., to a random
+     * access file).
+     *
+     * <p>For seekable streams, you don't need to calculate the CRC or
+     * uncompressed size for {@link #STORED} entries before
+     * invoking {@link #putNextEntry}.
+     * @return true if seekable
+     * @since 1.17
+     */
+    public boolean isSeekable() {
+        return raf != null;
+    }
+
+    /**
+     * 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>
+     * @param encoding the encoding value
+     * @since 1.3
+     */
+    public void setEncoding(String encoding) {
+        this.encoding = encoding;
+    }
+
+    /**
+     * The encoding to use for filenames and the file comment.
+     *
+     * @return null if using the platform's default character encoding.
+     *
+     * @since 1.3
+     */
+    public String getEncoding() {
+        return encoding;
+    }
+
+    /**
+     * Finishs writing the contents and closes this as well as the
+     * underlying stream.
+     *
+     * @since 1.1
+     * @throws IOException on error
+     */
+    public void finish() throws IOException {
+        closeEntry();
+        cdOffset = written;
+        for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) {
+            writeCentralFileHeader((ZipEntry) entries.elementAt(i));
+        }
+        cdLength = written - cdOffset;
+        writeCentralDirectoryEnd();
+        offsets.clear();
+        entries.removeAllElements();
+    }
+
+    /**
+     * Writes all necessary data for this entry.
+     *
+     * @since 1.1
+     * @throws IOException on error
+     */
+    public void closeEntry() throws IOException {
+        if (entry == null) {
+            return;
+        }
+
+        long realCrc = crc.getValue();
+        crc.reset();
+
+        if (entry.getMethod() == DEFLATED) {
+            def.finish();
+            while (!def.finished()) {
+                deflate();
+            }
+
+            entry.setSize(adjustToLong(def.getTotalIn()));
+            entry.setCompressedSize(adjustToLong(def.getTotalOut()));
+            entry.setCrc(realCrc);
+
+            def.reset();
+
+            written += entry.getCompressedSize();
+        } else if (raf == null) {
+            if (entry.getCrc() != realCrc) {
+                throw new ZipException("bad CRC checksum for entry "
+                                       + entry.getName() + ": "
+                                       + Long.toHexString(entry.getCrc())
+                                       + " instead of "
+                                       + Long.toHexString(realCrc));
+            }
+
+            if (entry.getSize() != written - dataStart) {
+                throw new ZipException("bad size for entry "
+                                       + entry.getName() + ": "
+                                       + entry.getSize()
+                                       + " instead of "
+                                       + (written - dataStart));
+            }
+        } else { /* method is STORED and we used RandomAccessFile */
+            long size = written - dataStart;
+
+            entry.setSize(size);
+            entry.setCompressedSize(size);
+            entry.setCrc(realCrc);
+        }
+
+        // If random access output, write the local file header containing
+        // the correct CRC and compressed/uncompressed sizes
+        if (raf != null) {
+            long save = raf.getFilePointer();
+
+            raf.seek(localDataStart);
+            writeOut(ZipLong.getBytes(entry.getCrc()));
+            writeOut(ZipLong.getBytes(entry.getCompressedSize()));
+            writeOut(ZipLong.getBytes(entry.getSize()));
+            raf.seek(save);
+        }
+
+        writeDataDescriptor(entry);
+        entry = null;
+    }
+
+    /**
+     * Begin writing next entry.
+     * @param ze the entry to write
+     * @since 1.1
+     * @throws IOException on error
+     */
+    public void putNextEntry(ZipEntry ze) throws IOException {
+        closeEntry();
+
+        entry = ze;
+        entries.addElement(entry);
+
+        if (entry.getMethod() == -1) { // not specified
+            entry.setMethod(method);
+        }
+
+        if (entry.getTime() == -1) { // not specified
+            entry.setTime(System.currentTimeMillis());
+        }
+
+        // Size/CRC not required if RandomAccessFile is used
+        if (entry.getMethod() == STORED && raf == null) {
+            if (entry.getSize() == -1) {
+                throw new ZipException("uncompressed size is required for"
+                                       + " STORED method when not writing to a"
+                                       + " file");
+            }
+            if (entry.getCrc() == -1) {
+                throw new ZipException("crc checksum is required for STORED"
+                                       + " method when not writing to a file");
+            }
+            entry.setCompressedSize(entry.getSize());
+        }
+
+        if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
+            def.setLevel(level);
+            hasCompressionLevelChanged = false;
+        }
+        writeLocalFileHeader(entry);
+    }
+
+    /**
+     * Set the file comment.
+     * @param comment the comment
+     * @since 1.1
+     */
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+
+    /**
+     * Sets the compression level for subsequent entries.
+     *
+     * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
+     * @param level the compression level.
+     * @throws IllegalArgumentException if an invalid compression level is specified.
+     * @since 1.1
+     */
+    public void setLevel(int level) {
+        if (level < Deflater.DEFAULT_COMPRESSION
+            || level > Deflater.BEST_COMPRESSION) {
+            throw new IllegalArgumentException(
+                "Invalid compression level: " + level);
+        }
+        hasCompressionLevelChanged = (this.level != level);
+        this.level = level;
+    }
+
+    /**
+     * Sets the default compression method for subsequent entries.
+     *
+     * <p>Default is DEFLATED.</p>
+     * @param method an <code>int</code> from java.util.zip.ZipEntry
+     * @since 1.1
+     */
+    public void setMethod(int method) {
+        this.method = method;
+    }
+
+    /**
+     * Writes bytes to ZIP entry.
+     * @param b the byte array to write
+     * @param offset the start position to write from
+     * @param length the number of bytes to write
+     * @throws IOException on error
+     */
+    public void write(byte[] b, int offset, int length) throws IOException {
+        if (entry.getMethod() == DEFLATED) {
+            if (length > 0) {
+                if (!def.finished()) {
+                    def.setInput(b, offset, length);
+                    while (!def.needsInput()) {
+                        deflate();
+                    }
+                }
+            }
+        } else {
+            writeOut(b, offset, length);
+            written += length;
+        }
+        crc.update(b, offset, length);
+    }
+
+    /**
+     * Writes a single byte to ZIP entry.
+     *
+     * <p>Delegates to the three arg method.</p>
+     * @param b the byte to write
+     * @since 1.14
+     * @throws IOException on error
+     */
+    public void write(int b) throws IOException {
+        byte[] buff = new byte[1];
+        buff[0] = (byte) (b & 0xff);
+        write(buff, 0, 1);
+    }
+
+    /**
+     * Closes this output stream and releases any system resources
+     * associated with the stream.
+     *
+     * @exception  IOException  if an I/O error occurs.
+     * @since 1.14
+     */
+    public void close() throws IOException {
+        finish();
+
+        if (raf != null) {
+            raf.close();
+        }
+        if (out != null) {
+            out.close();
+        }
+    }
+
+    /**
+     * Flushes this output stream and forces any buffered output bytes
+     * to be written out to the stream.
+     *
+     * @exception  IOException  if an I/O error occurs.
+     * @since 1.14
+     */
+    public void flush() throws IOException {
+        if (out != null) {
+            out.flush();
+        }
+    }
+
+    /*
+     * Various ZIP constants
+     */
+    /**
+     * local file header signature
+     *
+     * @since 1.1
+     */
+    protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);
+    /**
+     * data descriptor signature
+     *
+     * @since 1.1
+     */
+    protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
+    /**
+     * central file header signature
+     *
+     * @since 1.1
+     */
+    protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);
+    /**
+     * end of central dir signature
+     *
+     * @since 1.1
+     */
+    protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
+
+    /**
+     * Writes next block of compressed data to the output stream.
+     * @throws IOException on error
+     *
+     * @since 1.14
+     */
+    protected final void deflate() throws IOException {
+        int len = def.deflate(buf, 0, buf.length);
+        if (len > 0) {
+            writeOut(buf, 0, len);
+        }
+    }
+
+    /**
+     * Writes the local file header entry
+     * @param ze the entry to write
+     * @throws IOException on error
+     *
+     * @since 1.1
+     */
+    protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
+        offsets.put(ze, ZipLong.getBytes(written));
+
+        writeOut(LFH_SIG);
+        written += 4;
+
+        //store method in local variable to prevent multiple method calls
+        final int zipMethod = ze.getMethod();
+
+        // version needed to extract
+        // general purpose bit flag
+        if (zipMethod == DEFLATED && raf == null) {
+            // requires version 2 as we are going to store length info
+            // in the data descriptor
+            writeOut(ZipShort.getBytes(20));
+
+            // bit3 set to signal, we use a data descriptor
+            writeOut(ZipShort.getBytes(8));
+        } else {
+            writeOut(ZipShort.getBytes(10));
+            writeOut(ZERO);
+        }
+        written += 4;
+
+        // compression method
+        writeOut(ZipShort.getBytes(zipMethod));
+        written += 2;
+
+        // last mod. time and date
+        writeOut(toDosTime(ze.getTime()));
+        written += 4;
+
+        // CRC
+        // compressed length
+        // uncompressed length
+        localDataStart = written;
+        if (zipMethod == DEFLATED || raf != null) {
+            writeOut(LZERO);
+            writeOut(LZERO);
+            writeOut(LZERO);
+        } else {
+            writeOut(ZipLong.getBytes(ze.getCrc()));
+            writeOut(ZipLong.getBytes(ze.getSize()));
+            writeOut(ZipLong.getBytes(ze.getSize()));
+        }
+        written += 12;
+
+        // file name length
+        byte[] name = getBytes(ze.getName());
+        writeOut(ZipShort.getBytes(name.length));
+        written += 2;
+
+        // extra field length
+        byte[] extra = ze.getLocalFileDataExtra();
+        writeOut(ZipShort.getBytes(extra.length));
+        written += 2;
+
+        // file name
+        writeOut(name);
+        written += name.length;
+
+        // extra field
+        writeOut(extra);
+        written += extra.length;
+
+        dataStart = written;
+    }
+
+    /**
+     * Writes the data descriptor entry.
+     * @param ze the entry to write
+     * @throws IOException on error
+     *
+     * @since 1.1
+     */
+    protected void writeDataDescriptor(ZipEntry ze) throws IOException {
+        if (ze.getMethod() != DEFLATED || raf != null) {
+            return;
+        }
+        writeOut(DD_SIG);
+        writeOut(ZipLong.getBytes(entry.getCrc()));
+        writeOut(ZipLong.getBytes(entry.getCompressedSize()));
+        writeOut(ZipLong.getBytes(entry.getSize()));
+        written += 16;
+    }
+
+    /**
+     * Writes the central file header entry.
+     * @param ze the entry to write
+     * @throws IOException on error
+     *
+     * @since 1.1
+     */
+    protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
+        writeOut(CFH_SIG);
+        written += 4;
+
+        // version made by
+        writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
+        written += 2;
+
+        // version needed to extract
+        // general purpose bit flag
+        if (ze.getMethod() == DEFLATED && raf == null) {
+            // requires version 2 as we are going to store length info
+            // in the data descriptor
+            writeOut(ZipShort.getBytes(20));
+
+            // bit3 set to signal, we use a data descriptor
+            writeOut(ZipShort.getBytes(8));
+        } else {
+            writeOut(ZipShort.getBytes(10));
+            writeOut(ZERO);
+        }
+        written += 4;
+
+        // compression method
+        writeOut(ZipShort.getBytes(ze.getMethod()));
+        written += 2;
+
+        // last mod. time and date
+        writeOut(toDosTime(ze.getTime()));
+        written += 4;
+
+        // CRC
+        // compressed length
+        // uncompressed length
+        writeOut(ZipLong.getBytes(ze.getCrc()));
+        writeOut(ZipLong.getBytes(ze.getCompressedSize()));
+        writeOut(ZipLong.getBytes(ze.getSize()));
+        written += 12;
+
+        // file name length
+        byte[] name = getBytes(ze.getName());
+        writeOut(ZipShort.getBytes(name.length));
+        written += 2;
+
+        // extra field length
+        byte[] extra = ze.getCentralDirectoryExtra();
+        writeOut(ZipShort.getBytes(extra.length));
+        written += 2;
+
+        // file comment length
+        String comm = ze.getComment();
+        if (comm == null) {
+            comm = "";
+        }
+        byte[] commentB = getBytes(comm);
+        writeOut(ZipShort.getBytes(commentB.length));
+        written += 2;
+
+        // disk number start
+        writeOut(ZERO);
+        written += 2;
+
+        // internal file attributes
+        writeOut(ZipShort.getBytes(ze.getInternalAttributes()));
+        written += 2;
+
+        // external file attributes
+        writeOut(ZipLong.getBytes(ze.getExternalAttributes()));
+        written += 4;
+
+        // relative offset of LFH
+        writeOut((byte[]) offsets.get(ze));
+        written += 4;
+
+        // file name
+        writeOut(name);
+        written += name.length;
+
+        // extra field
+        writeOut(extra);
+        written += extra.length;
+
+        // file comment
+        writeOut(commentB);
+        written += commentB.length;
+    }
+
+    /**
+     * Writes the &quot;End of central dir record&quot;.
+     * @throws IOException on error
+     *
+     * @since 1.1
+     */
+    protected void writeCentralDirectoryEnd() throws IOException {
+        writeOut(EOCD_SIG);
+
+        // disk numbers
+        writeOut(ZERO);
+        writeOut(ZERO);
+
+        // number of entries
+        byte[] num = ZipShort.getBytes(entries.size());
+        writeOut(num);
+        writeOut(num);
+
+        // length and location of CD
+        writeOut(ZipLong.getBytes(cdLength));
+        writeOut(ZipLong.getBytes(cdOffset));
+
+        // ZIP file comment
+        byte[] data = getBytes(comment);
+        writeOut(ZipShort.getBytes(data.length));
+        writeOut(data);
+    }
+
+    /**
+     * Smallest date/time ZIP can handle.
+     *
+     * @since 1.1
+     */
+    private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);
+
+    /**
+     * Convert a Date object to a DOS date/time field.
+     * @param time the <code>Date</code> to convert
+     * @return the date as a <code>ZipLong</code>
+     * @since 1.1
+     */
+    protected static ZipLong toDosTime(Date time) {
+        return new ZipLong(toDosTime(time.getTime()));
+    }
+
+    /**
+     * Convert a Date object to a DOS date/time field.
+     *
+     * <p>Stolen from InfoZip's <code>fileio.c</code></p>
+     * @param t number of milliseconds since the epoch
+     * @return the date as a byte array
+     * @since 1.26
+     */
+    protected static byte[] toDosTime(long t) {
+        Date time = new Date(t);
+        int year = time.getYear() + 1900;
+        if (year < 1980) {
+            return DOS_TIME_MIN;
+        }
+        int month = time.getMonth() + 1;
+        long value =  ((year - 1980) << 25)
+            |         (month << 21)
+            |         (time.getDate() << 16)
+            |         (time.getHours() << 11)
+            |         (time.getMinutes() << 5)
+            |         (time.getSeconds() >> 1);
+        return ZipLong.getBytes(value);
+    }
+
+    /**
+     * Retrieve the bytes for the given String in the encoding set for
+     * this Stream.
+     * @param name the string to get bytes from
+     * @return the bytes as a byte array
+     * @throws ZipException on error
+     *
+     * @since 1.3
+     */
+    protected byte[] getBytes(String name) throws ZipException {
+        if (encoding == null) {
+            return name.getBytes();
+        } else {
+            try {
+                return name.getBytes(encoding);
+            } catch (UnsupportedEncodingException uee) {
+                throw new ZipException(uee.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Write bytes to output or random access file.
+     * @param data the byte array to write
+     * @throws IOException on error
+     *
+     * @since 1.14
+     */
+    protected final void writeOut(byte[] data) throws IOException {
+        writeOut(data, 0, data.length);
+    }
+
+    /**
+     * Write bytes to output or random access file.
+     * @param data the byte array to write
+     * @param offset the start position to write from
+     * @param length the number of bytes to write
+     * @throws IOException on error
+     *
+     * @since 1.14
+     */
+    protected final void writeOut(byte[] data, int offset, int length)
+        throws IOException {
+        if (raf != null) {
+            raf.write(data, offset, length);
+        } else {
+            out.write(data, offset, length);
+        }
+    }
+
+    /**
+     * Assumes a negative integer really is a positive integer that
+     * has wrapped around and re-creates the original value.
+     * @param i the value to treat as unsigned int.
+     * @return the unsigned int as a long.
+     * @since 1.34
+     */
+    protected static long adjustToLong(int i) {
+        if (i < 0) {
+            return 2 * ((long) Integer.MAX_VALUE) + 2 + i;
+        } else {
+            return i;
+        }
+    }
+
+}
diff --git a/src/org/apache/tools/zip/ZipShort.java b/src/org/apache/tools/zip/ZipShort.java
new file mode 100644 (file)
index 0000000..97bb03f
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ *  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.tools.zip;
+
+/**
+ * Utility class that represents a two byte integer with conversion
+ * rules for the big endian byte order of ZIP files.
+ *
+ */
+public final class ZipShort implements Cloneable {
+
+    private int value;
+
+    /**
+     * Create instance from a number.
+     * @param value the int to store as a ZipShort
+     * @since 1.1
+     */
+    public ZipShort (int value) {
+        this.value = value;
+    }
+
+    /**
+     * Create instance from bytes.
+     * @param bytes the bytes to store as a ZipShort
+     * @since 1.1
+     */
+    public ZipShort (byte[] bytes) {
+        this(bytes, 0);
+    }
+
+    /**
+     * Create instance from the two bytes starting at offset.
+     * @param bytes the bytes to store as a ZipShort
+     * @param offset the offset to start
+     * @since 1.1
+     */
+    public ZipShort (byte[] bytes, int offset) {
+        value = ZipShort.getValue(bytes, offset);
+    }
+
+    /**
+     * Get value as two bytes in big endian byte order.
+     * @return the value as a a two byte array in big endian byte order
+     * @since 1.1
+     */
+    public byte[] getBytes() {
+        byte[] result = new byte[2];
+        result[0] = (byte) (value & 0xFF);
+        result[1] = (byte) ((value & 0xFF00) >> 8);
+        return result;
+    }
+
+    /**
+     * Get value as Java int.
+     * @return value as a Java int
+     * @since 1.1
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Get value as two bytes in big endian byte order.
+     * @param value the Java int to convert to bytes
+     * @return the converted int as a byte array in big endian byte order
+     */
+    public static byte[] getBytes(int value) {
+        byte[] result = new byte[2];
+        result[0] = (byte) (value & 0xFF);
+        result[1] = (byte) ((value & 0xFF00) >> 8);
+        return result;
+    }
+
+    /**
+     * Helper method to get the value as a java int from two bytes starting at given array offset
+     * @param bytes the array of bytes
+     * @param offset the offset to start
+     * @return the correspondanding java int value
+     */
+    public static int getValue(byte[] bytes, int offset) {
+        int value = (bytes[offset + 1] << 8) & 0xFF00;
+        value += (bytes[offset] & 0xFF);
+        return value;
+    }
+
+    /**
+     * Helper method to get the value as a java int from a two-byte array
+     * @param bytes the array of bytes
+     * @return the correspondanding java int value
+     */
+    public static int getValue(byte[] bytes) {
+        return getValue(bytes, 0);
+    }
+
+    /**
+     * Override to make two instances with same value equal.
+     * @param o an object to compare
+     * @return true if the objects are equal
+     * @since 1.1
+     */
+    public boolean equals(Object o) {
+        if (o == null || !(o instanceof ZipShort)) {
+            return false;
+        }
+        return value == ((ZipShort) o).getValue();
+    }
+
+    /**
+     * Override to make two instances with same value equal.
+     * @return the value stored in the ZipShort
+     * @since 1.1
+     */
+    public int hashCode() {
+        return value;
+    }
+}