From dee95e3acc5154aee03ff5f91f9a56426ae79f3f Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Mon, 4 Jun 2012 16:35:06 +0000 Subject: [PATCH] merge Zip64 support from Commons Compress git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@1346025 13f79535-47bb-0310-9956-ffa450edef68 --- WHATSNEW | 5 +- .../tools/zip/AbstractUnicodeExtraField.java | 27 +- .../org/apache/tools/zip/AsiExtraField.java | 9 +- .../org/apache/tools/zip/ExtraFieldUtils.java | 23 +- .../apache/tools/zip/FallbackZipEncoding.java | 8 +- .../apache/tools/zip/GeneralPurposeBit.java | 171 +++ .../tools/zip/Simple8BitZipEncoding.java | 48 +- .../tools/zip/UnicodeCommentExtraField.java | 1 + .../tools/zip/UnicodePathExtraField.java | 1 + .../tools/zip/UnparseableExtraFieldData.java | 10 +- .../zip/UnsupportedZipFeatureException.java | 89 ++ .../Zip64ExtendedInformationExtraField.java | 354 +++++++ src/main/org/apache/tools/zip/Zip64Mode.java | 47 + .../tools/zip/Zip64RequiredException.java | 49 + .../org/apache/tools/zip/ZipConstants.java | 59 ++ .../apache/tools/zip/ZipEightByteInteger.java | 229 ++++ .../org/apache/tools/zip/ZipEncoding.java | 2 +- .../apache/tools/zip/ZipEncodingHelper.java | 9 +- src/main/org/apache/tools/zip/ZipEntry.java | 264 ++++- src/main/org/apache/tools/zip/ZipFile.java | 673 ++++++++---- src/main/org/apache/tools/zip/ZipLong.java | 39 +- .../org/apache/tools/zip/ZipOutputStream.java | 996 +++++++++++++----- src/main/org/apache/tools/zip/ZipShort.java | 17 +- src/main/org/apache/tools/zip/ZipUtil.java | 193 ++++ .../tools/ant/taskdefs/ZipExtraFieldTest.java | 5 +- .../apache/tools/zip/ExtraFieldUtilsTest.java | 14 +- .../org/apache/tools/zip/ZipEntryTest.java | 20 +- 27 files changed, 2775 insertions(+), 587 deletions(-) create mode 100644 src/main/org/apache/tools/zip/GeneralPurposeBit.java create mode 100644 src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java create mode 100644 src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java create mode 100644 src/main/org/apache/tools/zip/Zip64Mode.java create mode 100644 src/main/org/apache/tools/zip/Zip64RequiredException.java create mode 100644 src/main/org/apache/tools/zip/ZipConstants.java create mode 100644 src/main/org/apache/tools/zip/ZipEightByteInteger.java diff --git a/WHATSNEW b/WHATSNEW index 3c82ecc50..830967f66 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -9,7 +9,6 @@ Changes that could break older environments: the task now really leaves the EOL characters alone. This also implies that EOL ASIS will not insert a newline even if fixlast is set to true. Bugzilla report 53036 - Fixed bugs: ----------- @@ -34,6 +33,10 @@ Fixed bugs: Other changes: -------------- +* merged the ZIP package from Commons Compress, it can now read + archives using Zip64 extensions (files and archives bigger that 4GB + and with more that 64k entries). + Changes from Ant 1.8.3 TO Ant 1.8.4 =================================== diff --git a/src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java b/src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java index 02e6e1e70..0a7eec2f7 100644 --- a/src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java +++ b/src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java @@ -105,24 +105,42 @@ public abstract class AbstractUnicodeExtraField implements ZipExtraField { * @return The utf-8 encoded name. */ public byte[] getUnicodeName() { - return unicodeName; + byte[] b = null; + if (unicodeName != null) { + b = new byte[unicodeName.length]; + System.arraycopy(unicodeName, 0, b, 0, b.length); + } + return b; } /** * @param unicodeName The utf-8 encoded name to set. */ public void setUnicodeName(byte[] unicodeName) { - this.unicodeName = unicodeName; + if (unicodeName != null) { + this.unicodeName = new byte[unicodeName.length]; + System.arraycopy(unicodeName, 0, this.unicodeName, 0, + unicodeName.length); + } else { + this.unicodeName = null; + } data = null; } + /** {@inheritDoc} */ public byte[] getCentralDirectoryData() { if (data == null) { this.assembleData(); } - return data; + byte[] b = null; + if (data != null) { + b = new byte[data.length]; + System.arraycopy(data, 0, b, 0, b.length); + } + return b; } + /** {@inheritDoc} */ public ZipShort getCentralDirectoryLength() { if (data == null) { assembleData(); @@ -130,14 +148,17 @@ public abstract class AbstractUnicodeExtraField implements ZipExtraField { return new ZipShort(data.length); } + /** {@inheritDoc} */ public byte[] getLocalFileDataData() { return getCentralDirectoryData(); } + /** {@inheritDoc} */ public ZipShort getLocalFileDataLength() { return getCentralDirectoryLength(); } + /** {@inheritDoc} */ public void parseFromLocalFileData(byte[] buffer, int offset, int length) throws ZipException { diff --git a/src/main/org/apache/tools/zip/AsiExtraField.java b/src/main/org/apache/tools/zip/AsiExtraField.java index a9634dc43..d2ca6910d 100644 --- a/src/main/org/apache/tools/zip/AsiExtraField.java +++ b/src/main/org/apache/tools/zip/AsiExtraField.java @@ -45,6 +45,9 @@ import java.util.zip.ZipException; *

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

* + *

Since the documentation this class is based upon doesn't mention + * the character encoding of the file name at all, it is assumed that + * it uses the current platform's default encoding.

*/ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { @@ -116,6 +119,7 @@ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { + 2 // UID + 2 // GID + getLinkedFile().getBytes().length); + // Uses default charset - see class Javadoc } /** @@ -138,7 +142,7 @@ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { byte[] data = new byte[getLocalFileDataLength().getValue() - WORD]; System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2); - byte[] linkArray = getLinkedFile().getBytes(); + byte[] linkArray = getLinkedFile().getBytes(); // Uses default charset - see class Javadoc // CheckStyle:MagicNumber OFF System.arraycopy(ZipLong.getBytes(linkArray.length), 0, data, 2, WORD); @@ -311,7 +315,7 @@ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { link = ""; } else { System.arraycopy(tmp, 10, linkArray, 0, linkArray.length); - link = new String(linkArray); + link = new String(linkArray); // Uses default charset - see class Javadoc } // CheckStyle:MagicNumber ON setDirectory((newMode & DIR_FLAG) != 0); @@ -334,6 +338,7 @@ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { return type | (mode & PERM_MASK); } + @Override public Object clone() { try { AsiExtraField cloned = (AsiExtraField) super.clone(); diff --git a/src/main/org/apache/tools/zip/ExtraFieldUtils.java b/src/main/org/apache/tools/zip/ExtraFieldUtils.java index ffa03b6c0..396ab4d1b 100644 --- a/src/main/org/apache/tools/zip/ExtraFieldUtils.java +++ b/src/main/org/apache/tools/zip/ExtraFieldUtils.java @@ -38,14 +38,15 @@ public class ExtraFieldUtils { * * @since 1.1 */ - private static final Map implementations; + private static final Map> implementations; static { - implementations = new HashMap(); + implementations = new HashMap>(); register(AsiExtraField.class); register(JarMarker.class); register(UnicodePathExtraField.class); register(UnicodeCommentExtraField.class); + register(Zip64ExtendedInformationExtraField.class); } /** @@ -57,7 +58,7 @@ public class ExtraFieldUtils { * * @since 1.1 */ - public static void register(Class c) { + public static void register(Class c) { try { ZipExtraField ze = (ZipExtraField) c.newInstance(); implementations.put(ze.getHeaderId(), c); @@ -81,7 +82,7 @@ public class ExtraFieldUtils { */ public static ZipExtraField createExtraField(ZipShort headerId) throws InstantiationException, IllegalAccessException { - Class c = (Class) implementations.get(headerId); + Class c = implementations.get(headerId); if (c != null) { return (ZipExtraField) c.newInstance(); } @@ -132,7 +133,7 @@ public class ExtraFieldUtils { public static ZipExtraField[] parse(byte[] data, boolean local, UnparseableExtraField onUnparseableData) throws ZipException { - List v = new ArrayList(); + List v = new ArrayList(); int start = 0; LOOP: while (start <= data.length - WORD) { @@ -158,7 +159,7 @@ public class ExtraFieldUtils { data.length - start); } v.add(field); - /*FALLTHROUGH*/ + //$FALL-THROUGH$ case UnparseableExtraField.SKIP_KEY: // since we cannot parse the data we must assume // the extra field consumes the whole rest of the @@ -189,7 +190,7 @@ public class ExtraFieldUtils { } ZipExtraField[] result = new ZipExtraField[v.size()]; - return (ZipExtraField[]) v.toArray(result); + return v.toArray(result); } /** @@ -205,8 +206,8 @@ public class ExtraFieldUtils { lastIsUnparseableHolder ? data.length - 1 : data.length; int sum = WORD * regularExtraFieldCount; - for (int i = 0; i < data.length; i++) { - sum += data[i].getLocalFileDataLength().getValue(); + for (ZipExtraField element : data) { + sum += element.getLocalFileDataLength().getValue(); } byte[] result = new byte[sum]; @@ -240,8 +241,8 @@ public class ExtraFieldUtils { lastIsUnparseableHolder ? data.length - 1 : data.length; int sum = WORD * regularExtraFieldCount; - for (int i = 0; i < data.length; i++) { - sum += data[i].getCentralDirectoryLength().getValue(); + for (ZipExtraField element : data) { + sum += element.getCentralDirectoryLength().getValue(); } byte[] result = new byte[sum]; int start = 0; diff --git a/src/main/org/apache/tools/zip/FallbackZipEncoding.java b/src/main/org/apache/tools/zip/FallbackZipEncoding.java index 5b1dc8902..facc63e70 100644 --- a/src/main/org/apache/tools/zip/FallbackZipEncoding.java +++ b/src/main/org/apache/tools/zip/FallbackZipEncoding.java @@ -30,7 +30,7 @@ import java.nio.ByteBuffer; * marks leading to unreadable ZIP entries on some operating * systems.

* - *

Furthermore this implementation is unable to tell, whether a + *

Furthermore this implementation is unable to tell whether a * given name can be safely encoded or not.

* *

This implementation acts as a last resort implementation, when @@ -53,7 +53,7 @@ class FallbackZipEncoding implements ZipEncoding { /** * Construct a fallback zip encoding, which uses the given charset. * - * @param charset The name of the charset or null for + * @param charset The name of the charset or {@code null} for * the platform's default character set. */ public FallbackZipEncoding(String charset) { @@ -73,7 +73,7 @@ class FallbackZipEncoding implements ZipEncoding { * org.apache.tools.zip.ZipEncoding#encode(java.lang.String) */ public ByteBuffer encode(String name) throws IOException { - if (this.charset == null) { + if (this.charset == null) { // i.e. use default charset, see no-args constructor return ByteBuffer.wrap(name.getBytes()); } else { return ByteBuffer.wrap(name.getBytes(this.charset)); @@ -85,7 +85,7 @@ class FallbackZipEncoding implements ZipEncoding { * org.apache.tools.zip.ZipEncoding#decode(byte[]) */ public String decode(byte[] data) throws IOException { - if (this.charset == null) { + if (this.charset == null) { // i.e. use default charset, see no-args constructor return new String(data); } else { return new String(data,this.charset); diff --git a/src/main/org/apache/tools/zip/GeneralPurposeBit.java b/src/main/org/apache/tools/zip/GeneralPurposeBit.java new file mode 100644 index 000000000..1e92103aa --- /dev/null +++ b/src/main/org/apache/tools/zip/GeneralPurposeBit.java @@ -0,0 +1,171 @@ +/* + * 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; + +/** + * Parser/encoder for the "general purpose bit" field in ZIP's local + * file and central directory headers. + * + * @since Ant 1.9.0 + */ +public final class GeneralPurposeBit { + /** + * Indicates that the file is encrypted. + */ + private static final int ENCRYPTION_FLAG = 1 << 0; + + /** + * Indicates that a data descriptor stored after the file contents + * will hold CRC and size information. + */ + private static final int DATA_DESCRIPTOR_FLAG = 1 << 3; + + /** + * Indicates strong encryption. + */ + private static final int STRONG_ENCRYPTION_FLAG = 1 << 6; + + /** + * Indicates that filenames are written in utf-8. + * + *

The only reason this is public is that {@link + * ZipOutputStream#EFS_FLAG} was public in several versions of + * Apache Ant and we needed a substitute for it.

+ */ + public static final int UFT8_NAMES_FLAG = 1 << 11; + + private boolean languageEncodingFlag = false; + private boolean dataDescriptorFlag = false; + private boolean encryptionFlag = false; + private boolean strongEncryptionFlag = false; + + public GeneralPurposeBit() { + } + + /** + * whether the current entry uses UTF8 for file name and comment. + */ + public boolean usesUTF8ForNames() { + return languageEncodingFlag; + } + + /** + * whether the current entry will use UTF8 for file name and comment. + */ + public void useUTF8ForNames(boolean b) { + languageEncodingFlag = b; + } + + /** + * whether the current entry uses the data descriptor to store CRC + * and size information + */ + public boolean usesDataDescriptor() { + return dataDescriptorFlag; + } + + /** + * whether the current entry will use the data descriptor to store + * CRC and size information + */ + public void useDataDescriptor(boolean b) { + dataDescriptorFlag = b; + } + + /** + * whether the current entry is encrypted + */ + public boolean usesEncryption() { + return encryptionFlag; + } + + /** + * whether the current entry will be encrypted + */ + public void useEncryption(boolean b) { + encryptionFlag = b; + } + + /** + * whether the current entry is encrypted using strong encryption + */ + public boolean usesStrongEncryption() { + return encryptionFlag && strongEncryptionFlag; + } + + /** + * whether the current entry will be encrypted using strong encryption + */ + public void useStrongEncryption(boolean b) { + strongEncryptionFlag = b; + if (b) { + useEncryption(true); + } + } + + /** + * Encodes the set bits in a form suitable for ZIP archives. + */ + public byte[] encode() { + return + ZipShort.getBytes((dataDescriptorFlag ? DATA_DESCRIPTOR_FLAG : 0) + | + (languageEncodingFlag ? UFT8_NAMES_FLAG : 0) + | + (encryptionFlag ? ENCRYPTION_FLAG : 0) + | + (strongEncryptionFlag ? STRONG_ENCRYPTION_FLAG : 0) + ); + } + + /** + * Parses the supported flags from the given archive data. + * @param data local file header or a central directory entry. + * @param offset offset at which the general purpose bit starts + */ + public static GeneralPurposeBit parse(final byte[] data, final int offset) { + final int generalPurposeFlag = ZipShort.getValue(data, offset); + GeneralPurposeBit b = new GeneralPurposeBit(); + b.useDataDescriptor((generalPurposeFlag & DATA_DESCRIPTOR_FLAG) != 0); + b.useUTF8ForNames((generalPurposeFlag & UFT8_NAMES_FLAG) != 0); + b.useStrongEncryption((generalPurposeFlag & STRONG_ENCRYPTION_FLAG) + != 0); + b.useEncryption((generalPurposeFlag & ENCRYPTION_FLAG) != 0); + return b; + } + + @Override + public int hashCode() { + return 3 * (7 * (13 * (17 * (encryptionFlag ? 1 : 0) + + (strongEncryptionFlag ? 1 : 0)) + + (languageEncodingFlag ? 1 : 0)) + + (dataDescriptorFlag ? 1 : 0)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof GeneralPurposeBit)) { + return false; + } + GeneralPurposeBit g = (GeneralPurposeBit) o; + return g.encryptionFlag == encryptionFlag + && g.strongEncryptionFlag == strongEncryptionFlag + && g.languageEncodingFlag == languageEncodingFlag + && g.dataDescriptorFlag == dataDescriptorFlag; + } +} diff --git a/src/main/org/apache/tools/zip/Simple8BitZipEncoding.java b/src/main/org/apache/tools/zip/Simple8BitZipEncoding.java index 20ee231ba..5bff47293 100644 --- a/src/main/org/apache/tools/zip/Simple8BitZipEncoding.java +++ b/src/main/org/apache/tools/zip/Simple8BitZipEncoding.java @@ -49,7 +49,7 @@ class Simple8BitZipEncoding implements ZipEncoding { * A character entity, which is put to the reverse mapping table * of a simple encoding. */ - private static final class Simple8BitChar implements Comparable { + private static final class Simple8BitChar implements Comparable { public final char unicode; public final byte code; @@ -58,15 +58,28 @@ class Simple8BitZipEncoding implements ZipEncoding { this.unicode = unicode; } - public int compareTo(Object o) { - Simple8BitChar a = (Simple8BitChar) o; - + public int compareTo(Simple8BitChar a) { return this.unicode - a.unicode; } + @Override public String toString() { - return "0x" + Integer.toHexString(0xffff & (int) unicode) - + "->0x" + Integer.toHexString(0xff & (int) code); + return "0x" + Integer.toHexString(0xffff & unicode) + + "->0x" + Integer.toHexString(0xff & code); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Simple8BitChar) { + Simple8BitChar other = (Simple8BitChar) o; + return unicode == other.unicode && code == other.code; + } + return false; + } + + @Override + public int hashCode() { + return unicode; } } @@ -81,24 +94,25 @@ class Simple8BitZipEncoding implements ZipEncoding { * field. This list is used to binary search reverse mapping of * unicode characters with a character code greater than 127. */ - private final List reverseMapping; + private final List reverseMapping; /** * @param highChars The characters for byte values of 128 to 255 * stored as an array of 128 chars. */ public Simple8BitZipEncoding(char[] highChars) { - this.highChars = highChars; - this.reverseMapping = new ArrayList(this.highChars.length); + this.highChars = highChars.clone(); + List temp = + new ArrayList(this.highChars.length); byte code = 127; for (int i = 0; i < this.highChars.length; ++i) { - this.reverseMapping.add(new Simple8BitChar(++code, - this.highChars[i])); + temp.add(new Simple8BitChar(++code, this.highChars[i])); } - Collections.sort(this.reverseMapping); + Collections.sort(temp); + this.reverseMapping = Collections.unmodifiableList(temp); } /** @@ -114,7 +128,7 @@ class Simple8BitZipEncoding implements ZipEncoding { } // byte is signed, so 128 == -128 and 255 == -1 - return this.highChars[128 + (int) b]; + return this.highChars[128 + b]; } /** @@ -137,7 +151,7 @@ class Simple8BitZipEncoding implements ZipEncoding { * @param bb The byte buffer to write to. * @param c The character to encode. * @return Whether the given unicode character is covered by this encoding. - * If false is returned, nothing is pushed to the + * If {@code false} is returned, nothing is pushed to the * byte buffer. */ public boolean pushEncodedChar(ByteBuffer bb, char c) { @@ -158,7 +172,7 @@ class Simple8BitZipEncoding implements ZipEncoding { /** * @param c A unicode character in the range from 0x0080 to 0x7f00 * @return A Simple8BitChar, if this character is covered by this encoding. - * A null value is returned, if this character is not + * A {@code null} value is returned, if this character is not * covered by this encoding. */ private Simple8BitChar encodeHighChar(char c) { @@ -171,7 +185,7 @@ class Simple8BitZipEncoding implements ZipEncoding { int i = i0 + (i1 - i0) / 2; - Simple8BitChar m = (Simple8BitChar) this.reverseMapping.get(i); + Simple8BitChar m = this.reverseMapping.get(i); if (m.unicode == c) { return m; @@ -188,7 +202,7 @@ class Simple8BitZipEncoding implements ZipEncoding { return null; } - Simple8BitChar r = (Simple8BitChar) this.reverseMapping.get(i0); + Simple8BitChar r = this.reverseMapping.get(i0); if (r.unicode != c) { return null; diff --git a/src/main/org/apache/tools/zip/UnicodeCommentExtraField.java b/src/main/org/apache/tools/zip/UnicodeCommentExtraField.java index 90cb4349e..25b42915f 100644 --- a/src/main/org/apache/tools/zip/UnicodeCommentExtraField.java +++ b/src/main/org/apache/tools/zip/UnicodeCommentExtraField.java @@ -67,6 +67,7 @@ public class UnicodeCommentExtraField extends AbstractUnicodeExtraField { super(comment, bytes); } + /** {@inheritDoc} */ public ZipShort getHeaderId() { return UCOM_ID; } diff --git a/src/main/org/apache/tools/zip/UnicodePathExtraField.java b/src/main/org/apache/tools/zip/UnicodePathExtraField.java index c40b79a25..92ecf0088 100644 --- a/src/main/org/apache/tools/zip/UnicodePathExtraField.java +++ b/src/main/org/apache/tools/zip/UnicodePathExtraField.java @@ -66,6 +66,7 @@ public class UnicodePathExtraField extends AbstractUnicodeExtraField { super(name, bytes); } + /** {@inheritDoc} */ public ZipShort getHeaderId() { return UPATH_ID; } diff --git a/src/main/org/apache/tools/zip/UnparseableExtraFieldData.java b/src/main/org/apache/tools/zip/UnparseableExtraFieldData.java index f9381a503..92d30020e 100644 --- a/src/main/org/apache/tools/zip/UnparseableExtraFieldData.java +++ b/src/main/org/apache/tools/zip/UnparseableExtraFieldData.java @@ -21,12 +21,11 @@ package org.apache.tools.zip; /** * Wrapper for extra field data that doesn't conform to the recommended format of header-tag + size + data. * - *

The header-id is artificial (and not listed as a know ID in - * the .ZIP File Format Specification). - * Since it isn't used anywhere except to satisfy the + *

The header-id is artificial (and not listed as a known ID in + * {@link + * APPNOTE.TXT}). Since it isn't used anywhere except to satisfy the * ZipExtraField contract it shouldn't matter anyway.

- * @see .ZIP File Format Specification + * * @since Ant 1.8.1 */ public final class UnparseableExtraFieldData @@ -103,7 +102,6 @@ public final class UnparseableExtraFieldData * @param buffer the buffer to read data from * @param offset offset into buffer to read data * @param length the length of data - * @exception ZipException on error */ public void parseFromCentralDirectoryData(byte[] buffer, int offset, int length) { diff --git a/src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java b/src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java new file mode 100644 index 000000000..21d48f204 --- /dev/null +++ b/src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java @@ -0,0 +1,89 @@ +/* + * 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; + +/** + * Exception thrown when attempting to read or write data for a zip + * entry that uses ZIP features not supported by this library. + * @since Ant 1.9.0 + */ +public class UnsupportedZipFeatureException extends ZipException { + + private final Feature reason; + private final ZipEntry entry; + private static final long serialVersionUID = 4430521921766595597L; + + /** + * Creates an exception. + * @param reason the feature that is not supported + * @param entry the entry using the feature + */ + public UnsupportedZipFeatureException(Feature reason, + ZipEntry entry) { + super("unsupported feature " + reason + " used in entry " + + entry.getName()); + this.reason = reason; + this.entry = entry; + } + + /** + * The unsupported feature that has been used. + */ + public Feature getFeature() { + return reason; + } + + /** + * The entry using the unsupported feature. + */ + public ZipEntry getEntry() { + return entry; + } + + /** + * ZIP Features that may or may not be supported. + */ + public static class Feature { + /** + * The entry is encrypted. + */ + public static final Feature ENCRYPTION = new Feature("encryption"); + /** + * The entry used an unsupported compression method. + */ + public static final Feature METHOD = new Feature("compression method"); + /** + * The entry uses a data descriptor. + */ + public static final Feature DATA_DESCRIPTOR = new Feature("data descriptor"); + + private final String name; + + private Feature(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } +} \ No newline at end of file diff --git a/src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java b/src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java new file mode 100644 index 000000000..d4da80fd8 --- /dev/null +++ b/src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java @@ -0,0 +1,354 @@ +/* + * 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; + +import static org.apache.tools.zip.ZipConstants.DWORD; +import static org.apache.tools.zip.ZipConstants.WORD; + +/** + * Holds size and other extended information for entries that use Zip64 + * features. + * + *

From {@link "http://www.pkware.com/documents/casestudies/APPNOTE.TXT PKWARE's APPNOTE.TXT"} + *

+ * Zip64 Extended Information Extra Field (0x0001):
+ *
+ *          The following is the layout of the zip64 extended 
+ *          information "extra" block. If one of the size or
+ *          offset fields in the Local or Central directory
+ *          record is too small to hold the required data,
+ *          a Zip64 extended information record is created.
+ *          The order of the fields in the zip64 extended 
+ *          information record is fixed, but the fields will
+ *          only appear if the corresponding Local or Central
+ *          directory record field is set to 0xFFFF or 0xFFFFFFFF.
+ *
+ *          Note: all fields stored in Intel low-byte/high-byte order.
+ *
+ *          Value      Size       Description
+ *          -----      ----       -----------
+ *  (ZIP64) 0x0001     2 bytes    Tag for this "extra" block type
+ *          Size       2 bytes    Size of this "extra" block
+ *          Original 
+ *          Size       8 bytes    Original uncompressed file size
+ *          Compressed
+ *          Size       8 bytes    Size of compressed data
+ *          Relative Header
+ *          Offset     8 bytes    Offset of local header record
+ *          Disk Start
+ *          Number     4 bytes    Number of the disk on which
+ *                                this file starts 
+ *
+ *          This entry in the Local header must include BOTH original
+ *          and compressed file size fields. If encrypting the 
+ *          central directory and bit 13 of the general purpose bit
+ *          flag is set indicating masking, the value stored in the
+ *          Local Header for the original file size will be zero.
+ * 

+ * + *

Currently Ant doesn't support encrypting the + * central directory so the note about masking doesn't apply.

+ * + *

The implementation relies on data being read from the local file + * header and assumes that both size values are always present.

+ * + * @since Ant 1.9.0 + */ +public class Zip64ExtendedInformationExtraField + implements CentralDirectoryParsingZipExtraField { + + static final ZipShort HEADER_ID = new ZipShort(0x0001); + + private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = + "Zip64 extended information must contain" + + " both size values in the local file header."; + + private ZipEightByteInteger size, compressedSize, relativeHeaderOffset; + private ZipLong diskStart; + + /** + * Stored in {@link #parseFromCentralDirectoryData + * parseFromCentralDirectoryData} so it can be reused when ZipFile + * calls {@link #reparseCentralDirectoryData + * reparseCentralDirectoryData}. + * + *

Not used for anything else

+ */ + private byte[] rawCentralDirectoryData; + + /** + * This constructor should only be used by the code that reads + * archives inside of Ant. + */ + public Zip64ExtendedInformationExtraField() { } + + /** + * Creates an extra field based on the original and compressed size. + * + * @param size the entry's original size + * @param compressedSize the entry's compressed size + * + * @throws IllegalArgumentException if size or compressedSize is null + */ + public Zip64ExtendedInformationExtraField(ZipEightByteInteger size, + ZipEightByteInteger compressedSize) { + this(size, compressedSize, null, null); + } + + /** + * Creates an extra field based on all four possible values. + * + * @param size the entry's original size + * @param compressedSize the entry's compressed size + * + * @throws IllegalArgumentException if size or compressedSize is null + */ + public Zip64ExtendedInformationExtraField(ZipEightByteInteger size, + ZipEightByteInteger compressedSize, + ZipEightByteInteger relativeHeaderOffset, + ZipLong diskStart) { + this.size = size; + this.compressedSize = compressedSize; + this.relativeHeaderOffset = relativeHeaderOffset; + this.diskStart = diskStart; + } + + /** {@inheritDoc} */ + public ZipShort getHeaderId() { + return HEADER_ID; + } + + /** {@inheritDoc} */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(size != null ? 2 * DWORD : 0); + } + + /** {@inheritDoc} */ + public ZipShort getCentralDirectoryLength() { + return new ZipShort((size != null ? DWORD : 0) + + (compressedSize != null ? DWORD : 0) + + (relativeHeaderOffset != null ? DWORD : 0) + + (diskStart != null ? WORD : 0)); + } + + /** {@inheritDoc} */ + public byte[] getLocalFileDataData() { + if (size != null || compressedSize != null) { + if (size == null || compressedSize == null) { + throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG); + } + byte[] data = new byte[2 * DWORD]; + addSizes(data); + return data; + } + return new byte[0]; + } + + /** {@inheritDoc} */ + public byte[] getCentralDirectoryData() { + byte[] data = new byte[getCentralDirectoryLength().getValue()]; + int off = addSizes(data); + if (relativeHeaderOffset != null) { + System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD); + off += DWORD; + } + if (diskStart != null) { + System.arraycopy(diskStart.getBytes(), 0, data, off, WORD); + off += WORD; + } + return data; + } + + /** {@inheritDoc} */ + public void parseFromLocalFileData(byte[] buffer, int offset, int length) + throws ZipException { + if (length == 0) { + // no local file data at all, may happen if an archive + // only holds a ZIP64 extended information extra field + // inside the central directory but not inside the local + // file header + return; + } + if (length < 2 * DWORD) { + throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG); + } + size = new ZipEightByteInteger(buffer, offset); + offset += DWORD; + compressedSize = new ZipEightByteInteger(buffer, offset); + offset += DWORD; + int remaining = length - 2 * DWORD; + if (remaining >= DWORD) { + relativeHeaderOffset = new ZipEightByteInteger(buffer, offset); + offset += DWORD; + remaining -= DWORD; + } + if (remaining >= WORD) { + diskStart = new ZipLong(buffer, offset); + offset += WORD; + remaining -= WORD; + } + } + + /** {@inheritDoc} */ + public void parseFromCentralDirectoryData(byte[] buffer, int offset, + int length) + throws ZipException { + // store for processing in reparseCentralDirectoryData + rawCentralDirectoryData = new byte[length]; + System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length); + + // if there is no size information in here, we are screwed and + // can only hope things will get resolved by LFH data later + // But there are some cases that can be detected + // * all data is there + // * length == 24 -> both sizes and offset + // * length % 8 == 4 -> at least we can identify the diskStart field + if (length >= 3 * DWORD + WORD) { + parseFromLocalFileData(buffer, offset, length); + } else if (length == 3 * DWORD) { + size = new ZipEightByteInteger(buffer, offset); + offset += DWORD; + compressedSize = new ZipEightByteInteger(buffer, offset); + offset += DWORD; + relativeHeaderOffset = new ZipEightByteInteger(buffer, offset); + } else if (length % DWORD == WORD) { + diskStart = new ZipLong(buffer, offset + length - WORD); + } + } + + /** + * Parses the raw bytes read from the central directory extra + * field with knowledge which fields are expected to be there. + * + *

All four fields inside the zip64 extended information extra + * field are optional and only present if their corresponding + * entry inside the central directory contains the correct magic + * value.

+ */ + public void reparseCentralDirectoryData(boolean hasUncompressedSize, + boolean hasCompressedSize, + boolean hasRelativeHeaderOffset, + boolean hasDiskStart) + throws ZipException { + if (rawCentralDirectoryData != null) { + int expectedLength = (hasUncompressedSize ? DWORD : 0) + + (hasCompressedSize ? DWORD : 0) + + (hasRelativeHeaderOffset ? DWORD : 0) + + (hasDiskStart ? WORD : 0); + if (rawCentralDirectoryData.length != expectedLength) { + throw new ZipException("central directory zip64 extended" + + " information extra field's length" + + " doesn't match central directory" + + " data. Expected length " + + expectedLength + " but is " + + rawCentralDirectoryData.length); + } + int offset = 0; + if (hasUncompressedSize) { + size = new ZipEightByteInteger(rawCentralDirectoryData, offset); + offset += DWORD; + } + if (hasCompressedSize) { + compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, + offset); + offset += DWORD; + } + if (hasRelativeHeaderOffset) { + relativeHeaderOffset = + new ZipEightByteInteger(rawCentralDirectoryData, offset); + offset += DWORD; + } + if (hasDiskStart) { + diskStart = new ZipLong(rawCentralDirectoryData, offset); + offset += WORD; + } + } + } + + /** + * The uncompressed size stored in this extra field. + */ + public ZipEightByteInteger getSize() { + return size; + } + + /** + * The uncompressed size stored in this extra field. + */ + public void setSize(ZipEightByteInteger size) { + this.size = size; + } + + /** + * The compressed size stored in this extra field. + */ + public ZipEightByteInteger getCompressedSize() { + return compressedSize; + } + + /** + * The uncompressed size stored in this extra field. + */ + public void setCompressedSize(ZipEightByteInteger compressedSize) { + this.compressedSize = compressedSize; + } + + /** + * The relative header offset stored in this extra field. + */ + public ZipEightByteInteger getRelativeHeaderOffset() { + return relativeHeaderOffset; + } + + /** + * The relative header offset stored in this extra field. + */ + public void setRelativeHeaderOffset(ZipEightByteInteger rho) { + relativeHeaderOffset = rho; + } + + /** + * The disk start number stored in this extra field. + */ + public ZipLong getDiskStartNumber() { + return diskStart; + } + + /** + * The disk start number stored in this extra field. + */ + public void setDiskStartNumber(ZipLong ds) { + diskStart = ds; + } + + private int addSizes(byte[] data) { + int off = 0; + if (size != null) { + System.arraycopy(size.getBytes(), 0, data, 0, DWORD); + off += DWORD; + } + if (compressedSize != null) { + System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD); + off += DWORD; + } + return off; + } +} \ No newline at end of file diff --git a/src/main/org/apache/tools/zip/Zip64Mode.java b/src/main/org/apache/tools/zip/Zip64Mode.java new file mode 100644 index 000000000..30d9f1cb0 --- /dev/null +++ b/src/main/org/apache/tools/zip/Zip64Mode.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * The different modes {@link ZipOutputStream} can operate in. + * + * @see ZipOutputStream#setUseZip64 + * + * @since Ant 1.9.0 + */ +public enum Zip64Mode { + /** + * Use Zip64 extensions for all entries, even if it is clear it is + * not required. + */ + Always, + /** + * Don't use Zip64 extensions for any entries. + * + *

This will cause a {@link Zip64RequiredException} to be + * thrown if {@link ZipOutputStream} detects it needs Zip64 + * support.

+ */ + Never, + /** + * Use Zip64 extensions for all entries where they are required, + * don't use them for entries that clearly don't require them. + */ + AsNeeded +} diff --git a/src/main/org/apache/tools/zip/Zip64RequiredException.java b/src/main/org/apache/tools/zip/Zip64RequiredException.java new file mode 100644 index 000000000..f7fb77908 --- /dev/null +++ b/src/main/org/apache/tools/zip/Zip64RequiredException.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * Exception thrown when attempting to write data that requires Zip64 + * support to an archive and {@link ZipOutputStream#setUseZip64 + * UseZip64} has been set to {@link Zip64Mode#Never Never}. + * @since Ant 1.9.0 + */ +public class Zip64RequiredException extends ZipException { + + private static final long serialVersionUID = 20110809L; + + /** + * Helper to format "entry too big" messages. + */ + static String getEntryTooBigMessage(ZipEntry ze) { + return ze.getName() + "'s size exceeds the limit of 4GByte."; + } + + static final String ARCHIVE_TOO_BIG_MESSAGE = + "archive's size exceeds the limit of 4GByte."; + + static final String TOO_MANY_ENTRIES_MESSAGE = + "archive contains more than 65535 entries."; + + public Zip64RequiredException(String reason) { + super(reason); + } +} diff --git a/src/main/org/apache/tools/zip/ZipConstants.java b/src/main/org/apache/tools/zip/ZipConstants.java new file mode 100644 index 000000000..83ae95699 --- /dev/null +++ b/src/main/org/apache/tools/zip/ZipConstants.java @@ -0,0 +1,59 @@ +/* + * 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; + +/** + * Various constants used throughout the package. + */ +final class ZipConstants { + private ZipConstants() { } + + /** Masks last eight bits */ + static final int BYTE_MASK = 0xFF; + + /** length of a ZipShort in bytes */ + static final int SHORT = 2; + + /** length of a ZipLong in bytes */ + static final int WORD = 4; + + /** length of a ZipEightByteInteger in bytes */ + static final int DWORD = 8; + + /** Initial ZIP specification version */ + static final int INITIAL_VERSION = 10; + + /** ZIP specification version that introduced data descriptor method */ + static final int DATA_DESCRIPTOR_MIN_VERSION = 20; + + /** ZIP specification version that introduced ZIP64 */ + static final int ZIP64_MIN_VERSION = 45; + + /** + * Value stored in two-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final int ZIP64_MAGIC_SHORT = 0xFFFF; + + /** + * Value stored in four-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final long ZIP64_MAGIC = 0xFFFFFFFFL; + +} diff --git a/src/main/org/apache/tools/zip/ZipEightByteInteger.java b/src/main/org/apache/tools/zip/ZipEightByteInteger.java new file mode 100644 index 000000000..55cd3c5e4 --- /dev/null +++ b/src/main/org/apache/tools/zip/ZipEightByteInteger.java @@ -0,0 +1,229 @@ +/* + * 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.math.BigInteger; + +import static org.apache.tools.zip.ZipConstants.BYTE_MASK; + +/** + * Utility class that represents an eight byte integer with conversion + * rules for the big endian byte order of ZIP files. + */ +public final class ZipEightByteInteger { + + private static final int BYTE_1 = 1; + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private static final int BYTE_2 = 2; + private static final int BYTE_2_MASK = 0xFF0000; + private static final int BYTE_2_SHIFT = 16; + + private static final int BYTE_3 = 3; + private static final long BYTE_3_MASK = 0xFF000000L; + private static final int BYTE_3_SHIFT = 24; + + private static final int BYTE_4 = 4; + private static final long BYTE_4_MASK = 0xFF00000000L; + private static final int BYTE_4_SHIFT = 32; + + private static final int BYTE_5 = 5; + private static final long BYTE_5_MASK = 0xFF0000000000L; + private static final int BYTE_5_SHIFT = 40; + + private static final int BYTE_6 = 6; + private static final long BYTE_6_MASK = 0xFF000000000000L; + private static final int BYTE_6_SHIFT = 48; + + private static final int BYTE_7 = 7; + private static final long BYTE_7_MASK = 0x7F00000000000000L; + private static final int BYTE_7_SHIFT = 56; + + private static final int LEFTMOST_BIT_SHIFT = 63; + private static final byte LEFTMOST_BIT = (byte) 0x80; + + private final BigInteger value; + + public static final ZipEightByteInteger ZERO = new ZipEightByteInteger(0); + + /** + * Create instance from a number. + * @param value the long to store as a ZipEightByteInteger + */ + public ZipEightByteInteger(long value) { + this(BigInteger.valueOf(value)); + } + + /** + * Create instance from a number. + * @param value the BigInteger to store as a ZipEightByteInteger + */ + public ZipEightByteInteger(BigInteger value) { + this.value = value; + } + + /** + * Create instance from bytes. + * @param bytes the bytes to store as a ZipEightByteInteger + */ + public ZipEightByteInteger (byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the eight bytes starting at offset. + * @param bytes the bytes to store as a ZipEightByteInteger + * @param offset the offset to start + */ + public ZipEightByteInteger (byte[] bytes, int offset) { + value = ZipEightByteInteger.getValue(bytes, offset); + } + + /** + * Get value as eight bytes in big endian byte order. + * @return value as eight bytes in big endian order + */ + public byte[] getBytes() { + return ZipEightByteInteger.getBytes(value); + } + + /** + * Get value as Java long. + * @return value as a long + */ + public long getLongValue() { + return value.longValue(); + } + + /** + * Get value as Java long. + * @return value as a long + */ + public BigInteger getValue() { + return value; + } + + /** + * Get value as eight bytes in big endian byte order. + * @param value the value to convert + * @return value as eight bytes in big endian byte order + */ + public static byte[] getBytes(long value) { + return getBytes(BigInteger.valueOf(value)); + } + + /** + * Get value as eight bytes in big endian byte order. + * @param value the value to convert + * @return value as eight bytes in big endian byte order + */ + public static byte[] getBytes(BigInteger value) { + byte[] result = new byte[8]; + long val = value.longValue(); + result[0] = (byte) ((val & BYTE_MASK)); + result[BYTE_1] = (byte) ((val & BYTE_1_MASK) >> BYTE_1_SHIFT); + result[BYTE_2] = (byte) ((val & BYTE_2_MASK) >> BYTE_2_SHIFT); + result[BYTE_3] = (byte) ((val & BYTE_3_MASK) >> BYTE_3_SHIFT); + result[BYTE_4] = (byte) ((val & BYTE_4_MASK) >> BYTE_4_SHIFT); + result[BYTE_5] = (byte) ((val & BYTE_5_MASK) >> BYTE_5_SHIFT); + result[BYTE_6] = (byte) ((val & BYTE_6_MASK) >> BYTE_6_SHIFT); + result[BYTE_7] = (byte) ((val & BYTE_7_MASK) >> BYTE_7_SHIFT); + if (value.testBit(LEFTMOST_BIT_SHIFT)) { + result[BYTE_7] |= LEFTMOST_BIT; + } + return result; + } + + /** + * Helper method to get the value as a Java long from eight bytes + * starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java long value + */ + public static long getLongValue(byte[] bytes, int offset) { + return getValue(bytes, offset).longValue(); + } + + /** + * Helper method to get the value as a Java BigInteger from eight + * bytes starting at given array offset + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java BigInteger value + */ + public static BigInteger getValue(byte[] bytes, int offset) { + long value = ((long) bytes[offset + BYTE_7] << BYTE_7_SHIFT) & BYTE_7_MASK; + value += ((long) bytes[offset + BYTE_6] << BYTE_6_SHIFT) & BYTE_6_MASK; + value += ((long) bytes[offset + BYTE_5] << BYTE_5_SHIFT) & BYTE_5_MASK; + value += ((long) bytes[offset + BYTE_4] << BYTE_4_SHIFT) & BYTE_4_MASK; + value += ((long) bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; + value += ((long) bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK; + value += ((long) bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += ((long) bytes[offset] & BYTE_MASK); + BigInteger val = BigInteger.valueOf(value); + return (bytes[offset + BYTE_7] & LEFTMOST_BIT) == LEFTMOST_BIT + ? val.setBit(LEFTMOST_BIT_SHIFT) : val; + } + + /** + * Helper method to get the value as a Java long from an eight-byte array + * @param bytes the array of bytes + * @return the corresponding Java long value + */ + public static long getLongValue(byte[] bytes) { + return getLongValue(bytes, 0); + } + + /** + * Helper method to get the value as a Java long from an eight-byte array + * @param bytes the array of bytes + * @return the corresponding Java BigInteger value + */ + public static BigInteger 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 + */ + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof ZipEightByteInteger)) { + return false; + } + return value.equals(((ZipEightByteInteger) o).getValue()); + } + + /** + * Override to make two instances with same value equal. + * @return the hashCode of the value stored in the ZipEightByteInteger + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + return "ZipEightByteInteger value: " + value; + } +} diff --git a/src/main/org/apache/tools/zip/ZipEncoding.java b/src/main/org/apache/tools/zip/ZipEncoding.java index 53ea2db33..3df6329ec 100644 --- a/src/main/org/apache/tools/zip/ZipEncoding.java +++ b/src/main/org/apache/tools/zip/ZipEncoding.java @@ -68,7 +68,7 @@ interface ZipEncoding { * character sequences are mapped to a sequence of utf-16 * words encoded in the format %Uxxxx. It is * assumed, that the byte buffer is positioned at the - * beinning of the encoded result, the byte buffer has a + * beginning of the encoded result, the byte buffer has a * backing array and the limit of the byte buffer points * to the end of the encoded result. * @throws IOException diff --git a/src/main/org/apache/tools/zip/ZipEncodingHelper.java b/src/main/org/apache/tools/zip/ZipEncodingHelper.java index 451de70a1..1c6a0688d 100644 --- a/src/main/org/apache/tools/zip/ZipEncodingHelper.java +++ b/src/main/org/apache/tools/zip/ZipEncodingHelper.java @@ -62,10 +62,10 @@ abstract class ZipEncodingHelper { } } - private static final Map simpleEncodings; + private static final Map simpleEncodings; static { - simpleEncodings = new HashMap(); + simpleEncodings = new HashMap(); char[] cp437_high_chars = new char[] { 0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, @@ -203,7 +203,7 @@ abstract class ZipEncodingHelper { /** * Instantiates a zip encoding. * - * @param name The name of the zip encoding. Specify null for + * @param name The name of the zip encoding. Specify {@code null} for * the platform's default encoding. * @return A zip encoding for the given encoding name. */ @@ -218,8 +218,7 @@ abstract class ZipEncodingHelper { return new FallbackZipEncoding(); } - SimpleEncodingHolder h = - (SimpleEncodingHolder) simpleEncodings.get(name); + SimpleEncodingHolder h = simpleEncodings.get(name); if (h!=null) { return h.getEncoding(); diff --git a/src/main/org/apache/tools/zip/ZipEntry.java b/src/main/org/apache/tools/zip/ZipEntry.java index 834ba8ba9..ba71751d2 100644 --- a/src/main/org/apache/tools/zip/ZipEntry.java +++ b/src/main/org/apache/tools/zip/ZipEntry.java @@ -18,7 +18,10 @@ package org.apache.tools.zip; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.zip.ZipException; @@ -28,7 +31,8 @@ import java.util.zip.ZipException; * access to the internal and external file attributes. * *

The extra data is expected to follow the recommendation of - * the .ZIP File Format Specification created by PKWARE Inc. :

+ * {@link + * APPNOTE.txt}:

*
    *
  • the extra byte array consists of a sequence of extra fields
  • *
  • each extra fields starts by a two byte header id followed by @@ -38,11 +42,9 @@ import java.util.zip.ZipException; * *

    Any extra data that cannot be parsed by the rules above will be * consumed as "unparseable" extra data and treated differently by the - * methods of this class. Versions prior to Apache Commons Compress - * 1.1 would have thrown an exception if any attempt was made to read - * or write extra data not conforming to the recommendation.

    - * @see - * .ZIP File Format Specification + * methods of this class. Older versions would have thrown an + * exception if any attempt was made to read or write extra data not + * conforming to the recommendation.

    * */ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { @@ -52,30 +54,59 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { private static final int SHORT_MASK = 0xFFFF; private static final int SHORT_SHIFT = 16; + /** + * The {@link java.util.zip.ZipEntry} base class only supports + * the compression methods STORED and DEFLATED. We override the + * field so that any compression methods can be used. + *

    + * The default value -1 means that the method has not been specified. + */ + private int method = -1; + + /** + * The {@link java.util.zip.ZipEntry#setSize} method in the base + * class throws an IllegalArgumentException if the size is bigger + * than 2GB for Java versions < 7. Need to keep our own size + * information for Zip64 support. + */ + private long size = -1; + private int internalAttributes = 0; private int platform = PLATFORM_FAT; private long externalAttributes = 0; - private LinkedHashMap/**/ extraFields = null; + private LinkedHashMap extraFields = null; private UnparseableExtraFieldData unparseableExtra = null; private String name = null; + private byte[] rawName = null; + private GeneralPurposeBit gpb = new GeneralPurposeBit(); /** * Creates a new zip entry with the specified name. + * + *

    Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".

    + * * @param name the name of the entry * @since 1.1 */ public ZipEntry(String name) { super(name); + setName(name); } /** * Creates a new zip entry with fields taken from the specified zip entry. + * + *

    Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".

    + * * @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); + setName(entry.getName()); byte[] extra = entry.getExtra(); if (extra != null) { setExtraFields(ExtraFieldUtils.parse(extra, true, @@ -85,10 +116,16 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { // initializes extra data to an empty byte array setExtra(); } + setMethod(entry.getMethod()); + this.size = entry.getSize(); } /** * Creates a new zip entry with fields taken from the specified zip entry. + * + *

    Assumes the entry represents a directory if and only if the + * name ends with a forward slash "/".

    + * * @param entry the entry to get fields from * @throws ZipException on error * @since 1.1 @@ -104,7 +141,26 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @since 1.9 */ protected ZipEntry() { - super(""); + this(""); + } + + /** + * Creates a new zip entry taking some information from the given + * file and using the provided name. + * + *

    The name will be adjusted to end with a forward slash "/" if + * the file is a directory. If the file is not a directory a + * potential trailing forward slash will be stripped from the + * entry name.

    + */ + public ZipEntry(File inputFile, String entryName) { + this(inputFile.isDirectory() && !entryName.endsWith("/") ? + entryName + "/" : entryName); + if (inputFile.isFile()){ + setSize(inputFile.length()); + } + setTime(inputFile.lastModified()); + // TODO are there any other fields we can set here? } /** @@ -112,6 +168,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @return a cloned copy of this ZipEntry * @since 1.1 */ + @Override public Object clone() { ZipEntry e = (ZipEntry) super.clone(); @@ -121,6 +178,31 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { return e; } + /** + * Returns the compression method of this entry, or -1 if the + * compression method has not been specified. + * + * @return compression method + */ + @Override + public int getMethod() { + return method; + } + + /** + * Sets the compression method of this entry. + * + * @param method compression method + */ + @Override + public void setMethod(int method) { + if (method < 0) { + throw new IllegalArgumentException( + "ZIP compression method can not be negative: " + method); + } + this.method = method; + } + /** * Retrieves the internal file attributes. * @@ -213,12 +295,12 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @since 1.1 */ public void setExtraFields(ZipExtraField[] fields) { - extraFields = new LinkedHashMap(); - for (int i = 0; i < fields.length; i++) { - if (fields[i] instanceof UnparseableExtraFieldData) { - unparseableExtra = (UnparseableExtraFieldData) fields[i]; + extraFields = new LinkedHashMap(); + for (ZipExtraField field : fields) { + if (field instanceof UnparseableExtraFieldData) { + unparseableExtra = (UnparseableExtraFieldData) field; } else { - extraFields.put(fields[i].getHeaderId(), fields[i]); + extraFields.put(field.getHeaderId(), field); } } setExtra(); @@ -246,11 +328,12 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { ? new ZipExtraField[0] : new ZipExtraField[] { unparseableExtra }; } - List result = new ArrayList(extraFields.values()); + List result = + new ArrayList(extraFields.values()); if (includeUnparseable && unparseableExtra != null) { result.add(unparseableExtra); } - return (ZipExtraField[]) result.toArray(new ZipExtraField[0]); + return result.toArray(new ZipExtraField[0]); } /** @@ -267,7 +350,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { unparseableExtra = (UnparseableExtraFieldData) ze; } else { if (extraFields == null) { - extraFields = new LinkedHashMap(); + extraFields = new LinkedHashMap(); } extraFields.put(ze.getHeaderId(), ze); } @@ -286,8 +369,8 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { if (ze instanceof UnparseableExtraFieldData) { unparseableExtra = (UnparseableExtraFieldData) ze; } else { - LinkedHashMap copy = extraFields; - extraFields = new LinkedHashMap(); + LinkedHashMap copy = extraFields; + extraFields = new LinkedHashMap(); extraFields.put(ze.getHeaderId(), ze); if (copy != null) { copy.remove(ze.getHeaderId()); @@ -330,7 +413,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { */ public ZipExtraField getExtraField(ZipShort type) { if (extraFields != null) { - return (ZipExtraField) extraFields.get(type); + return extraFields.get(type); } return null; } @@ -353,13 +436,14 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @since 1.1 * @throws RuntimeException on error */ + @Override public void setExtra(byte[] extra) throws RuntimeException { try { ZipExtraField[] local = ExtraFieldUtils.parse(extra, true, ExtraFieldUtils.UnparseableExtraField.READ); mergeExtraFields(local, true); - } catch (Exception e) { + } catch (ZipException e) { // actually this is not be possible as of Ant 1.8.1 throw new RuntimeException("Error parsing extra fields for entry: " + getName() + " - " + e.getMessage(), e); @@ -387,7 +471,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { ExtraFieldUtils.parse(b, false, ExtraFieldUtils.UnparseableExtraField.READ); mergeExtraFields(central, false); - } catch (Exception e) { + } catch (ZipException e) { throw new RuntimeException(e.getMessage(), e); } } @@ -430,6 +514,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @return the entry name * @since 1.9 */ + @Override public String getName() { return name == null ? super.getName() : name; } @@ -439,6 +524,7 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @return true if the entry is a directory * @since 1.10 */ + @Override public boolean isDirectory() { return getName().endsWith("/"); } @@ -448,15 +534,72 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { * @param name the name to use */ protected void setName(String name) { + if (name != null && getPlatform() == PLATFORM_FAT + && name.indexOf("/") == -1) { + name = name.replace('\\', '/'); + } this.name = name; } + /** + * Gets the uncompressed size of the entry data. + * @return the entry size + */ + @Override + public long getSize() { + return size; + } + + /** + * Sets the uncompressed size of the entry data. + * @param size the uncompressed size in bytes + * @exception IllegalArgumentException if the specified size is less + * than 0 + */ + @Override + public void setSize(long size) { + if (size < 0) { + throw new IllegalArgumentException("invalid entry size"); + } + this.size = size; + } + + /** + * Sets the name using the raw bytes and the string created from + * it by guessing or using the configured encoding. + * @param name the name to use created from the raw bytes using + * the guessed or configured encoding + * @param rawName the bytes originally read as name from the + * archive + */ + protected void setName(String name, byte[] rawName) { + setName(name); + this.rawName = rawName; + } + + /** + * Returns the raw bytes that made up the name before it has been + * converted using the configured or guessed encoding. + * + *

    This method will return null if this instance has not been + * read from an archive.

    + */ + public byte[] getRawName() { + if (rawName != null) { + byte[] b = new byte[rawName.length]; + System.arraycopy(rawName, 0, b, 0, rawName.length); + return b; + } + return null; + } + /** * Get the hashCode of the entry. * This uses the name as the hashcode. * @return a hashcode. * @since Ant 1.7 */ + @Override public int hashCode() { // this method has severe consequences on performance. We cannot rely // on the super.hashCode() method since super.getName() always return @@ -466,14 +609,17 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { } /** - * 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 + * The "general purpose bit" field. + */ + public GeneralPurposeBit getGeneralPurposeBit() { + return gpb; + } + + /** + * The "general purpose bit" field. */ - public boolean equals(Object o) { - return (this == o); + public void setGeneralPurposeBit(GeneralPurposeBit b) { + gpb = b; } /** @@ -489,23 +635,23 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { if (extraFields == null) { setExtraFields(f); } else { - for (int i = 0; i < f.length; i++) { + for (ZipExtraField element : f) { ZipExtraField existing; - if (f[i] instanceof UnparseableExtraFieldData) { + if (element instanceof UnparseableExtraFieldData) { existing = unparseableExtra; } else { - existing = getExtraField(f[i].getHeaderId()); + existing = getExtraField(element.getHeaderId()); } if (existing == null) { - addExtraField(f[i]); + addExtraField(element); } else { if (local || !(existing instanceof CentralDirectoryParsingZipExtraField)) { - byte[] b = f[i].getLocalFileDataData(); + byte[] b = element.getLocalFileDataData(); existing.parseFromLocalFileData(b, 0, b.length); } else { - byte[] b = f[i].getCentralDirectoryData(); + byte[] b = element.getCentralDirectoryData(); ((CentralDirectoryParsingZipExtraField) existing) .parseFromCentralDirectoryData(b, 0, b.length); } @@ -514,4 +660,54 @@ public class ZipEntry extends java.util.zip.ZipEntry implements Cloneable { setExtra(); } } + + /** {@inheritDoc} */ + public Date getLastModifiedDate() { + return new Date(getTime()); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ZipEntry other = (ZipEntry) obj; + String myName = getName(); + String otherName = other.getName(); + if (myName == null) { + if (otherName != null) { + return false; + } + } else if (!myName.equals(otherName)) { + return false; + } + String myComment = getComment(); + String otherComment = other.getComment(); + if (myComment == null) { + myComment = ""; + } + if (otherComment == null) { + otherComment = ""; + } + return getTime() == other.getTime() + && myComment.equals(otherComment) + && getInternalAttributes() == other.getInternalAttributes() + && getPlatform() == other.getPlatform() + && getExternalAttributes() == other.getExternalAttributes() + && getMethod() == other.getMethod() + && getSize() == other.getSize() + && getCrc() == other.getCrc() + && getCompressedSize() == other.getCompressedSize() + && Arrays.equals(getCentralDirectoryExtra(), + other.getCentralDirectoryExtra()) + && Arrays.equals(getLocalFileDataExtra(), + other.getLocalFileDataExtra()) + && gpb.equals(other.gpb); + } } diff --git a/src/main/org/apache/tools/zip/ZipFile.java b/src/main/org/apache/tools/zip/ZipFile.java index 2336916fc..fba3ab298 100644 --- a/src/main/org/apache/tools/zip/ZipFile.java +++ b/src/main/org/apache/tools/zip/ZipFile.java @@ -18,22 +18,28 @@ package org.apache.tools.zip; +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; -import java.util.Calendar; +import java.util.Arrays; import java.util.Collections; -import java.util.Date; +import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.zip.CRC32; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import java.util.zip.ZipException; +import static org.apache.tools.zip.ZipConstants.DWORD; +import static org.apache.tools.zip.ZipConstants.SHORT; +import static org.apache.tools.zip.ZipConstants.WORD; +import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC; +import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC_SHORT; + /** * Replacement for java.util.ZipFile. * @@ -47,7 +53,10 @@ import java.util.zip.ZipException; *

    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.

    + * covers and supports compressed and uncompressed entries. As of + * Apache Ant 1.9.0 it also transparently supports Zip64 + * extensions and thus individual entries and archives larger than 4 + * GB or with more than 65536 entries.

    * *

    The method signatures mimic the ones of * java.util.zip.ZipFile, with a couple of exceptions: @@ -63,25 +72,25 @@ import java.util.zip.ZipException; */ public class ZipFile { private static final int HASH_SIZE = 509; - private static final int SHORT = 2; - private static final int WORD = 4; - private static final int NIBLET_MASK = 0x0f; - private static final int BYTE_SHIFT = 8; + static final int NIBLET_MASK = 0x0f; + static final int BYTE_SHIFT = 8; private static final int POS_0 = 0; private static final int POS_1 = 1; private static final int POS_2 = 2; private static final int POS_3 = 3; /** - * Maps ZipEntrys to Longs, recording the offsets of the local - * file headers. + * Maps ZipEntrys to two longs, recording the offsets of + * the local file headers and the start of entry data. */ - private final Map entries = new HashMap(HASH_SIZE); + private final Map entries = + new LinkedHashMap(HASH_SIZE); /** * Maps String to ZipEntrys, name -> actual entry. */ - private final Map nameMap = new HashMap(HASH_SIZE); + private final Map nameMap = + new HashMap(HASH_SIZE); private static final class OffsetEntry { private long headerOffset = -1; @@ -95,23 +104,33 @@ public class ZipFile { * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. * Defaults to the platform's default character encoding.

    */ - private String encoding = null; + private final String encoding; /** * The zip encoding to use for filenames and the file comment. */ private final ZipEncoding zipEncoding; + /** + * File name of actual source. + */ + private final String archiveName; + /** * The actual data source. */ - private RandomAccessFile archive; + private final RandomAccessFile archive; /** * Whether to look for and use Unicode extra fields. */ private final boolean useUnicodeExtraFields; + /** + * Whether the file is closed. + */ + private boolean closed; + /** * Opens the given file for reading, assuming the platform's * native encoding for file names. @@ -141,7 +160,8 @@ public class ZipFile { * encoding for file names, scanning unicode extra fields. * * @param name name of the archive. - * @param encoding the encoding to use for file names + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding * * @throws IOException if an error occurs while reading the file. */ @@ -177,18 +197,21 @@ public class ZipFile { */ public ZipFile(File f, String encoding, boolean useUnicodeExtraFields) throws IOException { + this.archiveName = f.getAbsolutePath(); this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); this.useUnicodeExtraFields = useUnicodeExtraFields; archive = new RandomAccessFile(f, "r"); boolean success = false; try { - Map entriesWithoutUTF8Flag = populateFromCentralDirectory(); + Map entriesWithoutUTF8Flag = + populateFromCentralDirectory(); resolveLocalFileHeaderData(entriesWithoutUTF8Flag); success = true; } finally { if (!success) { try { + closed = true; archive.close(); } catch (IOException e2) { // swallow, throw the original exception instead @@ -211,6 +234,11 @@ public class ZipFile { * @throws IOException if an error occurs closing the archive. */ public void close() throws IOException { + // this flag is only written here and read in finalize() which + // can never be run in parallel. + // no synchronization needed. + closed = true; + archive.close(); } @@ -231,37 +259,69 @@ public class ZipFile { /** * Returns all entries. + * + *

    Entries will be returned in the same order they appear + * within the archive's central directory.

    + * * @return all entries as {@link ZipEntry} instances */ - public Enumeration getEntries() { + public Enumeration getEntries() { return Collections.enumeration(entries.keySet()); } /** - * Returns a named entry - or null if no entry by + * Returns all entries in physical order. + * + *

    Entries will be returned in the same order their contents + * appear within the archive.

    + * + * @return all entries as {@link ZipEntry} instances + * + * @since Ant 1.9.0 + */ + public Enumeration getEntriesInPhysicalOrder() { + ZipEntry[] allEntries = + entries.keySet().toArray(new ZipEntry[0]); + Arrays.sort(allEntries, OFFSET_COMPARATOR); + return Collections.enumeration(Arrays.asList(allEntries)); + } + + /** + * Returns a named entry - or {@code 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. + * {@code null} if not present. */ public ZipEntry getEntry(String name) { - return (ZipEntry) nameMap.get(name); + return nameMap.get(name); + } + + /** + * Whether this class is able to read the given entry. + * + *

    May return false if it is set up to use encryption or a + * compression method that hasn't been implemented yet.

    + */ + public boolean canReadEntryData(ZipEntry ze) { + return ZipUtil.canHandleEntryData(ze); } /** * 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 zipentry - * @throws ZipException if the zipentry has an unsupported - * compression method + * @throws ZipException if the zipentry uses an unsupported feature */ public InputStream getInputStream(ZipEntry ze) throws IOException, ZipException { - OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); + OffsetEntry offsetEntry = entries.get(ze); if (offsetEntry == null) { return null; } + ZipUtil.checkRequestedFeatures(ze); long start = offsetEntry.dataOffset; BoundedInputStream bis = new BoundedInputStream(start, ze.getCompressedSize()); @@ -272,6 +332,7 @@ public class ZipFile { bis.addDummy(); final Inflater inflater = new Inflater(true); return new InflaterInputStream(bis, inflater) { + @Override public void close() throws IOException { super.close(); inflater.end(); @@ -283,6 +344,28 @@ public class ZipFile { } } + /** + * Ensures that the close method of this zipfile is called when + * there are no more references to it. + * @see #close() + */ + @Override + protected void finalize() throws Throwable { + try { + if (!closed) { + System.err.println("Cleaning up unclosed ZipFile for archive " + + archiveName); + close(); + } + } finally { + super.finalize(); + } + } + + /** + * Length of a "central directory" entry structure without file + * name, extra fields or comment. + */ private static final int CFH_LEN = /* version made by */ SHORT /* version needed to extract */ + SHORT @@ -301,6 +384,9 @@ public class ZipFile { /* external file attributes */ + WORD /* relative offset of local header */ + WORD; + private static final long CFH_SIG = + ZipLong.getValue(ZipOutputStream.CFH_SIG); + /** * Reads the central directory of the given archive and populates * the internal tables with ZipEntry instances. @@ -309,111 +395,179 @@ public class ZipFile { * the central directory alone, but not the data that requires the * local file header or additional data to be read.

    * - * @return a Map<ZipEntry, NameAndComment>> of - * zipentries that didn't have the language encoding flag set when - * read. + * @return a map of zipentries that didn't have the language + * encoding flag set when read. */ - private Map populateFromCentralDirectory() + private Map populateFromCentralDirectory() throws IOException { - HashMap noUTF8Flag = new HashMap(); + HashMap noUTF8Flag = + new HashMap(); positionAtCentralDirectory(); - byte[] cfh = new byte[CFH_LEN]; - byte[] signatureBytes = new byte[WORD]; archive.readFully(signatureBytes); long sig = ZipLong.getValue(signatureBytes); - final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG); - if (sig != cfhSig && startsWithLocalFileHeader()) { + + if (sig != CFH_SIG && startsWithLocalFileHeader()) { throw new IOException("central directory is empty, can't expand" + " corrupt archive."); } - while (sig == cfhSig) { - archive.readFully(cfh); - int off = 0; - ZipEntry ze = new ZipEntry(); - int versionMadeBy = ZipShort.getValue(cfh, off); - off += SHORT; - ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); + while (sig == CFH_SIG) { + readCentralDirectoryEntry(noUTF8Flag); + archive.readFully(signatureBytes); + sig = ZipLong.getValue(signatureBytes); + } + return noUTF8Flag; + } + + /** + * Reads an individual entry of the central directory, creats an + * ZipEntry from it and adds it to the global maps. + * + * @param noUTF8Flag map used to collect entries that don't have + * their UTF-8 flag set and whose name will be set by data read + * from the local file header later. The current entry may be + * added to this map. + */ + private void + readCentralDirectoryEntry(Map noUTF8Flag) + throws IOException { + byte[] cfh = new byte[CFH_LEN]; - off += SHORT; // skip version info + archive.readFully(cfh); + int off = 0; + ZipEntry ze = new ZipEntry(); - final int generalPurposeFlag = ZipShort.getValue(cfh, off); - final boolean hasUTF8Flag = - (generalPurposeFlag & ZipOutputStream.UFT8_NAMES_FLAG) != 0; - final ZipEncoding entryEncoding = - hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + int versionMadeBy = ZipShort.getValue(cfh, off); + off += SHORT; + ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); - off += SHORT; + off += SHORT; // skip version info - ze.setMethod(ZipShort.getValue(cfh, off)); - off += SHORT; + final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfh, off); + final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames(); + final ZipEncoding entryEncoding = + hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + ze.setGeneralPurposeBit(gpFlag); - // FIXME this is actually not very cpu cycles friendly as we are converting from - // dos to java while the underlying Sun implementation will convert - // from java to dos time for internal storage... - long time = dosToJavaTime(ZipLong.getValue(cfh, off)); - ze.setTime(time); - off += WORD; + off += SHORT; - ze.setCrc(ZipLong.getValue(cfh, off)); - off += WORD; + ze.setMethod(ZipShort.getValue(cfh, off)); + off += SHORT; - ze.setCompressedSize(ZipLong.getValue(cfh, off)); - off += WORD; + long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfh, off)); + ze.setTime(time); + off += WORD; - ze.setSize(ZipLong.getValue(cfh, off)); - off += WORD; + ze.setCrc(ZipLong.getValue(cfh, off)); + off += WORD; - int fileNameLen = ZipShort.getValue(cfh, off); - off += SHORT; + ze.setCompressedSize(ZipLong.getValue(cfh, off)); + off += WORD; - int extraLen = ZipShort.getValue(cfh, off); - off += SHORT; + ze.setSize(ZipLong.getValue(cfh, off)); + off += WORD; - int commentLen = ZipShort.getValue(cfh, off); - off += SHORT; + int fileNameLen = ZipShort.getValue(cfh, off); + off += SHORT; - off += SHORT; // disk number + int extraLen = ZipShort.getValue(cfh, off); + off += SHORT; - ze.setInternalAttributes(ZipShort.getValue(cfh, off)); - off += SHORT; + int commentLen = ZipShort.getValue(cfh, off); + off += SHORT; - ze.setExternalAttributes(ZipLong.getValue(cfh, off)); - off += WORD; + int diskStart = ZipShort.getValue(cfh, off); + off += SHORT; - byte[] fileName = new byte[fileNameLen]; - archive.readFully(fileName); - ze.setName(entryEncoding.decode(fileName)); + ze.setInternalAttributes(ZipShort.getValue(cfh, off)); + off += SHORT; - // LFH offset, - OffsetEntry offset = new OffsetEntry(); - offset.headerOffset = ZipLong.getValue(cfh, off); - // data offset will be filled later - entries.put(ze, offset); + ze.setExternalAttributes(ZipLong.getValue(cfh, off)); + off += WORD; - nameMap.put(ze.getName(), ze); + byte[] fileName = new byte[fileNameLen]; + archive.readFully(fileName); + ze.setName(entryEncoding.decode(fileName), fileName); - byte[] cdExtraData = new byte[extraLen]; - archive.readFully(cdExtraData); - ze.setCentralDirectoryExtra(cdExtraData); + // LFH offset, + OffsetEntry offset = new OffsetEntry(); + offset.headerOffset = ZipLong.getValue(cfh, off); + // data offset will be filled later + entries.put(ze, offset); - byte[] comment = new byte[commentLen]; - archive.readFully(comment); - ze.setComment(entryEncoding.decode(comment)); + nameMap.put(ze.getName(), ze); - archive.readFully(signatureBytes); - sig = ZipLong.getValue(signatureBytes); + byte[] cdExtraData = new byte[extraLen]; + archive.readFully(cdExtraData); + ze.setCentralDirectoryExtra(cdExtraData); + + setSizesAndOffsetFromZip64Extra(ze, offset, diskStart); + + byte[] comment = new byte[commentLen]; + archive.readFully(comment); + ze.setComment(entryEncoding.decode(comment)); + + if (!hasUTF8Flag && useUnicodeExtraFields) { + noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); + } + } + + /** + * If the entry holds a Zip64 extended information extra field, + * read sizes from there if the entry's sizes are set to + * 0xFFFFFFFFF, do the same for the offset of the local file + * header. + * + *

    Ensures the Zip64 extra either knows both compressed and + * uncompressed size or neither of both as the internal logic in + * ExtraFieldUtils forces the field to create local header data + * even if they are never used - and here a field with only one + * size would be invalid.

    + */ + private void setSizesAndOffsetFromZip64Extra(ZipEntry ze, + OffsetEntry offset, + int diskStart) + throws IOException { + Zip64ExtendedInformationExtraField z64 = + (Zip64ExtendedInformationExtraField) + ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); + if (z64 != null) { + boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC; + boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC; + boolean hasRelativeHeaderOffset = + offset.headerOffset == ZIP64_MAGIC; + z64.reparseCentralDirectoryData(hasUncompressedSize, + hasCompressedSize, + hasRelativeHeaderOffset, + diskStart == ZIP64_MAGIC_SHORT); + + if (hasUncompressedSize) { + ze.setSize(z64.getSize().getLongValue()); + } else if (hasCompressedSize) { + z64.setSize(new ZipEightByteInteger(ze.getSize())); + } + + if (hasCompressedSize) { + ze.setCompressedSize(z64.getCompressedSize().getLongValue()); + } else if (hasUncompressedSize) { + z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); + } - if (!hasUTF8Flag && useUnicodeExtraFields) { - noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); + if (hasRelativeHeaderOffset) { + offset.headerOffset = + z64.getRelativeHeaderOffset().getLongValue(); } } - return noUTF8Flag; } + /** + * Length of the "End of central directory record" - which is + * supposed to be the last structure of the archive - without file + * comment. + */ private static final int MIN_EOCD_SIZE = /* end of central dir signature */ WORD /* number of this disk */ + SHORT @@ -429,9 +583,19 @@ public class ZipFile { /* the starting disk number */ + WORD /* zipfile comment length */ + SHORT; + /** + * Maximum length of the "End of central directory record" with a + * file comment. + */ private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE - /* maximum length of zipfile comment */ + 0xFFFF; + /* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT; + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "End of central directory + * record" relative to the start of the "End of central directory + * record". + */ private static final int CFD_LOCATOR_OFFSET = /* end of central dir signature */ WORD /* number of this disk */ + SHORT @@ -444,18 +608,133 @@ public class ZipFile { /* size of the central directory */ + WORD; /** - * Searches for the "End of central dir record", parses + * Length of the "Zip64 end of central directory locator" - which + * should be right in front of the "end of central directory + * record" if one is present at all. + */ + private static final int ZIP64_EOCDL_LENGTH = + /* zip64 end of central dir locator sig */ WORD + /* number of the disk with the start */ + /* start of the zip64 end of */ + /* central directory */ + WORD + /* relative offset of the zip64 */ + /* end of central directory record */ + DWORD + /* total number of disks */ + WORD; + + /** + * Offset of the field that holds the location of the "Zip64 end + * of central directory record" inside the "Zip64 end of central + * directory locator" relative to the start of the "Zip64 end of + * central directory locator". + */ + private static final int ZIP64_EOCDL_LOCATOR_OFFSET = + /* zip64 end of central dir locator sig */ WORD + /* number of the disk with the start */ + /* start of the zip64 end of */ + /* central directory */ + WORD; + + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the start of the "Zip64 end of + * central directory record". + */ + private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET = + /* zip64 end of central dir */ + /* signature */ WORD + /* size of zip64 end of central */ + /* directory record */ + DWORD + /* version made by */ + SHORT + /* version needed to extract */ + SHORT + /* number of this disk */ + WORD + /* number of the disk with the */ + /* start of the central directory */ + WORD + /* total number of entries in the */ + /* central directory on this disk */ + DWORD + /* total number of entries in the */ + /* central directory */ + DWORD + /* size of the central directory */ + DWORD; + + /** + * Searches for either the "Zip64 end of central directory + * locator" or 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 = tryToLocateSignature(MIN_EOCD_SIZE + ZIP64_EOCDL_LENGTH, + MAX_EOCD_SIZE + ZIP64_EOCDL_LENGTH, + ZipOutputStream + .ZIP64_EOCD_LOC_SIG); + if (!found) { + // not a ZIP64 archive + positionAtCentralDirectory32(); + } else { + positionAtCentralDirectory64(); + } + } + + /** + * Parses the "Zip64 end of central directory locator", + * finds the "Zip64 end of central directory record" using the + * parsed information, parses that and positions the stream at the + * first central directory record. + */ + private void positionAtCentralDirectory64() + throws IOException { + skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET); + byte[] zip64EocdOffset = new byte[DWORD]; + archive.readFully(zip64EocdOffset); + archive.seek(ZipEightByteInteger.getLongValue(zip64EocdOffset)); + byte[] sig = new byte[WORD]; + archive.readFully(sig); + if (sig[POS_0] != ZipOutputStream.ZIP64_EOCD_SIG[POS_0] + || sig[POS_1] != ZipOutputStream.ZIP64_EOCD_SIG[POS_1] + || sig[POS_2] != ZipOutputStream.ZIP64_EOCD_SIG[POS_2] + || sig[POS_3] != ZipOutputStream.ZIP64_EOCD_SIG[POS_3] + ) { + throw new ZipException("archive's ZIP64 end of central " + + "directory locator is corrupt."); + } + skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET + - WORD /* signature has already been read */); + byte[] cfdOffset = new byte[DWORD]; + archive.readFully(cfdOffset); + archive.seek(ZipEightByteInteger.getLongValue(cfdOffset)); + } + + /** + * Searches for the "End of central dir record", parses + * it and positions the stream at the first central directory + * record. + */ + private void positionAtCentralDirectory32() + throws IOException { + boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE, + ZipOutputStream.EOCD_SIG); + if (!found) { + throw new ZipException("archive is not a ZIP archive"); + } + skipBytes(CFD_LOCATOR_OFFSET); + byte[] cfdOffset = new byte[WORD]; + archive.readFully(cfdOffset); + archive.seek(ZipLong.getValue(cfdOffset)); + } + + /** + * Searches the archive backwards from minDistance to maxDistance + * for the given signature, positions the RandomaccessFile right + * at the signature if it has been found. + */ + private boolean tryToLocateSignature(long minDistanceFromEnd, + long maxDistanceFromEnd, + byte[] sig) throws IOException { boolean found = false; - long off = archive.length() - MIN_EOCD_SIZE; + long off = archive.length() - minDistanceFromEnd; final long stopSearching = - Math.max(0L, archive.length() - MAX_EOCD_SIZE); + Math.max(0L, archive.length() - maxDistanceFromEnd); if (off >= 0) { - final byte[] sig = ZipOutputStream.EOCD_SIG; for (; off >= stopSearching; off--) { archive.seek(off); int curr = archive.read(); @@ -477,13 +756,25 @@ public class ZipFile { } } } - if (!found) { - throw new ZipException("archive is not a ZIP archive"); + if (found) { + archive.seek(off); + } + return found; + } + + /** + * Skips the given number of bytes or throws an EOFException if + * skipping failed. + */ + private void skipBytes(final int count) throws IOException { + int totalSkipped = 0; + while (totalSkipped < count) { + int skippedNow = archive.skipBytes(count - totalSkipped); + if (skippedNow <= 0) { + throw new EOFException(); + } + totalSkipped += skippedNow; } - archive.seek(off + CFD_LOCATOR_OFFSET); - byte[] cfdOffset = new byte[WORD]; - archive.readFully(cfdOffset); - archive.seek(ZipLong.getValue(cfdOffset)); } /** @@ -508,12 +799,19 @@ public class ZipFile { *

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

    */ - private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag) + private void resolveLocalFileHeaderData(Map + entriesWithoutUTF8Flag) throws IOException { - Enumeration e = Collections.enumeration(new HashSet(entries.keySet())); - while (e.hasMoreElements()) { - ZipEntry ze = (ZipEntry) e.nextElement(); - OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze); + // changing the name of a ZipEntry is going to change + // the hashcode - see COMPRESS-164 + // Map needs to be reconstructed in order to keep central + // directory order + Map origMap = + new LinkedHashMap(entries); + entries.clear(); + for (Map.Entry ent : origMap.entrySet()) { + ZipEntry ze = ent.getKey(); + OffsetEntry offsetEntry = ent.getValue(); long offset = offsetEntry.headerOffset; archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); byte[] b = new byte[SHORT]; @@ -525,75 +823,28 @@ public class ZipFile { while (lenToSkip > 0) { int skipped = archive.skipBytes(lenToSkip); if (skipped <= 0) { - throw new RuntimeException("failed to skip file name in" - + " local file header"); + throw new IOException("failed to skip file name in" + + " local file header"); } lenToSkip -= skipped; - } + } byte[] localExtraData = new byte[extraFieldLen]; archive.readFully(localExtraData); ze.setExtra(localExtraData); - /*dataOffsets.put(ze, - new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH - + SHORT + SHORT + fileNameLen + extraFieldLen)); - */ offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH + SHORT + SHORT + fileNameLen + extraFieldLen; if (entriesWithoutUTF8Flag.containsKey(ze)) { - // changing the name of a ZipEntry is going to change - // the hashcode - // - see https://issues.apache.org/jira/browse/COMPRESS-164 - entries.remove(ze); - setNameAndCommentFromExtraFields(ze, - (NameAndComment) - entriesWithoutUTF8Flag.get(ze)); - entries.put(ze, offsetEntry); + String orig = ze.getName(); + NameAndComment nc = entriesWithoutUTF8Flag.get(ze); + ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name, + nc.comment); + if (!orig.equals(ze.getName())) { + nameMap.remove(orig); + nameMap.put(ze.getName(), ze); + } } - } - } - - /** - * Convert a DOS date/time field to a Date object. - * - * @param zipDosTime contains the stored DOS time. - * @return a Date instance corresponding to the given time. - */ - protected static Date fromDosTime(ZipLong zipDosTime) { - long dosTime = zipDosTime.getValue(); - return new Date(dosToJavaTime(dosTime)); - } - - /* - * Converts DOS time to Java time (number of milliseconds since epoch). - */ - private static long dosToJavaTime(long dosTime) { - Calendar cal = Calendar.getInstance(); - // CheckStyle:MagicNumberCheck OFF - no point - cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); - cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); - cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); - cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); - cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); - cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); - // CheckStyle:MagicNumberCheck ON - return cal.getTime().getTime(); - } - - - /** - * Retrieve a String from the given bytes using the encoding set - * for this ZipFile. - * - * @param bytes the byte array to transform - * @return String obtained by using the given encoding - * @throws ZipException if the encoding cannot be recognized. - */ - protected String getString(byte[] bytes) throws ZipException { - try { - return ZipEncodingHelper.getZipEncoding(encoding).decode(bytes); - } catch (IOException ex) { - throw new ZipException("Failed to decode name: " + ex.getMessage()); + entries.put(ze, offsetEntry); } } @@ -613,64 +864,6 @@ public class ZipFile { return true; } - /** - * If the entry has Unicode*ExtraFields and the CRCs of the - * names/comments match those of the extra fields, transfer the - * known Unicode values from the extra field. - */ - private void setNameAndCommentFromExtraFields(ZipEntry ze, - NameAndComment nc) { - UnicodePathExtraField name = (UnicodePathExtraField) - ze.getExtraField(UnicodePathExtraField.UPATH_ID); - String originalName = ze.getName(); - String newName = getUnicodeStringIfOriginalMatches(name, nc.name); - if (newName != null && !originalName.equals(newName)) { - ze.setName(newName); - nameMap.remove(originalName); - nameMap.put(newName, ze); - } - - if (nc.comment != null && nc.comment.length > 0) { - UnicodeCommentExtraField cmt = (UnicodeCommentExtraField) - ze.getExtraField(UnicodeCommentExtraField.UCOM_ID); - String newComment = - getUnicodeStringIfOriginalMatches(cmt, nc.comment); - if (newComment != null) { - ze.setComment(newComment); - } - } - } - - /** - * If the stored CRC matches the one of the given name, return the - * Unicode name of the given field. - * - *

    If the field is null or the CRCs don't match, return null - * instead.

    - */ - private String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f, - byte[] orig) { - if (f != null) { - CRC32 crc32 = new CRC32(); - crc32.update(orig); - long origCRC32 = crc32.getValue(); - - if (origCRC32 == f.getNameCRC32()) { - try { - return ZipEncodingHelper - .UTF8_ZIP_ENCODING.decode(f.getUnicodeName()); - } catch (IOException ex) { - // UTF-8 unsupported? should be impossible the - // Unicode*ExtraField must contain some bad bytes - - // TODO log this anywhere? - return null; - } - } - } - return null; - } - /** * InputStream that delegates requests to the underlying * RandomAccessFile, making sure that only bytes from a certain @@ -686,6 +879,7 @@ public class ZipFile { loc = start; } + @Override public int read() throws IOException { if (remaining-- <= 0) { if (addDummyByte) { @@ -700,6 +894,7 @@ public class ZipFile { } } + @Override public int read(byte[] b, int off, int len) throws IOException { if (remaining <= 0) { if (addDummyByte) { @@ -746,4 +941,32 @@ public class ZipFile { this.comment = comment; } } + + /** + * Compares two ZipEntries based on their offset within the archive. + * + *

    Won't return any meaningful results if one of the entries + * isn't part of the archive at all.

    + * + * @since Ant 1.9.0 + */ + private final Comparator OFFSET_COMPARATOR = + new Comparator() { + public int compare(ZipEntry e1, ZipEntry e2) { + if (e1 == e2) { + return 0; + } + + OffsetEntry off1 = entries.get(e1); + OffsetEntry off2 = entries.get(e2); + if (off1 == null) { + return 1; + } + if (off2 == null) { + return -1; + } + long val = (off1.headerOffset - off2.headerOffset); + return val == 0 ? 0 : val < 0 ? -1 : +1; + } + }; } diff --git a/src/main/org/apache/tools/zip/ZipLong.java b/src/main/org/apache/tools/zip/ZipLong.java index 324d0f2ee..78d8e8004 100644 --- a/src/main/org/apache/tools/zip/ZipLong.java +++ b/src/main/org/apache/tools/zip/ZipLong.java @@ -18,6 +18,9 @@ package org.apache.tools.zip; +import static org.apache.tools.zip.ZipConstants.BYTE_MASK; +import static org.apache.tools.zip.ZipConstants.WORD; + /** * Utility class that represents a four byte integer with conversion * rules for the big endian byte order of ZIP files. @@ -25,8 +28,7 @@ package org.apache.tools.zip; */ public final class ZipLong implements Cloneable { - private static final int WORD = 4; - private static final int BYTE_MASK = 0xFF; + //private static final int BYTE_BIT_SIZE = 8; private static final int BYTE_1 = 1; private static final int BYTE_1_MASK = 0xFF00; @@ -40,7 +42,26 @@ public final class ZipLong implements Cloneable { private static final long BYTE_3_MASK = 0xFF000000L; private static final int BYTE_3_SHIFT = 24; - private long value; + private final long value; + + /** Central File Header Signature */ + public static final ZipLong CFH_SIG = new ZipLong(0X02014B50L); + + /** Local File Header Signature */ + public static final ZipLong LFH_SIG = new ZipLong(0X04034B50L); + + /** + * Data Descriptor signature + * @since 1.1 + */ + public static final ZipLong DD_SIG = new ZipLong(0X08074B50L); + + /** + * Value stored in size and similar fields if ZIP64 extensions are + * used. + * @since 1.3 + */ + static final ZipLong ZIP64_MAGIC = new ZipLong(ZipConstants.ZIP64_MAGIC); /** * Create instance from a number. @@ -106,7 +127,7 @@ public final class ZipLong implements Cloneable { * 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 + * @return the corresponding Java long value */ public static long getValue(byte[] bytes, int offset) { long value = (bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; @@ -119,7 +140,7 @@ public final class ZipLong implements Cloneable { /** * 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 + * @return the corresponding Java long value */ public static long getValue(byte[] bytes) { return getValue(bytes, 0); @@ -131,6 +152,7 @@ public final class ZipLong implements Cloneable { * @return true if the objects are equal * @since 1.1 */ + @Override public boolean equals(Object o) { if (o == null || !(o instanceof ZipLong)) { return false; @@ -143,10 +165,12 @@ public final class ZipLong implements Cloneable { * @return the value stored in the ZipLong * @since 1.1 */ + @Override public int hashCode() { return (int) value; } + @Override public Object clone() { try { return super.clone(); @@ -155,4 +179,9 @@ public final class ZipLong implements Cloneable { throw new RuntimeException(cnfe); } } + + @Override + public String toString() { + return "ZipLong value: " + value; + } } diff --git a/src/main/org/apache/tools/zip/ZipOutputStream.java b/src/main/org/apache/tools/zip/ZipOutputStream.java index 0d222912e..c7a7a524b 100644 --- a/src/main/org/apache/tools/zip/ZipOutputStream.java +++ b/src/main/org/apache/tools/zip/ZipOutputStream.java @@ -27,7 +27,6 @@ import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -35,6 +34,15 @@ import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.ZipException; +import static org.apache.tools.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION; +import static org.apache.tools.zip.ZipConstants.DWORD; +import static org.apache.tools.zip.ZipConstants.INITIAL_VERSION; +import static org.apache.tools.zip.ZipConstants.SHORT; +import static org.apache.tools.zip.ZipConstants.WORD; +import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC; +import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC_SHORT; +import static org.apache.tools.zip.ZipConstants.ZIP64_MIN_VERSION; + /** * Reimplementation of {@link java.util.zip.ZipOutputStream * java.util.zip.ZipOutputStream} that does handle the extended @@ -54,13 +62,22 @@ import java.util.zip.ZipException; * uncompressed size information is required before {@link * #putNextEntry putNextEntry} can be called.

    * + *

    As of Apache Ant 1.9.0 it transparently supports Zip64 + * extensions and thus individual entries and archives larger than 4 + * GB or with more than 65536 entries in most cases but explicit + * control is provided via {@link #setUseZip64}. If the stream can not + * user RandomAccessFile and you try to write a ZipEntry of + * unknown size then Zip64 extensions will be disabled by default.

    */ public class ZipOutputStream extends FilterOutputStream { - private static final int BYTE_MASK = 0xFF; - private static final int SHORT = 2; - private static final int WORD = 4; private static final int BUFFER_SIZE = 512; + + /** + * indicates if this archive is finished. + */ + private boolean finished = false; + /* * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs * when it gets handed a really big buffer. See @@ -99,22 +116,17 @@ public class ZipOutputStream extends FilterOutputStream { /** * General purpose flag, which indicates that filenames are * written in utf-8. + * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead */ - public static final int UFT8_NAMES_FLAG = 1 << 11; - - /** - * General purpose flag, which indicates that filenames are - * written in utf-8. - * @deprecated use {@link #UFT8_NAMES_FLAG} instead - */ - public static final int EFS_FLAG = UFT8_NAMES_FLAG; + @Deprecated + public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG; /** * Current entry. * * @since 1.1 */ - private ZipEntry entry; + private CurrentEntry entry; /** * The file comment. @@ -150,7 +162,7 @@ public class ZipOutputStream extends FilterOutputStream { * * @since 1.1 */ - private final List entries = new LinkedList(); + private final List entries = new LinkedList(); /** * CRC instance to avoid parsing DEFLATED data twice. @@ -166,21 +178,6 @@ public class ZipOutputStream extends FilterOutputStream { */ 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. * @@ -214,7 +211,7 @@ public class ZipOutputStream extends FilterOutputStream { * * @since 1.1 */ - private final Map offsets = new HashMap(); + private final Map offsets = new HashMap(); /** * The encoding to use for filenames and the file comment. @@ -241,17 +238,11 @@ public class ZipOutputStream extends FilterOutputStream { /** * 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); + protected final Deflater def = new Deflater(level, true); /** - * This buffer servers as a Deflater. + * This buffer serves as a Deflater. * *

    This attribute is only protected to provide a level of API * backwards compatibility. This class used to extend {@link @@ -269,7 +260,7 @@ public class ZipOutputStream extends FilterOutputStream { * * @since 1.14 */ - private RandomAccessFile raf = null; + private final RandomAccessFile raf; /** * whether to use the general purpose bit flag when writing UTF-8 @@ -285,8 +276,14 @@ public class ZipOutputStream extends FilterOutputStream { /** * whether to create UnicodePathExtraField-s for each entry. */ - private UnicodeExtraFieldPolicy createUnicodeExtraFields = - UnicodeExtraFieldPolicy.NEVER; + private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER; + + /** + * Whether anything inside this archive has used a ZIP64 feature. + */ + private boolean hasUsedZip64 = false; + + private Zip64Mode zip64Mode = Zip64Mode.AsNeeded; /** * Creates a new ZIP OutputStream filtering the underlying stream. @@ -295,6 +292,7 @@ public class ZipOutputStream extends FilterOutputStream { */ public ZipOutputStream(OutputStream out) { super(out); + this.raf = null; } /** @@ -306,21 +304,22 @@ public class ZipOutputStream extends FilterOutputStream { */ public ZipOutputStream(File file) throws IOException { super(null); - + RandomAccessFile _raf = null; try { - raf = new RandomAccessFile(file, "rw"); - raf.setLength(0); + _raf = new RandomAccessFile(file, "rw"); + _raf.setLength(0); } catch (IOException e) { - if (raf != null) { + if (_raf != null) { try { - raf.close(); - } catch (IOException inner) { + _raf.close(); + } catch (IOException inner) { // NOPMD // ignore } - raf = null; + _raf = null; } out = new FileOutputStream(file); } + raf = _raf; } /** @@ -349,7 +348,9 @@ public class ZipOutputStream extends FilterOutputStream { public void setEncoding(final String encoding) { this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); - useUTF8Flag = useUTF8Flag && ZipEncodingHelper.isUTF8(encoding); + if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) { + useUTF8Flag = false; + } } /** @@ -393,23 +394,79 @@ public class ZipOutputStream extends FilterOutputStream { } /** - * Finishs writing the contents and closes this as well as the - * underlying stream. + * Whether Zip64 extensions will be used. * - * @since 1.1 - * @throws IOException on error + *

    When setting the mode to {@link Zip64Mode#Never Never}, + * {@link #putNextEntry}, {@link #closeEntry}, {@link + * #finish} or {@link #close} may throw a {@link + * Zip64RequiredException} if the entry's size or the total size + * of the archive exceeds 4GB or there are more than 65536 entries + * inside the archive. Any archive created in this mode will be + * readable by implementations that don't support Zip64.

    + * + *

    When setting the mode to {@link Zip64Mode#Always Always}, + * Zip64 extensions will be used for all entries. Any archive + * created in this mode may be unreadable by implementations that + * don't support Zip64 even if all its contents would be.

    + * + *

    When setting the mode to {@link Zip64Mode#AsNeeded + * AsNeeded}, Zip64 extensions will transparently be used for + * those entries that require them. This mode can only be used if + * the uncompressed size of the {@link ZipEntry} is known + * when calling {@link #putNextEntry} or the archive is written + * to a seekable output (i.e. you have used the {@link + * #ZipOutputStream(java.io.File) File-arg constructor}) - + * this mode is not valid when the output stream is not seekable + * and the uncompressed size is unknown when {@link + * #putNextEntry} is called.

    + * + *

    If no entry inside the resulting archive requires Zip64 + * extensions then {@link Zip64Mode#Never Never} will create the + * smallest archive. {@link Zip64Mode#AsNeeded AsNeeded} will + * create a slightly bigger archive if the uncompressed size of + * any entry has initially been unknown and create an archive + * identical to {@link Zip64Mode#Never Never} otherwise. {@link + * Zip64Mode#Always Always} will create an archive that is at + * least 24 bytes per entry bigger than the one {@link + * Zip64Mode#Never Never} would create.

    + * + *

    Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless + * {@link #putNextEntry} is called with an entry of unknown + * size and data is written to a non-seekable stream - in this + * case the default is {@link Zip64Mode#Never Never}.

    + * + * @since 1.3 + */ + public void setUseZip64(Zip64Mode mode) { + zip64Mode = mode; + } + + /** + * {@inheritDoc} + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link #setUseZip64} is {@link Zip64Mode#Never}. */ public void finish() throws IOException { - closeEntry(); + if (finished) { + throw new IOException("This archive has already been finished"); + } + + if (entry != null) { + closeEntry(); + } + cdOffset = written; - for (Iterator i = entries.iterator(); i.hasNext(); ) { - writeCentralFileHeader((ZipEntry) i.next()); + for (ZipEntry ze : entries) { + writeCentralFileHeader(ze); } cdLength = written - cdOffset; + writeZip64CentralDirectory(); writeCentralDirectoryEnd(); offsets.clear(); entries.clear(); def.end(); + finished = true; } /** @@ -417,80 +474,206 @@ public class ZipOutputStream extends FilterOutputStream { * * @since 1.1 * @throws IOException on error + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size exceeds 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. */ public void closeEntry() throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + if (entry == null) { - return; + throw new IOException("No current entry to close"); } + if (!entry.hasWritten) { + write(new byte[0], 0, 0); + } + + flushDeflater(); + + final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); + long bytesWritten = written - entry.dataStart; long realCrc = crc.getValue(); crc.reset(); - if (entry.getMethod() == DEFLATED) { + final boolean actuallyNeedsZip64 = + handleSizesAndCrc(bytesWritten, realCrc, effectiveMode); + + if (raf != null) { + rewriteSizesAndCrc(actuallyNeedsZip64); + } + + writeDataDescriptor(entry.entry); + entry = null; + } + + /** + * Ensures all bytes sent to the deflater are written to the stream. + */ + private void flushDeflater() throws IOException { + if (entry.entry.getMethod() == DEFLATED) { def.finish(); while (!def.finished()) { deflate(); } + } + } - entry.setSize(adjustToLong(def.getTotalIn())); - entry.setCompressedSize(adjustToLong(def.getTotalOut())); - entry.setCrc(realCrc); + /** + * Ensures the current entry's size and CRC information is set to + * the values just written, verifies it isn't too big in the + * Zip64Mode.Never case and returns whether the entry would + * require a Zip64 extra field. + */ + private boolean handleSizesAndCrc(long bytesWritten, long crc, + Zip64Mode effectiveMode) + throws ZipException { + if (entry.entry.getMethod() == DEFLATED) { + /* It turns out def.getBytesRead() returns wrong values if + * the size exceeds 4 GB on Java < Java7 + entry.entry.setSize(def.getBytesRead()); + */ + entry.entry.setSize(entry.bytesRead); + entry.entry.setCompressedSize(bytesWritten); + entry.entry.setCrc(crc); def.reset(); - - written += entry.getCompressedSize(); } else if (raf == null) { - if (entry.getCrc() != realCrc) { + if (entry.entry.getCrc() != crc) { throw new ZipException("bad CRC checksum for entry " - + entry.getName() + ": " - + Long.toHexString(entry.getCrc()) + + entry.entry.getName() + ": " + + Long.toHexString(entry.entry.getCrc()) + " instead of " - + Long.toHexString(realCrc)); + + Long.toHexString(crc)); } - if (entry.getSize() != written - dataStart) { + if (entry.entry.getSize() != bytesWritten) { throw new ZipException("bad size for entry " - + entry.getName() + ": " - + entry.getSize() + + entry.entry.getName() + ": " + + entry.entry.getSize() + " instead of " - + (written - dataStart)); + + bytesWritten); } } else { /* method is STORED and we used RandomAccessFile */ - long size = written - dataStart; + entry.entry.setSize(bytesWritten); + entry.entry.setCompressedSize(bytesWritten); + entry.entry.setCrc(crc); + } - entry.setSize(size); - entry.setCompressedSize(size); - entry.setCrc(realCrc); + final boolean actuallyNeedsZip64 = effectiveMode == Zip64Mode.Always + || entry.entry.getSize() >= ZIP64_MAGIC + || entry.entry.getCompressedSize() >= ZIP64_MAGIC; + if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException + .getEntryTooBigMessage(entry.entry)); } + return actuallyNeedsZip64; + } - // If random access output, write the local file header containing - // the correct CRC and compressed/uncompressed sizes - if (raf != null) { - long save = raf.getFilePointer(); + /** + * When using random access output, write the local file header + * and potentiall the ZIP64 extra containing the correct CRC and + * compressed/uncompressed sizes. + */ + private void rewriteSizesAndCrc(boolean actuallyNeedsZip64) + throws IOException { + 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); + raf.seek(entry.localDataStart); + writeOut(ZipLong.getBytes(entry.entry.getCrc())); + if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) { + writeOut(ZipLong.getBytes(entry.entry.getCompressedSize())); + writeOut(ZipLong.getBytes(entry.entry.getSize())); + } else { + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); } - writeDataDescriptor(entry); - entry = null; + if (hasZip64Extra(entry.entry)) { + // seek to ZIP64 extra, skip header and size information + raf.seek(entry.localDataStart + 3 * WORD + 2 * SHORT + + getName(entry.entry).limit() + 2 * SHORT); + // inside the ZIP64 extra uncompressed size comes + // first, unlike the LFH, CD or data descriptor + writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize())); + writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize())); + + if (!actuallyNeedsZip64) { + // do some cleanup: + // * rewrite version needed to extract + raf.seek(entry.localDataStart - 5 * SHORT); + writeOut(ZipShort.getBytes(INITIAL_VERSION)); + + // * remove ZIP64 extra so it doesn't get written + // to the central directory + entry.entry.removeExtraField(Zip64ExtendedInformationExtraField + .HEADER_ID); + entry.entry.setExtra(); + + // * reset hasUsedZip64 if it has been set because + // of this entry + if (entry.causedUseOfZip64) { + hasUsedZip64 = false; + } + } + } + raf.seek(save); } /** - * Begin writing next entry. - * @param ze the entry to write - * @since 1.1 - * @throws IOException on error + * {@inheritDoc} + * @throws Zip64RequiredException if the entry's uncompressed or + * compressed size is known to exceed 4 GByte and {@link #setUseZip64} + * is {@link Zip64Mode#Never}. */ - public void putNextEntry(ZipEntry ze) throws IOException { - closeEntry(); + public void putNextEntry(ZipEntry archiveEntry) throws IOException { + if (finished) { + throw new IOException("Stream has already been finished"); + } + + if (entry != null) { + closeEntry(); + } + + entry = new CurrentEntry(archiveEntry); + entries.add(entry.entry); + + setDefaults(entry.entry); + + final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); + validateSizeInformation(effectiveMode); + + if (shouldAddZip64Extra(entry.entry, effectiveMode)) { + + Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry); + + // just a placeholder, real data will be in data + // descriptor or inserted later via RandomAccessFile + ZipEightByteInteger size = ZipEightByteInteger.ZERO; + if (entry.entry.getMethod() == STORED + && entry.entry.getSize() != -1) { + // actually, we already know the sizes + size = new ZipEightByteInteger(entry.entry.getSize()); + } + z64.setSize(size); + z64.setCompressedSize(size); + entry.entry.setExtra(); + } - entry = ze; - entries.add(entry); + if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { + def.setLevel(level); + hasCompressionLevelChanged = false; + } + writeLocalFileHeader(entry.entry); + } + /** + * Provides default values for compression method and last + * modification time. + */ + private void setDefaults(ZipEntry entry) { if (entry.getMethod() == -1) { // not specified entry.setMethod(method); } @@ -498,32 +681,63 @@ public class ZipOutputStream extends FilterOutputStream { if (entry.getTime() == -1) { // not specified entry.setTime(System.currentTimeMillis()); } + } + /** + * Throws an exception if the size is unknown for a stored entry + * that is written to a non-seekable output or the entry is too + * big to be written without Zip64 extra but the mode has been set + * to Never. + */ + private void validateSizeInformation(Zip64Mode effectiveMode) + throws ZipException { // Size/CRC not required if RandomAccessFile is used - if (entry.getMethod() == STORED && raf == null) { - if (entry.getSize() == -1) { + if (entry.entry.getMethod() == STORED && raf == null) { + if (entry.entry.getSize() == -1) { throw new ZipException("uncompressed size is required for" + " STORED method when not writing to a" + " file"); } - if (entry.getCrc() == -1) { + if (entry.entry.getCrc() == -1) { throw new ZipException("crc checksum is required for STORED" + " method when not writing to a file"); } - entry.setCompressedSize(entry.getSize()); + entry.entry.setCompressedSize(entry.entry.getSize()); } - if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { - def.setLevel(level); - hasCompressionLevelChanged = false; + if ((entry.entry.getSize() >= ZIP64_MAGIC + || entry.entry.getCompressedSize() >= ZIP64_MAGIC) + && effectiveMode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException + .getEntryTooBigMessage(entry.entry)); } - writeLocalFileHeader(entry); + } + + /** + * Whether to addd a Zip64 extended information extra field to the + * local file header. + * + *

    Returns true if

    + * + *
      + *
    • mode is Always
    • + *
    • or we already know it is going to be needed
    • + *
    • or the size is unknown and we can ensure it won't hurt + * other implementations if we add it (i.e. we can erase its + * usage
    • + *
    + */ + private boolean shouldAddZip64Extra(ZipEntry entry, Zip64Mode mode) { + return mode == Zip64Mode.Always + || entry.getSize() >= ZIP64_MAGIC + || entry.getCompressedSize() >= ZIP64_MAGIC + || (entry.getSize() == -1 + && raf != null && mode != Zip64Mode.Never); } /** * Set the file comment. * @param comment the comment - * @since 1.1 */ public void setComment(String comment) { this.comment = comment; @@ -559,6 +773,16 @@ public class ZipOutputStream extends FilterOutputStream { this.method = method; } + /** + * Whether this stream is able to write the given entry. + * + *

    May return false if it is set up to use encryption or a + * compression method that hasn't been implemented yet.

    + */ + public boolean canWriteEntryData(ZipEntry ae) { + return ZipUtil.canHandleEntryData(ae); + } + /** * Writes bytes to ZIP entry. * @param b the byte array to write @@ -566,28 +790,12 @@ public class ZipOutputStream extends FilterOutputStream { * @param length the number of bytes to write * @throws IOException on error */ + @Override public void write(byte[] b, int offset, int length) throws IOException { - if (entry.getMethod() == DEFLATED) { - if (length > 0) { - if (!def.finished()) { - if (length <= DEFLATER_BLOCK_SIZE) { - def.setInput(b, offset, length); - deflateUntilInputIsNeeded(); - } else { - final int fullblocks = length / DEFLATER_BLOCK_SIZE; - for (int i = 0; i < fullblocks; i++) { - def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, - DEFLATER_BLOCK_SIZE); - deflateUntilInputIsNeeded(); - } - final int done = fullblocks * DEFLATER_BLOCK_SIZE; - if (done < length) { - def.setInput(b, offset + done, length - done); - deflateUntilInputIsNeeded(); - } - } - } - } + ZipUtil.checkRequestedFeatures(entry.entry); + entry.hasWritten = true; + if (entry.entry.getMethod() == DEFLATED) { + writeDeflated(b, offset, length); } else { writeOut(b, offset, length); written += length; @@ -596,17 +804,29 @@ public class ZipOutputStream extends FilterOutputStream { } /** - * 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 + * write implementation for DEFLATED entries. */ - public void write(int b) throws IOException { - byte[] buff = new byte[1]; - buff[0] = (byte) (b & BYTE_MASK); - write(buff, 0, 1); + private void writeDeflated(byte[]b, int offset, int length) + throws IOException { + if (length > 0 && !def.finished()) { + entry.bytesRead += length; + if (length <= DEFLATER_BLOCK_SIZE) { + def.setInput(b, offset, length); + deflateUntilInputIsNeeded(); + } else { + final int fullblocks = length / DEFLATER_BLOCK_SIZE; + for (int i = 0; i < fullblocks; i++) { + def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, + DEFLATER_BLOCK_SIZE); + deflateUntilInputIsNeeded(); + } + final int done = fullblocks * DEFLATER_BLOCK_SIZE; + if (done < length) { + def.setInput(b, offset + done, length - done); + deflateUntilInputIsNeeded(); + } + } + } } /** @@ -614,17 +834,16 @@ public class ZipOutputStream extends FilterOutputStream { * associated with the stream. * * @exception IOException if an I/O error occurs. - * @since 1.14 + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link #setUseZip64} is {@link Zip64Mode#Never}. */ + @Override public void close() throws IOException { - finish(); - - if (raf != null) { - raf.close(); - } - if (out != null) { - out.close(); + if (!finished) { + finish(); } + destroy(); } /** @@ -632,8 +851,8 @@ public class ZipOutputStream extends FilterOutputStream { * to be written out to the stream. * * @exception IOException if an I/O error occurs. - * @since 1.14 */ + @Override public void flush() throws IOException { if (out != null) { out.flush(); @@ -648,25 +867,33 @@ public class ZipOutputStream extends FilterOutputStream { * * @since 1.1 */ - protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L); + protected static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); /** * data descriptor signature * * @since 1.1 */ - protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L); + protected static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); /** * central file header signature * * @since 1.1 */ - protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L); + protected static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); /** * end of central dir signature * * @since 1.1 */ protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); + /** + * ZIP64 end of central dir signature + */ + static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); + /** + * ZIP64 end of central dir locator signature + */ + static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); /** * Writes next block of compressed data to the output stream. @@ -678,6 +905,7 @@ public class ZipOutputStream extends FilterOutputStream { int len = def.deflate(buf, 0, buf.length); if (len > 0) { writeOut(buf, 0, len); + written += len; } } @@ -691,45 +919,13 @@ public class ZipOutputStream extends FilterOutputStream { protected void writeLocalFileHeader(ZipEntry ze) throws IOException { boolean encodable = zipEncoding.canEncode(ze.getName()); - - final ZipEncoding entryEncoding; - - if (!encodable && fallbackToUTF8) { - entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING; - } else { - entryEncoding = zipEncoding; - } - - ByteBuffer name = entryEncoding.encode(ze.getName()); + ByteBuffer name = getName(ze); if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { - - if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS - || !encodable) { - ze.addExtraField(new UnicodePathExtraField(ze.getName(), - name.array(), - name.arrayOffset(), - name.limit())); - } - - String comm = ze.getComment(); - if (comm != null && !"".equals(comm)) { - - boolean commentEncodable = this.zipEncoding.canEncode(comm); - - if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS - || !commentEncodable) { - ByteBuffer commentB = entryEncoding.encode(comm); - ze.addExtraField(new UnicodeCommentExtraField(comm, - commentB.array(), - commentB.arrayOffset(), - commentB.limit()) - ); - } - } + addUnicodeExtraFields(ze, encodable, name); } - offsets.put(ze, ZipLong.getBytes(written)); + offsets.put(ze, Long.valueOf(written)); writeOut(LFH_SIG); written += WORD; @@ -739,7 +935,8 @@ public class ZipOutputStream extends FilterOutputStream { writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable - && fallbackToUTF8); + && fallbackToUTF8, + hasZip64Extra(ze)); written += WORD; // compression method @@ -747,21 +944,33 @@ public class ZipOutputStream extends FilterOutputStream { written += SHORT; // last mod. time and date - writeOut(toDosTime(ze.getTime())); + writeOut(ZipUtil.toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length - localDataStart = written; + entry.localDataStart = written; if (zipMethod == DEFLATED || raf != null) { writeOut(LZERO); - writeOut(LZERO); - writeOut(LZERO); + if (hasZip64Extra(entry.entry)) { + // point to ZIP64 extended information extra field for + // sizes, may get rewritten once sizes are known if + // stream is seekable + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + } else { + writeOut(LZERO); + writeOut(LZERO); + } } else { writeOut(ZipLong.getBytes(ze.getCrc())); - writeOut(ZipLong.getBytes(ze.getSize())); - writeOut(ZipLong.getBytes(ze.getSize())); + byte[] size = ZipLong.ZIP64_MAGIC.getBytes(); + if (!hasZip64Extra(ze)) { + size = ZipLong.getBytes(ze.getSize()); + } + writeOut(size); + writeOut(size); } // CheckStyle:MagicNumber OFF written += 12; @@ -784,7 +993,40 @@ public class ZipOutputStream extends FilterOutputStream { writeOut(extra); written += extra.length; - dataStart = written; + entry.dataStart = written; + } + + /** + * Adds UnicodeExtra fields for name and file comment if mode is + * ALWAYS or the data cannot be encoded using the configured + * encoding. + */ + private void addUnicodeExtraFields(ZipEntry ze, boolean encodable, + ByteBuffer name) + throws IOException { + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !encodable) { + ze.addExtraField(new UnicodePathExtraField(ze.getName(), + name.array(), + name.arrayOffset(), + name.limit())); + } + + String comm = ze.getComment(); + if (comm != null && !"".equals(comm)) { + + boolean commentEncodable = zipEncoding.canEncode(comm); + + if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS + || !commentEncodable) { + ByteBuffer commentB = getEntryEncoding(ze).encode(comm); + ze.addExtraField(new UnicodeCommentExtraField(comm, + commentB.array(), + commentB.arrayOffset(), + commentB.limit()) + ); + } + } } /** @@ -799,35 +1041,60 @@ public class ZipOutputStream extends FilterOutputStream { return; } writeOut(DD_SIG); - writeOut(ZipLong.getBytes(entry.getCrc())); - writeOut(ZipLong.getBytes(entry.getCompressedSize())); - writeOut(ZipLong.getBytes(entry.getSize())); - // CheckStyle:MagicNumber OFF - written += 16; - // CheckStyle:MagicNumber ON + writeOut(ZipLong.getBytes(ze.getCrc())); + int sizeFieldSize = WORD; + if (!hasZip64Extra(ze)) { + writeOut(ZipLong.getBytes(ze.getCompressedSize())); + writeOut(ZipLong.getBytes(ze.getSize())); + } else { + sizeFieldSize = DWORD; + writeOut(ZipEightByteInteger.getBytes(ze.getCompressedSize())); + writeOut(ZipEightByteInteger.getBytes(ze.getSize())); + } + written += 2 * WORD + 2 * sizeFieldSize; } /** * Writes the central file header entry. * @param ze the entry to write * @throws IOException on error - * - * @since 1.1 + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte and {@link Zip64Mode #setUseZip64} is {@link + * Zip64Mode#Never}. */ protected void writeCentralFileHeader(ZipEntry ze) throws IOException { writeOut(CFH_SIG); written += WORD; + final long lfhOffset = offsets.get(ze).longValue(); + final boolean needsZip64Extra = hasZip64Extra(ze) + || ze.getCompressedSize() >= ZIP64_MAGIC + || ze.getSize() >= ZIP64_MAGIC + || lfhOffset >= ZIP64_MAGIC; + + if (needsZip64Extra && zip64Mode == Zip64Mode.Never) { + // must be the offset that is too big, otherwise an + // exception would have been throw in putNextEntry or + // closeEntry + throw new Zip64RequiredException(Zip64RequiredException + .ARCHIVE_TOO_BIG_MESSAGE); + } + + handleZip64Extra(ze, lfhOffset, needsZip64Extra); + // version made by // CheckStyle:MagicNumber OFF - writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20)); + writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | + (!hasUsedZip64 ? DATA_DESCRIPTOR_MIN_VERSION + : ZIP64_MIN_VERSION))); written += SHORT; final int zipMethod = ze.getMethod(); final boolean encodable = zipEncoding.canEncode(ze.getName()); writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable - && fallbackToUTF8); + && fallbackToUTF8, + needsZip64Extra); written += WORD; // compression method @@ -835,29 +1102,26 @@ public class ZipOutputStream extends FilterOutputStream { written += SHORT; // last mod. time and date - writeOut(toDosTime(ze.getTime())); + writeOut(ZipUtil.toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length writeOut(ZipLong.getBytes(ze.getCrc())); - writeOut(ZipLong.getBytes(ze.getCompressedSize())); - writeOut(ZipLong.getBytes(ze.getSize())); + if (ze.getCompressedSize() >= ZIP64_MAGIC + || ze.getSize() >= ZIP64_MAGIC) { + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + writeOut(ZipLong.ZIP64_MAGIC.getBytes()); + } else { + writeOut(ZipLong.getBytes(ze.getCompressedSize())); + writeOut(ZipLong.getBytes(ze.getSize())); + } // CheckStyle:MagicNumber OFF written += 12; // CheckStyle:MagicNumber ON - // file name length - final ZipEncoding entryEncoding; - - if (!encodable && fallbackToUTF8) { - entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING; - } else { - entryEncoding = zipEncoding; - } - - ByteBuffer name = entryEncoding.encode(ze.getName()); + ByteBuffer name = getName(ze); writeOut(ZipShort.getBytes(name.limit())); written += SHORT; @@ -872,9 +1136,9 @@ public class ZipOutputStream extends FilterOutputStream { if (comm == null) { comm = ""; } - - ByteBuffer commentB = entryEncoding.encode(comm); - + + ByteBuffer commentB = getEntryEncoding(ze).encode(comm); + writeOut(ZipShort.getBytes(commentB.limit())); written += SHORT; @@ -891,7 +1155,7 @@ public class ZipOutputStream extends FilterOutputStream { written += WORD; // relative offset of LFH - writeOut((byte[]) offsets.get(ze)); + writeOut(ZipLong.getBytes(Math.min(lfhOffset, ZIP64_MAGIC))); written += WORD; // file name @@ -907,11 +1171,36 @@ public class ZipOutputStream extends FilterOutputStream { written += commentB.limit(); } + /** + * If the entry needs Zip64 extra information inside the central + * directory then configure its data. + */ + private void handleZip64Extra(ZipEntry ze, long lfhOffset, + boolean needsZip64Extra) { + if (needsZip64Extra) { + Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze); + if (ze.getCompressedSize() >= ZIP64_MAGIC + || ze.getSize() >= ZIP64_MAGIC) { + z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); + z64.setSize(new ZipEightByteInteger(ze.getSize())); + } else { + // reset value that may have been set for LFH + z64.setCompressedSize(null); + z64.setSize(null); + } + if (lfhOffset >= ZIP64_MAGIC) { + z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset)); + } + ze.setExtra(); + } + } + /** * Writes the "End of central dir record". * @throws IOException on error - * - * @since 1.1 + * @throws Zip64RequiredException if the archive's size exceeds 4 + * GByte or there are more than 65535 entries inside the archive + * and {@link Zip64Mode #setUseZip64} is {@link Zip64Mode#Never}. */ protected void writeCentralDirectoryEnd() throws IOException { writeOut(EOCD_SIG); @@ -921,13 +1210,25 @@ public class ZipOutputStream extends FilterOutputStream { writeOut(ZERO); // number of entries - byte[] num = ZipShort.getBytes(entries.size()); + int numberOfEntries = entries.size(); + if (numberOfEntries > ZIP64_MAGIC_SHORT + && zip64Mode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException + .TOO_MANY_ENTRIES_MESSAGE); + } + if (cdOffset > ZIP64_MAGIC && zip64Mode == Zip64Mode.Never) { + throw new Zip64RequiredException(Zip64RequiredException + .ARCHIVE_TOO_BIG_MESSAGE); + } + + byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, + ZIP64_MAGIC_SHORT)); writeOut(num); writeOut(num); // length and location of CD - writeOut(ZipLong.getBytes(cdLength)); - writeOut(ZipLong.getBytes(cdOffset)); + writeOut(ZipLong.getBytes(Math.min(cdLength, ZIP64_MAGIC))); + writeOut(ZipLong.getBytes(Math.min(cdOffset, ZIP64_MAGIC))); // ZIP file comment ByteBuffer data = this.zipEncoding.encode(comment); @@ -935,21 +1236,15 @@ public class ZipOutputStream extends FilterOutputStream { writeOut(data.array(), data.arrayOffset(), data.limit()); } - /** - * 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 + * @deprecated use ZipUtil#toDosTime */ protected static ZipLong toDosTime(Date time) { - return new ZipLong(toDosTime(time.getTime())); + return ZipUtil.toDosTime(time); } /** @@ -959,24 +1254,10 @@ public class ZipOutputStream extends FilterOutputStream { * @param t number of milliseconds since the epoch * @return the date as a byte array * @since 1.26 + * @deprecated use ZipUtil#toDosTime */ protected static byte[] toDosTime(long t) { - Date time = new Date(t); - // CheckStyle:MagicNumberCheck OFF - I do not think that using constants - // here will improve the readablity - 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); - // CheckStyle:MagicNumberCheck ON + return ZipUtil.toDosTime(t); } /** @@ -1001,6 +1282,76 @@ public class ZipOutputStream extends FilterOutputStream { } } + private static final byte[] ONE = ZipLong.getBytes(1L); + + /** + * Writes the "ZIP64 End of central dir record" and + * "ZIP64 End of central dir locator". + * @throws IOException on error + * @since 1.3 + */ + protected void writeZip64CentralDirectory() throws IOException { + if (zip64Mode == Zip64Mode.Never) { + return; + } + + if (!hasUsedZip64 + && (cdOffset >= ZIP64_MAGIC || cdLength >= ZIP64_MAGIC + || entries.size() >= ZIP64_MAGIC_SHORT)) { + // actually "will use" + hasUsedZip64 = true; + } + + if (!hasUsedZip64) { + return; + } + + long offset = written; + + writeOut(ZIP64_EOCD_SIG); + // size, we don't have any variable length as we don't support + // the extensible data sector, yet + writeOut(ZipEightByteInteger + .getBytes(SHORT /* version made by */ + + SHORT /* version needed to extract */ + + WORD /* disk number */ + + WORD /* disk with central directory */ + + DWORD /* number of entries in CD on this disk */ + + DWORD /* total number of entries */ + + DWORD /* size of CD */ + + DWORD /* offset of CD */ + )); + + // version made by and version needed to extract + writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); + writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); + + // disk numbers - four bytes this time + writeOut(LZERO); + writeOut(LZERO); + + // number of entries + byte[] num = ZipEightByteInteger.getBytes(entries.size()); + writeOut(num); + writeOut(num); + + // length and location of CD + writeOut(ZipEightByteInteger.getBytes(cdLength)); + writeOut(ZipEightByteInteger.getBytes(cdOffset)); + + // no "zip64 extensible data sector" for now + + // and now the "ZIP64 end of central directory locator" + writeOut(ZIP64_EOCD_LOC_SIG); + + // disk number holding the ZIP64 EOCD record + writeOut(LZERO); + // relative offset of ZIP64 EOCD record + writeOut(ZipEightByteInteger.getBytes(offset)); + // total number of disks + writeOut(ONE); + } + /** * Write bytes to output or random access file. * @param data the byte array to write @@ -1036,13 +1387,10 @@ public class ZipOutputStream extends FilterOutputStream { * @param i the value to treat as unsigned int. * @return the unsigned int as a long. * @since 1.34 + * @deprecated use ZipUtil#adjustToLong */ protected static long adjustToLong(int i) { - if (i < 0) { - return 2 * ((long) Integer.MAX_VALUE) + 2 + i; - } else { - return i; - } + return ZipUtil.adjustToLong(i); } private void deflateUntilInputIsNeeded() throws IOException { @@ -1054,25 +1402,116 @@ public class ZipOutputStream extends FilterOutputStream { private void writeVersionNeededToExtractAndGeneralPurposeBits(final int zipMethod, final boolean - utfFallback) + utfFallback, + final boolean + zip64) throws IOException { // CheckStyle:MagicNumber OFF - int versionNeededToExtract = 10; - int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0; + int versionNeededToExtract = INITIAL_VERSION; + GeneralPurposeBit b = new GeneralPurposeBit(); + b.useUTF8ForNames(useUTF8Flag || utfFallback); if (zipMethod == DEFLATED && raf == null) { // requires version 2 as we are going to store length info // in the data descriptor - versionNeededToExtract = 20; - // bit3 set to signal, we use a data descriptor - generalPurposeFlag |= 8; + versionNeededToExtract = DATA_DESCRIPTOR_MIN_VERSION; + b.useDataDescriptor(true); + } + if (zip64) { + versionNeededToExtract = ZIP64_MIN_VERSION; } // CheckStyle:MagicNumber ON // version needed to extract writeOut(ZipShort.getBytes(versionNeededToExtract)); // general purpose bit flag - writeOut(ZipShort.getBytes(generalPurposeFlag)); + writeOut(b.encode()); + } + + /** + * Get the existing ZIP64 extended information extra field or + * create a new one and add it to the entry. + * + * @since 1.3 + */ + private Zip64ExtendedInformationExtraField getZip64Extra(ZipEntry ze) { + if (entry != null) { + entry.causedUseOfZip64 = !hasUsedZip64; + } + hasUsedZip64 = true; + Zip64ExtendedInformationExtraField z64 = + (Zip64ExtendedInformationExtraField) + ze.getExtraField(Zip64ExtendedInformationExtraField + .HEADER_ID); + if (z64 == null) { + /* + System.err.println("Adding z64 for " + ze.getName() + + ", method: " + ze.getMethod() + + " (" + (ze.getMethod() == STORED) + ")" + + ", raf: " + (raf != null)); + */ + z64 = new Zip64ExtendedInformationExtraField(); + } + + // even if the field is there already, make sure it is the first one + ze.addAsFirstExtraField(z64); + + return z64; + } + + /** + * Is there a ZIP64 extended information extra field for the + * entry? + * + * @since 1.3 + */ + private boolean hasZip64Extra(ZipEntry ze) { + return ze.getExtraField(Zip64ExtendedInformationExtraField + .HEADER_ID) + != null; + } + + /** + * If the mode is AsNeeded and the entry is a compressed entry of + * unknown size that gets written to a non-seekable stream the + * change the default to Never. + * + * @since 1.3 + */ + private Zip64Mode getEffectiveZip64Mode(ZipEntry ze) { + if (zip64Mode != Zip64Mode.AsNeeded + || raf != null + || ze.getMethod() != DEFLATED + || ze.getSize() != -1) { + return zip64Mode; + } + return Zip64Mode.Never; + } + + private ZipEncoding getEntryEncoding(ZipEntry ze) { + boolean encodable = zipEncoding.canEncode(ze.getName()); + return !encodable && fallbackToUTF8 + ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + } + + private ByteBuffer getName(ZipEntry ze) throws IOException { + return getEntryEncoding(ze).encode(ze.getName()); + } + + /** + * Closes the underlying stream/file without finishing the + * archive, the result will likely be a corrupt archive. + * + *

    This method only exists to support tests that generate + * corrupt archives so they can clean up any temporary files.

    + */ + void destroy() throws IOException { + if (raf != null) { + raf.close(); + } + if (out != null) { + out.close(); + } } /** @@ -1101,8 +1540,51 @@ public class ZipOutputStream extends FilterOutputStream { private UnicodeExtraFieldPolicy(String n) { name = n; } + @Override public String toString() { return name; } } + + /** + * Structure collecting information for the entry that is + * currently being written. + */ + private static final class CurrentEntry { + private CurrentEntry(ZipEntry entry) { + this.entry = entry; + } + /** + * Current ZIP entry. + */ + private final ZipEntry entry; + /** + * Offset for CRC entry in the local file header data for the + * current entry starts here. + */ + private long localDataStart = 0; + /** + * Data for local header data + */ + private long dataStart = 0; + /** + * Number of bytes read for the current entry (can't rely on + * Deflater#getBytesRead) when using DEFLATED. + */ + private long bytesRead = 0; + /** + * Whether current entry was the first one using ZIP64 features. + */ + private boolean causedUseOfZip64 = false; + /** + * Whether write() has been called at all. + * + *

    In order to create a valid archive {@link + * #closeEntry closeEntry} will write an empty + * array to get the CRC right if nothing has been written to + * the stream at all.

    + */ + private boolean hasWritten; + } + } diff --git a/src/main/org/apache/tools/zip/ZipShort.java b/src/main/org/apache/tools/zip/ZipShort.java index b50b2fba2..5d1d0f4a1 100644 --- a/src/main/org/apache/tools/zip/ZipShort.java +++ b/src/main/org/apache/tools/zip/ZipShort.java @@ -18,17 +18,18 @@ package org.apache.tools.zip; +import static org.apache.tools.zip.ZipConstants.BYTE_MASK; + /** * 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 static final int BYTE_MASK = 0xFF; private static final int BYTE_1_MASK = 0xFF00; private static final int BYTE_1_SHIFT = 8; - private int value; + private final int value; /** * Create instance from a number. @@ -95,7 +96,7 @@ public final class ZipShort implements Cloneable { * 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 + * @return the corresponding java int value */ public static int getValue(byte[] bytes, int offset) { int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK; @@ -106,7 +107,7 @@ public final class ZipShort implements Cloneable { /** * 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 + * @return the corresponding java int value */ public static int getValue(byte[] bytes) { return getValue(bytes, 0); @@ -118,6 +119,7 @@ public final class ZipShort implements Cloneable { * @return true if the objects are equal * @since 1.1 */ + @Override public boolean equals(Object o) { if (o == null || !(o instanceof ZipShort)) { return false; @@ -130,10 +132,12 @@ public final class ZipShort implements Cloneable { * @return the value stored in the ZipShort * @since 1.1 */ + @Override public int hashCode() { return value; } + @Override public Object clone() { try { return super.clone(); @@ -142,4 +146,9 @@ public final class ZipShort implements Cloneable { throw new RuntimeException(cnfe); } } + + @Override + public String toString() { + return "ZipShort value: " + value; + } } diff --git a/src/main/org/apache/tools/zip/ZipUtil.java b/src/main/org/apache/tools/zip/ZipUtil.java index 9d7dcf22c..2910149d0 100644 --- a/src/main/org/apache/tools/zip/ZipUtil.java +++ b/src/main/org/apache/tools/zip/ZipUtil.java @@ -17,11 +17,159 @@ */ package org.apache.tools.zip; +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.zip.CRC32; + /** * Utility class for handling DOS and Java time conversions. * @since Ant 1.8.1 */ public abstract class ZipUtil { + /** + * Smallest date/time ZIP can handle. + */ + 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 + */ + public 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 + */ + public static byte[] toDosTime(long t) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(t); + + int year = c.get(Calendar.YEAR); + if (year < 1980) { + return copy(DOS_TIME_MIN); // stop callers from changing the array + } + int month = c.get(Calendar.MONTH) + 1; + long value = ((year - 1980) << 25) + | (month << 21) + | (c.get(Calendar.DAY_OF_MONTH) << 16) + | (c.get(Calendar.HOUR_OF_DAY) << 11) + | (c.get(Calendar.MINUTE) << 5) + | (c.get(Calendar.SECOND) >> 1); + return ZipLong.getBytes(value); + } + + /** + * Assumes a negative integer really is a positive integer that + * has wrapped around and re-creates the original value. + * + *

    This methods is no longer used as of Apache Ant 1.9.0

    + * + * @param i the value to treat as unsigned int. + * @return the unsigned int as a long. + */ + public static long adjustToLong(int i) { + if (i < 0) { + return 2 * ((long) Integer.MAX_VALUE) + 2 + i; + } else { + return i; + } + } + + /** + * 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. + */ + public 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). + */ + public static long dosToJavaTime(long dosTime) { + Calendar cal = Calendar.getInstance(); + // CheckStyle:MagicNumberCheck OFF - no point + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + // CheckStyle:MagicNumberCheck ON + return cal.getTime().getTime(); + } + + /** + * If the entry has Unicode*ExtraFields and the CRCs of the + * names/comments match those of the extra fields, transfer the + * known Unicode values from the extra field. + */ + static void setNameAndCommentFromExtraFields(ZipEntry ze, + byte[] originalNameBytes, + byte[] commentBytes) { + UnicodePathExtraField name = (UnicodePathExtraField) + ze.getExtraField(UnicodePathExtraField.UPATH_ID); + String originalName = ze.getName(); + String newName = getUnicodeStringIfOriginalMatches(name, + originalNameBytes); + if (newName != null && !originalName.equals(newName)) { + ze.setName(newName); + } + + if (commentBytes != null && commentBytes.length > 0) { + UnicodeCommentExtraField cmt = (UnicodeCommentExtraField) + ze.getExtraField(UnicodeCommentExtraField.UCOM_ID); + String newComment = + getUnicodeStringIfOriginalMatches(cmt, commentBytes); + if (newComment != null) { + ze.setComment(newComment); + } + } + } + + /** + * If the stored CRC matches the one of the given name, return the + * Unicode name of the given field. + * + *

    If the field is null or the CRCs don't match, return null + * instead.

    + */ + private static + String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f, + byte[] orig) { + if (f != null) { + CRC32 crc32 = new CRC32(); + crc32.update(orig); + long origCRC32 = crc32.getValue(); + + if (origCRC32 == f.getNameCRC32()) { + try { + return ZipEncodingHelper + .UTF8_ZIP_ENCODING.decode(f.getUnicodeName()); + } catch (IOException ex) { + // UTF-8 unsupported? should be impossible the + // Unicode*ExtraField must contain some bad bytes + + // TODO log this anywhere? + return null; + } + } + } + return null; + } + /** * Create a copy of the given array - or return null if the * argument is null. @@ -35,4 +183,49 @@ public abstract class ZipUtil { return null; } + /** + * Whether this library is able to read or write the given entry. + */ + static boolean canHandleEntryData(ZipEntry entry) { + return supportsEncryptionOf(entry) && supportsMethodOf(entry); + } + + /** + * Whether this library supports the encryption used by the given + * entry. + * + * @return true if the entry isn't encrypted at all + */ + private static boolean supportsEncryptionOf(ZipEntry entry) { + return !entry.getGeneralPurposeBit().usesEncryption(); + } + + /** + * Whether this library supports the compression method used by + * the given entry. + * + * @return true if the compression method is STORED or DEFLATED + */ + private static boolean supportsMethodOf(ZipEntry entry) { + return entry.getMethod() == ZipEntry.STORED + || entry.getMethod() == ZipEntry.DEFLATED; + } + + /** + * Checks whether the entry requires features not (yet) supported + * by the library and throws an exception if it does. + */ + static void checkRequestedFeatures(ZipEntry ze) + throws UnsupportedZipFeatureException { + if (!supportsEncryptionOf(ze)) { + throw + new UnsupportedZipFeatureException(UnsupportedZipFeatureException + .Feature.ENCRYPTION, ze); + } + if (!supportsMethodOf(ze)) { + throw + new UnsupportedZipFeatureException(UnsupportedZipFeatureException + .Feature.METHOD, ze); + } + } } \ No newline at end of file diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java index 799a759ab..8fb20c105 100644 --- a/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java @@ -32,6 +32,7 @@ import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.resources.ZipResource; import org.apache.tools.zip.JarMarker; +import org.apache.tools.zip.Zip64ExtendedInformationExtraField; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipExtraField; import org.apache.tools.zip.ZipFile; @@ -79,8 +80,10 @@ public class ZipExtraFieldTest extends TestCase { zf = new ZipFile(f); ZipEntry ze = zf.getEntry("x"); assertNotNull(ze); - assertEquals(1, ze.getExtraFields().length); + assertEquals(2, ze.getExtraFields().length); assertTrue(ze.getExtraFields()[0] instanceof JarMarker); + assertTrue(ze.getExtraFields()[1] + instanceof Zip64ExtendedInformationExtraField); } finally { ZipFile.closeQuietly(zf); if (f.exists()) { diff --git a/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java b/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java index 51a77f1af..1c4976894 100644 --- a/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java +++ b/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java @@ -29,17 +29,26 @@ public class ExtraFieldUtilsTest extends TestCase implements UnixStat { super(name); } + /** + * Header-ID of a ZipExtraField not supported by Ant. + * + *

    Used to be ZipShort(1) but this is the ID of the Zip64 extra + * field.

    + */ + static final ZipShort UNRECOGNIZED_HEADER = new ZipShort(0x5555); + private AsiExtraField a; private UnrecognizedExtraField dummy; private byte[] data; private byte[] aLocal; + @Override public void setUp() { a = new AsiExtraField(); a.setMode(0755); a.setDirectory(true); dummy = new UnrecognizedExtraField(); - dummy.setHeaderId(new ZipShort(1)); + dummy.setHeaderId(UNRECOGNIZED_HEADER); dummy.setLocalFileDataData(new byte[] {0}); dummy.setCentralDirectoryData(new byte[] {0}); @@ -167,7 +176,8 @@ public class ExtraFieldUtilsTest extends TestCase implements UnixStat { public void testMergeWithUnparseableData() throws Exception { ZipExtraField d = new UnparseableExtraFieldData(); - d.parseFromLocalFileData(new byte[] {1, 0, 1, 0}, 0, 4); + byte[] b = UNRECOGNIZED_HEADER.getBytes(); + d.parseFromLocalFileData(new byte[] {b[0], b[1], 1, 0}, 0, 4); byte[] local = ExtraFieldUtils.mergeLocalFileDataData(new ZipExtraField[] {a, d}); assertEquals("local length", data.length - 1, local.length); diff --git a/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java b/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java index 5875821dc..0770b2367 100644 --- a/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java +++ b/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java @@ -38,7 +38,7 @@ public class ZipEntryTest extends TestCase { a.setDirectory(true); a.setMode(0755); UnrecognizedExtraField u = new UnrecognizedExtraField(); - u.setHeaderId(new ZipShort(1)); + u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); u.setLocalFileDataData(new byte[0]); ZipEntry ze = new ZipEntry("test/"); @@ -50,7 +50,7 @@ public class ZipEntryTest extends TestCase { assertSame(u, result[1]); UnrecognizedExtraField u2 = new UnrecognizedExtraField(); - u2.setHeaderId(new ZipShort(1)); + u2.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); u2.setLocalFileDataData(new byte[] {1}); ze.addExtraField(u2); @@ -68,7 +68,7 @@ public class ZipEntryTest extends TestCase { result = ze.getExtraFields(); assertEquals("third pass", 3, result.length); - ze.removeExtraField(new ZipShort(1)); + ze.removeExtraField(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); byte[] data3 = ze.getExtra(); result = ze.getExtraFields(); assertEquals("fourth pass", 2, result.length); @@ -77,7 +77,7 @@ public class ZipEntryTest extends TestCase { assertEquals("length fourth pass", data2.length, data3.length); try { - ze.removeExtraField(new ZipShort(1)); + ze.removeExtraField(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); fail("should be no such element"); } catch (java.util.NoSuchElementException nse) { } @@ -91,7 +91,7 @@ public class ZipEntryTest extends TestCase { a.setDirectory(true); a.setMode(0755); UnrecognizedExtraField u = new UnrecognizedExtraField(); - u.setHeaderId(new ZipShort(1)); + u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); u.setLocalFileDataData(new byte[0]); ZipEntry ze = new ZipEntry("test/"); @@ -99,12 +99,14 @@ public class ZipEntryTest extends TestCase { // merge // Header-ID 1 + length 1 + one byte of data - ze.setCentralDirectoryExtra(new byte[] {1, 0, 1, 0, 127}); + byte[] b = ExtraFieldUtilsTest.UNRECOGNIZED_HEADER.getBytes(); + ze.setCentralDirectoryExtra(new byte[] {b[0], b[1], 1, 0, 127}); ZipExtraField[] result = ze.getExtraFields(); assertEquals("first pass", 2, result.length); assertSame(a, result[0]); - assertEquals(new ZipShort(1), result[1].getHeaderId()); + assertEquals(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER, + result[1].getHeaderId()); assertEquals(new ZipShort(0), result[1].getLocalFileDataLength()); assertEquals(new ZipShort(1), result[1].getCentralDirectoryLength()); @@ -135,7 +137,7 @@ public class ZipEntryTest extends TestCase { a.setDirectory(true); a.setMode(0755); UnrecognizedExtraField u = new UnrecognizedExtraField(); - u.setHeaderId(new ZipShort(1)); + u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); u.setLocalFileDataData(new byte[0]); ZipEntry ze = new ZipEntry("test/"); @@ -143,7 +145,7 @@ public class ZipEntryTest extends TestCase { byte[] data1 = ze.getExtra(); UnrecognizedExtraField u2 = new UnrecognizedExtraField(); - u2.setHeaderId(new ZipShort(1)); + u2.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER); u2.setLocalFileDataData(new byte[] {1}); ze.addAsFirstExtraField(u2);