Browse Source

merge Zip64 support from Commons Compress

git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@1346025 13f79535-47bb-0310-9956-ffa450edef68
master
Stefan Bodewig 13 years ago
parent
commit
dee95e3acc
27 changed files with 2775 additions and 587 deletions
  1. +4
    -1
      WHATSNEW
  2. +24
    -3
      src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java
  3. +7
    -2
      src/main/org/apache/tools/zip/AsiExtraField.java
  4. +12
    -11
      src/main/org/apache/tools/zip/ExtraFieldUtils.java
  5. +4
    -4
      src/main/org/apache/tools/zip/FallbackZipEncoding.java
  6. +171
    -0
      src/main/org/apache/tools/zip/GeneralPurposeBit.java
  7. +31
    -17
      src/main/org/apache/tools/zip/Simple8BitZipEncoding.java
  8. +1
    -0
      src/main/org/apache/tools/zip/UnicodeCommentExtraField.java
  9. +1
    -0
      src/main/org/apache/tools/zip/UnicodePathExtraField.java
  10. +4
    -6
      src/main/org/apache/tools/zip/UnparseableExtraFieldData.java
  11. +89
    -0
      src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java
  12. +354
    -0
      src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java
  13. +47
    -0
      src/main/org/apache/tools/zip/Zip64Mode.java
  14. +49
    -0
      src/main/org/apache/tools/zip/Zip64RequiredException.java
  15. +59
    -0
      src/main/org/apache/tools/zip/ZipConstants.java
  16. +229
    -0
      src/main/org/apache/tools/zip/ZipEightByteInteger.java
  17. +1
    -1
      src/main/org/apache/tools/zip/ZipEncoding.java
  18. +4
    -5
      src/main/org/apache/tools/zip/ZipEncodingHelper.java
  19. +230
    -34
      src/main/org/apache/tools/zip/ZipEntry.java
  20. +448
    -225
      src/main/org/apache/tools/zip/ZipFile.java
  21. +34
    -5
      src/main/org/apache/tools/zip/ZipLong.java
  22. +739
    -257
      src/main/org/apache/tools/zip/ZipOutputStream.java
  23. +13
    -4
      src/main/org/apache/tools/zip/ZipShort.java
  24. +193
    -0
      src/main/org/apache/tools/zip/ZipUtil.java
  25. +4
    -1
      src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java
  26. +12
    -2
      src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java
  27. +11
    -9
      src/tests/junit/org/apache/tools/zip/ZipEntryTest.java

+ 4
- 1
WHATSNEW View File

@@ -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
===================================



+ 24
- 3
src/main/org/apache/tools/zip/AbstractUnicodeExtraField.java View File

@@ -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 {



+ 7
- 2
src/main/org/apache/tools/zip/AsiExtraField.java View File

@@ -45,6 +45,9 @@ import java.util.zip.ZipException;
* <p>Short is two bytes and Long is four bytes in big endian byte and
* word order, device numbers are currently not supported.</p>
*
* <p>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.</p>
*/
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();


+ 12
- 11
src/main/org/apache/tools/zip/ExtraFieldUtils.java View File

@@ -38,14 +38,15 @@ public class ExtraFieldUtils {
*
* @since 1.1
*/
private static final Map implementations;
private static final Map<ZipShort, Class<?>> implementations;

static {
implementations = new HashMap();
implementations = new HashMap<ZipShort, Class<?>>();
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<ZipExtraField> v = new ArrayList<ZipExtraField>();
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;


+ 4
- 4
src/main/org/apache/tools/zip/FallbackZipEncoding.java View File

@@ -30,7 +30,7 @@ import java.nio.ByteBuffer;
* marks leading to unreadable ZIP entries on some operating
* systems.</p>
*
* <p>Furthermore this implementation is unable to tell, whether a
* <p>Furthermore this implementation is unable to tell whether a
* given name can be safely encoded or not.</p>
*
* <p>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 <code>null</code> 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);


+ 171
- 0
src/main/org/apache/tools/zip/GeneralPurposeBit.java View File

@@ -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.
*
* <p>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.</p>
*/
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;
}
}

+ 31
- 17
src/main/org/apache/tools/zip/Simple8BitZipEncoding.java View File

@@ -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<Simple8BitChar> {
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<Simple8BitChar> 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<Simple8BitChar> temp =
new ArrayList<Simple8BitChar>(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 <code>false</code> 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 <code>null</code> 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;


+ 1
- 0
src/main/org/apache/tools/zip/UnicodeCommentExtraField.java View File

@@ -67,6 +67,7 @@ public class UnicodeCommentExtraField extends AbstractUnicodeExtraField {
super(comment, bytes);
}

/** {@inheritDoc} */
public ZipShort getHeaderId() {
return UCOM_ID;
}


+ 1
- 0
src/main/org/apache/tools/zip/UnicodePathExtraField.java View File

@@ -66,6 +66,7 @@ public class UnicodePathExtraField extends AbstractUnicodeExtraField {
super(name, bytes);
}

/** {@inheritDoc} */
public ZipShort getHeaderId() {
return UPATH_ID;
}


+ 4
- 6
src/main/org/apache/tools/zip/UnparseableExtraFieldData.java View File

@@ -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.
*
* <p>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
* <p>The header-id is artificial (and not listed as a known ID in
* {@link <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">
* APPNOTE.TXT</a>}). Since it isn't used anywhere except to satisfy the
* ZipExtraField contract it shouldn't matter anyway.</p>
* @see <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* APPNOTE.TXT">.ZIP File Format Specification</a>
*
* @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) {


+ 89
- 0
src/main/org/apache/tools/zip/UnsupportedZipFeatureException.java View File

@@ -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;
}
}
}

+ 354
- 0
src/main/org/apache/tools/zip/Zip64ExtendedInformationExtraField.java View File

@@ -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.
*
* <p>From {@link "http://www.pkware.com/documents/casestudies/APPNOTE.TXT PKWARE's APPNOTE.TXT"}
* <pre>
* 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.
* </pre></p>
*
* <p>Currently Ant doesn't support encrypting the
* central directory so the note about masking doesn't apply.</p>
*
* <p>The implementation relies on data being read from the local file
* header and assumes that both size values are always present.</p>
*
* @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}.
*
* <p>Not used for anything else</p>
*/
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.
*
* <p>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.</p>
*/
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;
}
}

+ 47
- 0
src/main/org/apache/tools/zip/Zip64Mode.java View File

@@ -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.
*
* <p>This will cause a {@link Zip64RequiredException} to be
* thrown if {@link ZipOutputStream} detects it needs Zip64
* support.</p>
*/
Never,
/**
* Use Zip64 extensions for all entries where they are required,
* don't use them for entries that clearly don't require them.
*/
AsNeeded
}

+ 49
- 0
src/main/org/apache/tools/zip/Zip64RequiredException.java View File

@@ -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);
}
}

+ 59
- 0
src/main/org/apache/tools/zip/ZipConstants.java View File

@@ -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;

}

+ 229
- 0
src/main/org/apache/tools/zip/ZipEightByteInteger.java View File

@@ -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;
}
}

+ 1
- 1
src/main/org/apache/tools/zip/ZipEncoding.java View File

@@ -68,7 +68,7 @@ interface ZipEncoding {
* character sequences are mapped to a sequence of utf-16
* words encoded in the format <code>%Uxxxx</code>. 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


+ 4
- 5
src/main/org/apache/tools/zip/ZipEncodingHelper.java View File

@@ -62,10 +62,10 @@ abstract class ZipEncodingHelper {
}
}

private static final Map simpleEncodings;
private static final Map<String, SimpleEncodingHolder> simpleEncodings;

static {
simpleEncodings = new HashMap();
simpleEncodings = new HashMap<String, SimpleEncodingHolder>();

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 <code>null</code> 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();


+ 230
- 34
src/main/org/apache/tools/zip/ZipEntry.java View File

@@ -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.
*
* <p>The extra data is expected to follow the recommendation of
* the .ZIP File Format Specification created by PKWARE Inc. :</p>
* {@link <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">
* APPNOTE.txt</a>}:</p>
* <ul>
* <li>the extra byte array consists of a sequence of extra fields</li>
* <li>each extra fields starts by a two byte header id followed by
@@ -38,11 +42,9 @@ import java.util.zip.ZipException;
*
* <p>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.</p>
* @see <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">
* .ZIP File Format Specification</a>
* 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.</p>
*
*/
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.
* <p>
* 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/*<ZipShort, ZipExtraField>*/ extraFields = null;
private LinkedHashMap<ZipShort, ZipExtraField> 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.
*
* <p>Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".</p>
*
* @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.
*
* <p>Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".</p>
*
* @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.
*
* <p>Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".</p>
*
* @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.
*
* <p>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.</p>
*/
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<ZipShort, ZipExtraField>();
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<ZipExtraField> result =
new ArrayList<ZipExtraField>(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<ZipShort, ZipExtraField>();
}
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<ZipShort, ZipExtraField> copy = extraFields;
extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
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.
*
* <p>This method will return null if this instance has not been
* read from an archive.</p>
*/
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 <code>o</code>
* @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);
}
}

+ 448
- 225
src/main/org/apache/tools/zip/ZipFile.java View File

@@ -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 <code>java.util.ZipFile</code>.
*
@@ -47,7 +53,10 @@ import java.util.zip.ZipException;
* <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
* have to reimplement all methods anyway. Like
* <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
* covers and supports compressed and uncompressed entries.</p>
* 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.</p>
*
* <p>The method signatures mimic the ones of
* <code>java.util.zip.ZipFile</code>, 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<ZipEntry, OffsetEntry> entries =
new LinkedHashMap<ZipEntry, OffsetEntry>(HASH_SIZE);

/**
* Maps String to ZipEntrys, name -> actual entry.
*/
private final Map nameMap = new HashMap(HASH_SIZE);
private final Map<String, ZipEntry> nameMap =
new HashMap<String, ZipEntry>(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</a>.
* Defaults to the platform's default character encoding.</p>
*/
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<ZipEntry, NameAndComment> 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.
*
* <p>Entries will be returned in the same order they appear
* within the archive's central directory.</p>
*
* @return all entries as {@link ZipEntry} instances
*/
public Enumeration getEntries() {
public Enumeration<ZipEntry> getEntries() {
return Collections.enumeration(entries.keySet());
}

/**
* Returns a named entry - or <code>null</code> if no entry by
* Returns all entries in physical order.
*
* <p>Entries will be returned in the same order their contents
* appear within the archive.</p>
*
* @return all entries as {@link ZipEntry} instances
*
* @since Ant 1.9.0
*/
public Enumeration<ZipEntry> 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
* <code>null</code> 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.
*
* <p>May return false if it is set up to use encryption or a
* compression method that hasn't been implemented yet.</p>
*/
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.</p>
*
* @return a Map&lt;ZipEntry, NameAndComment>&gt; 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<ZipEntry, NameAndComment> populateFromCentralDirectory()
throws IOException {
HashMap noUTF8Flag = new HashMap();
HashMap<ZipEntry, NameAndComment> noUTF8Flag =
new HashMap<ZipEntry, NameAndComment>();

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<ZipEntry, NameAndComment> 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.
*
* <p>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.</p>
*/
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 &quot;End of central dir record&quot;, 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 &quot;Zip64 end of central directory
* locator&quot; or the &quot;End of central dir record&quot;, parses
* it and positions the stream at the first central directory
* record.
*/
private void positionAtCentralDirectory()
throws IOException {
boolean found = 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 &quot;Zip64 end of central directory locator&quot;,
* finds the &quot;Zip64 end of central directory record&quot; 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 &quot;End of central dir record&quot;, 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 {
* <p>Also records the offsets for the data to read from the
* entries.</p>
*/
private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag)
private void resolveLocalFileHeaderData(Map<ZipEntry, NameAndComment>
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<ZipEntry, OffsetEntry> origMap =
new LinkedHashMap<ZipEntry, OffsetEntry>(entries);
entries.clear();
for (Map.Entry<ZipEntry, OffsetEntry> 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.
*
* <p>If the field is null or the CRCs don't match, return null
* instead.</p>
*/
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.
*
* <p>Won't return any meaningful results if one of the entries
* isn't part of the archive at all.</p>
*
* @since Ant 1.9.0
*/
private final Comparator<ZipEntry> OFFSET_COMPARATOR =
new Comparator<ZipEntry>() {
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;
}
};
}

+ 34
- 5
src/main/org/apache/tools/zip/ZipLong.java View File

@@ -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;
}
}

+ 739
- 257
src/main/org/apache/tools/zip/ZipOutputStream.java
File diff suppressed because it is too large
View File


+ 13
- 4
src/main/org/apache/tools/zip/ZipShort.java View File

@@ -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;
}
}

+ 193
- 0
src/main/org/apache/tools/zip/ZipUtil.java View File

@@ -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 <code>Date</code> to convert
* @return the date as a <code>ZipLong</code>
*/
public static ZipLong toDosTime(Date time) {
return new ZipLong(toDosTime(time.getTime()));
}

/**
* Convert a Date object to a DOS date/time field.
*
* <p>Stolen from InfoZip's <code>fileio.c</code></p>
* @param t number of milliseconds since the epoch
* @return the date as a byte array
*/
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.
*
* <p>This methods is no longer used as of Apache Ant 1.9.0</p>
*
* @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.
*
* <p>If the field is null or the CRCs don't match, return null
* instead.</p>
*/
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);
}
}
}

+ 4
- 1
src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java View File

@@ -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()) {


+ 12
- 2
src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java View File

@@ -29,17 +29,26 @@ public class ExtraFieldUtilsTest extends TestCase implements UnixStat {
super(name);
}

/**
* Header-ID of a ZipExtraField not supported by Ant.
*
* <p>Used to be ZipShort(1) but this is the ID of the Zip64 extra
* field.</p>
*/
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);


+ 11
- 9
src/tests/junit/org/apache/tools/zip/ZipEntryTest.java View File

@@ -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);


Loading…
Cancel
Save