From 7b1c4a0d95b3d019f16f5752667e8918d9ab775b Mon Sep 17 00:00:00 2001 From: jprocter Date: Thu, 27 Sep 2007 13:02:31 +0000 Subject: [PATCH] imported from ant 1.7 source distribution 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 | 333 ++++++++ src/org/apache/tools/zip/ExtraFieldUtils.java | 174 ++++ src/org/apache/tools/zip/JarMarker.java | 108 +++ src/org/apache/tools/zip/UnixStat.java | 75 ++ .../apache/tools/zip/UnrecognizedExtraField.java | 137 +++ src/org/apache/tools/zip/ZipEntry.java | 368 ++++++++ src/org/apache/tools/zip/ZipExtraField.java | 85 ++ src/org/apache/tools/zip/ZipFile.java | 593 +++++++++++++ src/org/apache/tools/zip/ZipLong.java | 134 +++ src/org/apache/tools/zip/ZipOutputStream.java | 900 ++++++++++++++++++++ src/org/apache/tools/zip/ZipShort.java | 133 +++ 11 files changed, 3040 insertions(+) create mode 100644 src/org/apache/tools/zip/AsiExtraField.java create mode 100644 src/org/apache/tools/zip/ExtraFieldUtils.java create mode 100644 src/org/apache/tools/zip/JarMarker.java create mode 100644 src/org/apache/tools/zip/UnixStat.java create mode 100644 src/org/apache/tools/zip/UnrecognizedExtraField.java create mode 100644 src/org/apache/tools/zip/ZipEntry.java create mode 100644 src/org/apache/tools/zip/ZipExtraField.java create mode 100644 src/org/apache/tools/zip/ZipFile.java create mode 100644 src/org/apache/tools/zip/ZipLong.java create mode 100644 src/org/apache/tools/zip/ZipOutputStream.java create mode 100644 src/org/apache/tools/zip/ZipShort.java diff --git a/src/org/apache/tools/zip/AsiExtraField.java b/src/org/apache/tools/zip/AsiExtraField.java new file mode 100644 index 0000000..8d59670 --- /dev/null +++ b/src/org/apache/tools/zip/AsiExtraField.java @@ -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. + * + *

This class uses the ASi extra field in the format: + *

+ *         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
+ * 
+ * taken from appnote.iz (Info-ZIP note, 981119) found at ftp://ftp.uu.net/pub/archiving/zip/doc/

+ + * + *

Short is two bytes and Long is four bytes in big endian byte and + * word order, device numbers are currently not supported.

+ * + */ +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. + * + *

empty string - if entry is not a symbolic link.

+ * + * @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 ZipShort 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 index 0000000..e9811f4 --- /dev/null +++ b/src/org/apache/tools/zip/ExtraFieldUtils.java @@ -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. + * + *

The given class must have a no-arg constructor and implement + * the {@link ZipExtraField ZipExtraField interface}.

+ * @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 index 0000000..c063353 --- /dev/null +++ b/src/org/apache/tools/zip/JarMarker.java @@ -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 index 0000000..3509ec4 --- /dev/null +++ b/src/org/apache/tools/zip/UnixStat.java @@ -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 index 0000000..79f2e6e --- /dev/null +++ b/src/org/apache/tools/zip/UnrecognizedExtraField.java @@ -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. + * + *

Assumes local file data and central directory entries are + * identical - unless told the opposite.

+ * + */ +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 index 0000000..fc43e02 --- /dev/null +++ b/src/org/apache/tools/zip/ZipEntry.java @@ -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/**/ 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 int 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 long 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 int 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 "version made + * by" 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 int 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. + * + *

This either stores the size for later usage or invokes + * setCompressedSize via reflection.

+ * @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 o + * @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 index 0000000..622ff17 --- /dev/null +++ b/src/org/apache/tools/zip/ZipExtraField.java @@ -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. + * + *

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.

+ * + */ +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 index 0000000..2de51aa --- /dev/null +++ b/src/org/apache/tools/zip/ZipFile.java @@ -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 java.util.ZipFile. + * + *

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 + * org.apache.tools.zip.ZipEntry instead of + * java.util.zip.ZipEntry.

+ * + *

It doesn't extend java.util.zip.ZipFile as it would + * have to reimplement all methods anyway. Like + * java.util.ZipFile, it uses RandomAccessFile under the + * covers and supports compressed and uncompressed entries.

+ *

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 java.nio.FileChannel.Lock + * mechanism.

+ *

The method signatures mimic the ones of + * java.util.zip.ZipFile, with a couple of exceptions: + * + *

+ * + */ +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. + * + *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. + * Defaults to the platform's default character encoding.

+ */ + 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 null if no entry by + * that name exists. + * @param name name of the entry. + * @return the ZipEntry corresponding to the given name - or + * null 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. + * + *

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.

+ */ + 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 "End of central dir record", parses + * it and positions the stream at the first central directory + * record. + */ + private void positionAtCentralDirectory() + throws IOException { + boolean found = false; + long off = archive.length() - MIN_EOCD_SIZE; + 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 "length of + * filename" 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. + * + *

Also records the offsets for the data to read from the + * entries.

+ */ + 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 index 0000000..5804798 --- /dev/null +++ b/src/org/apache/tools/zip/ZipLong.java @@ -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 index 0000000..2c4c4a9 --- /dev/null +++ b/src/org/apache/tools/zip/ZipOutputStream.java @@ -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. + * + *

This class will try to use {@link java.io.RandomAccessFile + * RandomAccessFile} when you know that the output is going to go to a + * file.

+ * + *

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.

+ * + */ +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. + * + *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. + * Defaults to the platform's default character encoding.

+ * + * @since 1.3 + */ + private String encoding = null; + + // CheckStyle:VisibilityModifier OFF - bc + + /** + * This Deflater object is used for output. + * + *

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.

+ * + * @since 1.14 + */ + protected Deflater def = new Deflater(level, true); + + /** + * This buffer servers as a Deflater. + * + *

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.

+ * + * @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). + * + *

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. + * + *

For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. + * Defaults to the platform's default character encoding.

+ * @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. + * + *

Default is Deflater.DEFAULT_COMPRESSION.

+ * @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. + * + *

Default is DEFLATED.

+ * @param method an int 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. + * + *

Delegates to the three arg method.

+ * @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 "End of central dir record". + * @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 Date to convert + * @return the date as a ZipLong + * @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. + * + *

Stolen from InfoZip's fileio.c

+ * @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 index 0000000..97bb03f --- /dev/null +++ b/src/org/apache/tools/zip/ZipShort.java @@ -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; + } +} -- 1.7.10.2