From: jprocter 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 aZipShort
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/*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 aso
+ * @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 forjava.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:
+ *
+ *
org.apache.tools.zip.ZipEntry
instances.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 - ornull
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 anint
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 theDate
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