Author: julius
Date: Tue Jan  8 21:54:15 2013
New Revision: 1430562

URL: http://svn.apache.org/viewvc?rev=1430562&view=rev
Log:
COMPRESS-210 - handle zip extra field 0x5455 - Extended Timestamp

Added:
    
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.java
    
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestampTest.java
    
commons/proper/compress/trunk/src/test/resources/COMPRESS-210_unix_time_zip_test.zip
   (with props)
Modified:
    
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java

Modified: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java?rev=1430562&r1=1430561&r2=1430562&view=diff
==============================================================================
--- 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java
 (original)
+++ 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java
 Tue Jan  8 21:54:15 2013
@@ -40,6 +40,7 @@ public class ExtraFieldUtils {
     static {
         implementations = new ConcurrentHashMap<ZipShort, Class<?>>();
         register(AsiExtraField.class);
+        register(X5455_ExtendedTimestamp.class);
         register(X7875_NewUnix.class);
         register(JarMarker.class);
         register(UnicodePathExtraField.class);

Added: 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.java?rev=1430562&view=auto
==============================================================================
--- 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.java
 (added)
+++ 
commons/proper/compress/trunk/src/main/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestamp.java
 Tue Jan  8 21:54:15 2013
@@ -0,0 +1,560 @@
+/*
+ * 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.commons.compress.archivers.zip;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.zip.ZipException;
+
+/**
+ * <p>An extra field that stores additional file and directory timestamp data
+ * for zip entries.   Each zip entry can include up to three timestamps
+ * (modify, access, create*).  The timestamps are stored as 32 bit unsigned
+ * integers representing seconds since UNIX epoch (Jan 1st, 1970, UTC).
+ * This field improves on zip's default timestamp granularity, since it
+ * allows one to store additional timestamps, and, in addition, the timestamps
+ * are stored using per-second granularity (zip's default behaviour can only 
store
+ * timestamps to the nearest <em>even</em> second).
+ * <p/>
+ * </p><p>
+ * Unfortunately, 32 (unsigned) bits can only store dates up to the year 2106,
+ * and so this extra field will eventually be obsolete.  Enjoy it while it 
lasts!
+ * </p>
+ * <ul>
+ * <li><b>modifyTime:</b>
+ * most recent time of file/directory modification
+ * (or file/dir creation if the entry has not been
+ * modified since it was created).
+ * </li>
+ * <li><b>accessTime:</b>
+ * most recent time file/directory was opened
+ * (e.g., read from disk).  Many people disable
+ * their operating systems from updating this value
+ * using the NOATIME mount option to optimize disk behaviour,
+ * and thus it's not always reliable.  In those cases
+ * it's always equal to modifyTime.
+ * </li>
+ * <li><b>*createTime:</b>
+ * modern linux file systems (e.g., ext2 and newer)
+ * do not appear to store a value like this, and so
+ * it's usually omitted altogether in the zip extra
+ * field.  Perhaps other unix systems track this.
+ * </li></ul>
+ * <p>
+ * We're using the field definition given in Info-Zip's source archive:
+ * zip-3.0.tar.gz/proginfo/extrafld.txt
+ * </p>
+ * <pre>
+ * Value         Size        Description
+ * -----         ----        -----------
+ * 0x5455        Short       tag for this extra block type ("UT")
+ * TSize         Short       total data size for this block
+ * Flags         Byte        info bits
+ * (ModTime)     Long        time of last modification (UTC/GMT)
+ * (AcTime)      Long        time of last access (UTC/GMT)
+ * (CrTime)      Long        time of original creation (UTC/GMT)
+ *
+ * Central-header version:
+ *
+ * Value         Size        Description
+ * -----         ----        -----------
+ * 0x5455        Short       tag for this extra block type ("UT")
+ * TSize         Short       total data size for this block
+ * Flags         Byte        info bits (refers to local header!)
+ * (ModTime)     Long        time of last modification (UTC/GMT)
+ * </pre>
+ */
+public class X5455_ExtendedTimestamp implements ZipExtraField, Cloneable, 
Serializable {
+    private static final ZipShort HEADER_ID = new ZipShort(0x5455);
+    private static final long serialVersionUID = 1L;
+
+    static final byte MODIFY_TIME_BIT = 1;
+    static final byte ACCESS_TIME_BIT = 2;
+    static final byte CREATE_TIME_BIT = 4;
+
+    // The 3 boolean fields (below) come from this flags byte.  The remaining 
5 bits
+    // are ignored according to the current version of the spec (December 
2012).
+    private byte flags;
+
+    // Note: even if bit1 and bit2 are set, the Central data will still not 
contain
+    // access/create fields:  only local data ever holds those!  This causes
+    // some of our implementation to look a little odd, with seemingly spurious
+    // != null and length checks.
+    private boolean bit0_modifyTimePresent;
+    private boolean bit1_accessTimePresent;
+    private boolean bit2_createTimePresent;
+
+    private ZipLong modifyTime;
+    private ZipLong accessTime;
+    private ZipLong createTime;
+
+    /**
+     * Constructor for X5455_ExtendedTimestamp.
+     */
+    public X5455_ExtendedTimestamp() {}
+
+    /**
+     * The Header-ID.
+     *
+     * @return the value for the header id for this extrafield
+     */
+    public ZipShort getHeaderId() {
+        return HEADER_ID;
+    }
+
+    /**
+     * Length of the extra field in the local file data - without
+     * Header-ID or length specifier.
+     *
+     * @return a <code>ZipShort</code> for the length of the data of this 
extra field
+     */
+    public ZipShort getLocalFileDataLength() {
+        return new ZipShort(1 +
+                (bit0_modifyTimePresent ? 4 : 0) +
+                (bit1_accessTimePresent && accessTime != null ? 4 : 0) +
+                (bit2_createTimePresent && createTime != null ? 4 : 0)
+        );
+    }
+
+    /**
+     * Length of the extra field in the local file data - without
+     * Header-ID or length specifier.
+     * <p/>
+     * For X5455 the central length is often smaller than the
+     * local length, because central cannot contain access or create
+     * timestamps.
+     *
+     * @return a <code>ZipShort</code> for the length of the data of this 
extra field
+     */
+    public ZipShort getCentralDirectoryLength() {
+        return new ZipShort(1 +
+                (bit0_modifyTimePresent ? 4 : 0)
+        );
+    }
+
+    /**
+     * The actual data to put into local file data - without Header-ID
+     * or length specifier.
+     *
+     * @return get the data
+     */
+    public byte[] getLocalFileDataData() {
+        byte[] data = new byte[getLocalFileDataLength().getValue()];
+        int pos = 0;
+        data[pos++] = 0;
+        if (bit0_modifyTimePresent) {
+            data[0] |= MODIFY_TIME_BIT;
+            System.arraycopy(modifyTime.getBytes(), 0, data, pos, 4);
+            pos += 4;
+        }
+        if (bit1_accessTimePresent && accessTime != null) {
+            data[0] |= ACCESS_TIME_BIT;
+            System.arraycopy(accessTime.getBytes(), 0, data, pos, 4);
+            pos += 4;
+        }
+        if (bit2_createTimePresent && createTime != null) {
+            data[0] |= CREATE_TIME_BIT;
+            System.arraycopy(createTime.getBytes(), 0, data, pos, 4);
+            pos += 4;
+        }
+        return data;
+    }
+
+    /**
+     * The actual data to put into central directory data - without Header-ID
+     * or length specifier.
+     *
+     * @return the central directory data
+     */
+    public byte[] getCentralDirectoryData() {
+        byte[] centralData = new byte[getCentralDirectoryLength().getValue()];
+        byte[] localData = getLocalFileDataData();
+
+        // Truncate out create & access time (last 8 bytes) from
+        // the copy of the local data we obtained:
+        System.arraycopy(localData, 0, centralData, 0, centralData.length);
+        return centralData;
+    }
+
+    /**
+     * Populate data from this array as if it was in local file data.
+     *
+     * @param data   an array of bytes
+     * @param offset the start offset
+     * @param length the number of bytes in the array from offset
+     * @throws java.util.zip.ZipException on error
+     */
+    public void parseFromLocalFileData(
+            byte[] data, int offset, int length
+    ) throws ZipException {
+        reset();
+        final int len = offset + length;
+        setFlags(data[offset++]);
+        if (bit0_modifyTimePresent) {
+            modifyTime = new ZipLong(data, offset);
+            offset += 4;
+        }
+
+        // Notice the extra length check in case we are parsing the shorter
+        // central data field (for both access and create timestamps).
+        if (bit1_accessTimePresent && offset + 4 <= len) {
+            accessTime = new ZipLong(data, offset);
+            offset += 4;
+        }
+        if (bit2_createTimePresent && offset + 4 <= len) {
+            createTime = new ZipLong(data, offset);
+            offset += 4;
+        }
+    }
+
+    /**
+     * Doesn't do anything special since this class always uses the
+     * same parsing logic for both central directory and local file data.
+     */
+    public void parseFromCentralDirectoryData(
+            byte[] buffer, int offset, int length
+    ) throws ZipException {
+        reset();
+        parseFromLocalFileData(buffer, offset, length);
+    }
+
+    /**
+     * Reset state back to newly constructed state.  Helps us make sure
+     * parse() calls always generate clean results.
+     */
+    private void reset() {
+        setFlags((byte) 0);
+        this.modifyTime = null;
+        this.accessTime = null;
+        this.createTime = null;
+    }
+
+    /**
+     * Sets flags byte.  The flags byte tells us which of the
+     * three datestamp fields are present in the data:
+     * <pre>
+     * bit0 - modify time
+     * bit1 - access time
+     * bit2 - create time
+     * </pre>
+     * Only first 3 bits of flags are used according to the
+     * latest version of the spec (December 2012).
+     *
+     * @param flags flags byte indicating which of the
+     *              three datestamp fields are present.
+     */
+    public void setFlags(byte flags) {
+        this.flags = flags;
+        this.bit0_modifyTimePresent = (flags & MODIFY_TIME_BIT) == 
MODIFY_TIME_BIT;
+        this.bit1_accessTimePresent = (flags & ACCESS_TIME_BIT) == 
ACCESS_TIME_BIT;
+        this.bit2_createTimePresent = (flags & CREATE_TIME_BIT) == 
CREATE_TIME_BIT;
+    }
+
+    /**
+     * Gets flags byte.  The flags byte tells us which of the
+     * three datestamp fields are present in the data:
+     * <pre>
+     * bit0 - modify time
+     * bit1 - access time
+     * bit2 - create time
+     * </pre>
+     * Only first 3 bits of flags are used according to the
+     * latest version of the spec (December 2012).
+     *
+     * @return flags byte indicating which of the
+     *         three datestamp fields are present.
+     */
+    public byte getFlags() { return flags; }
+
+    /**
+     * Returns whether bit0 of the flags byte is set or not,
+     * which should correspond to the presence or absence of
+     * a modify timestamp in this particular zip entry.
+     *
+     * @return true if bit0 of the flags byte is set.
+     */
+    public boolean isBit0_modifyTimePresent() { return bit0_modifyTimePresent; 
}
+
+    /**
+     * Returns whether bit1 of the flags byte is set or not,
+     * which should correspond to the presence or absence of
+     * a "last access" timestamp in this particular zip entry.
+     *
+     * @return true if bit1 of the flags byte is set.
+     */
+    public boolean isBit1_accessTimePresent() { return bit1_accessTimePresent; 
}
+
+    /**
+     * Returns whether bit2 of the flags byte is set or not,
+     * which should correspond to the presence or absence of
+     * a create timestamp in this particular zip entry.
+     *
+     * @return true if bit2 of the flags byte is set.
+     */
+    public boolean isBit2_createTimePresent() { return bit2_createTimePresent; 
}
+
+    /**
+     * Returns the modify time (seconds since epoch) of this zip entry
+     * as a ZipLong object, or null if no such timestamp exists in the
+     * zip entry.
+     *
+     * @return modify time (seconds since epoch) or null.
+     */
+    public ZipLong getModifyTime() { return modifyTime; }
+
+    /**
+     * Returns the access time (seconds since epoch) of this zip entry
+     * as a ZipLong object, or null if no such timestamp exists in the
+     * zip entry.
+     *
+     * @return access time (seconds since epoch) or null.
+     */
+    public ZipLong getAccessTime() { return accessTime; }
+
+    /**
+     * <p>
+     * Returns the create time (seconds since epoch) of this zip entry
+     * as a ZipLong object, or null if no such timestamp exists in the
+     * zip entry.
+     * </p><p>
+     * Note: modern linux file systems (e.g., ext2)
+     * do not appear to store a "create time" value, and so
+     * it's usually omitted altogether in the zip extra
+     * field.  Perhaps other unix systems track this.
+     *
+     * @return create time (seconds since epoch) or null.
+     */
+    public ZipLong getCreateTime() { return createTime; }
+
+    /**
+     * Returns the modify time as a java.util.Date
+     * of this zip entry, or null if no such timestamp exists in the zip entry.
+     * The milliseconds are always zeroed out, since the underlying data
+     * offers only per-second precision.
+     *
+     * @return modify time as java.util.Date or null.
+     */
+    public Date getModifyJavaTime() {
+        return modifyTime != null ? new Date(modifyTime.getValue() * 1000) : 
null;
+    }
+
+    /**
+     * Returns the access time as a java.util.Date
+     * of this zip entry, or null if no such timestamp exists in the zip entry.
+     * The milliseconds are always zeroed out, since the underlying data
+     * offers only per-second precision.
+     *
+     * @return access time as java.util.Date or null.
+     */
+    public Date getAccessJavaTime() {
+        return accessTime != null ? new Date(accessTime.getValue() * 1000) : 
null;
+    }
+
+    /**
+     * <p>
+     * Returns the create time as a a java.util.Date
+     * of this zip entry, or null if no such timestamp exists in the zip entry.
+     * The milliseconds are always zeroed out, since the underlying data
+     * offers only per-second precision.
+     * </p><p>
+     * Note: modern linux file systems (e.g., ext2)
+     * do not appear to store a "create time" value, and so
+     * it's usually omitted altogether in the zip extra
+     * field.  Perhaps other unix systems track this.
+     *
+     * @return create time as java.util.Date or null.
+     */
+    public Date getCreateJavaTime() {
+        return createTime != null ? new Date(createTime.getValue() * 1000) : 
null;
+    }
+
+    /**
+     * <p>
+     * Sets the modify time (seconds since epoch) of this zip entry
+     * using a ZipLong object.
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param l ZipLong of the modify time (seconds per epoch)
+     */
+    public void setModifyTime(ZipLong l) { this.modifyTime = l; }
+
+    /**
+     * <p>
+     * Sets the access time (seconds since epoch) of this zip entry
+     * using a ZipLong object
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param l ZipLong of the access time (seconds per epoch)
+     */
+    public void setAccessTime(ZipLong l) { this.accessTime = l; }
+
+    /**
+     * <p>
+     * Sets the create time (seconds since epoch) of this zip entry
+     * using a ZipLong object
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param l ZipLong of the create time (seconds per epoch)
+     */
+    public void setCreateTime(ZipLong l) { this.createTime = l; }
+
+    /**
+     * <p>
+     * Sets the modify time as a java.util.Date
+     * of this zip entry.  Supplied value is truncated to per-second
+     * precision (milliseconds zeroed-out).
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param d modify time as java.util.Date
+     */
+    public void setModifyJavaTime(Date d) { setModifyTime(dateToZipLong(d)); }
+
+    /**
+     * <p>
+     * Sets the access time as a java.util.Date
+     * of this zip entry.  Supplied value is truncated to per-second
+     * precision (milliseconds zeroed-out).
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param d access time as java.util.Date
+     */
+    public void setAccessJavaTime(Date d) { setAccessTime(dateToZipLong(d)); }
+
+    /**
+     * <p>
+     * Sets the create time as a java.util.Date
+     * of this zip entry.  Supplied value is truncated to per-second
+     * precision (milliseconds zeroed-out).
+     * </p><p>
+     * Note: the setters for flags and timestamps are decoupled.
+     * Even if the timestamp is not-null, it will only be written
+     * out if the corresponding bit in the flags is also set.
+     * </p>
+     *
+     * @param d create time as java.util.Date
+     */
+    public void setCreateJavaTime(Date d) { setCreateTime(dateToZipLong(d)); }
+
+    /**
+     * Utility method converts java.util.Date (milliseconds since epoch)
+     * into a ZipLong (seconds since epoch).
+     * <p/>
+     * Also makes sure the converted ZipLong is not too big to fit
+     * in 32 unsigned bits.
+     *
+     * @param d java.util.Date to convert to ZipLong
+     * @return ZipLong
+     */
+    private static ZipLong dateToZipLong(final Date d) {
+        if (d == null) { return null; }
+
+        final long TWO_TO_32 = 0x100000000L;
+        final long l = d.getTime() / 1000;
+        if (l >= TWO_TO_32) {
+            throw new IllegalArgumentException("Cannot set an X5455 timestamp 
larger than 2^32: " + l);
+        }
+        return new ZipLong(l);
+    }
+
+    /**
+     * Returns a String representation of this class useful for
+     * debugging purposes.
+     *
+     * @return A String representation of this class useful for
+     *         debugging purposes.
+     */
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("0x5455 Zip Extra Field: Flags=");
+        
buf.append(Integer.toBinaryString(ZipUtil.unsignedIntToSignedByte(flags))).append("
 ");
+        if (bit0_modifyTimePresent && modifyTime != null) {
+            Date m = getModifyJavaTime();
+            buf.append(" Modify:[").append(m).append("] ");
+        }
+        if (bit1_accessTimePresent && accessTime != null) {
+            Date a = getAccessJavaTime();
+            buf.append(" Access:[").append(a).append("] ");
+        }
+        if (bit2_createTimePresent && createTime != null) {
+            Date c = getCreateJavaTime();
+            buf.append(" Create:[").append(c).append("] ");
+        }
+        return buf.toString();
+    }
+
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof X5455_ExtendedTimestamp) {
+            X5455_ExtendedTimestamp xf = (X5455_ExtendedTimestamp) o;
+
+            // The ZipLong==ZipLong clauses handle the cases where both are 
null.
+            // and only last 3 bits of flags matter.
+            return ((flags & 0x07) == (xf.flags & 0x07)) &&
+                    (modifyTime == xf.modifyTime || (modifyTime != null && 
modifyTime.equals(xf.modifyTime))) &&
+                    (accessTime == xf.accessTime || (accessTime != null && 
accessTime.equals(xf.accessTime))) &&
+                    (createTime == xf.createTime || (createTime != null && 
createTime.equals(xf.createTime)));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        int hc = (-123 * (flags & 0x07)); // only last 3 bits of flags matter
+        if (modifyTime != null) {
+            hc ^= modifyTime.hashCode();
+        }
+        if (accessTime != null) {
+            // Since accessTime is often same as modifyTime,
+            // this prevents them from XOR negating each other.
+            hc ^= Integer.rotateLeft(accessTime.hashCode(), 11);
+        }
+        if (createTime != null) {
+            hc ^= Integer.rotateLeft(createTime.hashCode(), 22);
+        }
+        return hc;
+    }
+
+}
\ No newline at end of file

Added: 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestampTest.java
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestampTest.java?rev=1430562&view=auto
==============================================================================
--- 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestampTest.java
 (added)
+++ 
commons/proper/compress/trunk/src/test/java/org/apache/commons/compress/archivers/zip/X5455_ExtendedTimestampTest.java
 Tue Jan  8 21:54:15 2013
@@ -0,0 +1,461 @@
+/*
+ *  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.commons.compress.archivers.zip;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.TimeZone;
+import java.util.zip.ZipException;
+
+import static 
org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp.ACCESS_TIME_BIT;
+import static 
org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp.CREATE_TIME_BIT;
+import static 
org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp.MODIFY_TIME_BIT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class X5455_ExtendedTimestampTest {
+    private final static ZipShort X5455 = new ZipShort(0x5455);
+
+    private final static ZipLong ZERO_TIME = new ZipLong(0);
+    private final static ZipLong MAX_TIME_SECONDS = new ZipLong(0xFFFFFFFFL);
+    private final static SimpleDateFormat DATE_FORMAT = new 
SimpleDateFormat("YYYY-MM-dd/HH:mm:ss Z");
+
+    static {
+        DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+
+    /**
+     * The extended field (xf) we are testing.
+     */
+    private X5455_ExtendedTimestamp xf;
+
+    @Before
+    public void before() {
+        xf = new X5455_ExtendedTimestamp();
+    }
+
+    @Test
+    public void testSampleFile() throws Exception {
+
+        /*
+        Contains entries with zipTime, accessTime, and modifyTime.
+        The file name tells you the year we tried to set the time to
+        (Jan 1st, Midnight, UTC).
+
+        For example:
+
+        COMPRESS-210_unix_time_zip_test/1999
+        COMPRESS-210_unix_time_zip_test/2000
+        COMPRESS-210_unix_time_zip_test/2108
+
+        File's last-modified is 1st second after midnight.
+        Zip-time's 2-second granularity rounds that up to 2nd second.
+        File's last-access is 3rd second after midnight.
+
+        So, from example above:
+
+        1999's zip time:  Jan 1st, 1999-01-01/00:00:02
+        1999's mod time:  Jan 1st, 1999-01-01/00:00:01
+        1999's acc time:  Jan 1st, 1999-01-01/00:00:03
+         */
+
+        URL zip = 
getClass().getResource("/COMPRESS-210_unix_time_zip_test.zip");
+        File archive = new File(new URI(zip.toString()));
+        ZipFile zf = null;
+
+        try {
+            zf = new ZipFile(archive);
+            Enumeration<ZipArchiveEntry> en = zf.getEntries();
+
+            // We expect EVERY entry of this zip file
+            // to contain extra field 0x5455.
+            while (en.hasMoreElements()) {
+
+                ZipArchiveEntry zae = en.nextElement();
+                String name = zae.getName();
+                X5455_ExtendedTimestamp xf = (X5455_ExtendedTimestamp) 
zae.getExtraField(X5455);
+
+                Date z = zae.getLastModifiedDate();
+                Date m = xf.getModifyJavaTime();
+                Date a = xf.getAccessJavaTime();
+
+                String zipTime = DATE_FORMAT.format(z);
+                String modTime = DATE_FORMAT.format(m);
+                String accTime = DATE_FORMAT.format(a);
+
+                if (!zae.isDirectory()) {
+                    int x = name.lastIndexOf('/');
+                    String yearString = name.substring(x + 1);
+                    int year;
+                    try {
+                        year = Integer.parseInt(yearString);
+                    } catch (NumberFormatException nfe) {
+                        year = -1;
+                    }
+                    if (year >= 0) {
+                        switch (year) {
+                            case 2107:
+                                // Zip time is okay up to 2107.
+                                assertEquals(zipTime, year + "-01-01/00:00:02 
+0000");
+                                // But the X5455 data has overflowed:
+                                assertEquals(modTime, "1970-11-24/17:31:45 
+0000");
+                                assertEquals(accTime, "1970-11-24/17:31:47 
+0000");
+                                break;
+                            case 2108:
+                                // Zip time is still okay at Jan 1st midnight 
(UTC) in 2108
+                                // because we created the zip file in pacific 
time zone, so it's
+                                // actually still 2107 in the zip file!
+                                assertEquals(zipTime, year + "-01-01/00:00:02 
+0000");
+                                // The X5455 data is still overflowed, of 
course:
+                                assertEquals(modTime, "1971-11-24/17:31:45 
+0000");
+                                assertEquals(accTime, "1971-11-24/17:31:47 
+0000");
+                                break;
+                            case 2109:
+                                // All three timestamps have overflowed by 
2109.
+                                assertEquals(zipTime, "1981-01-01/00:00:02 
+0000");
+                                assertEquals(modTime, "1972-11-24/17:31:45 
+0000");
+                                assertEquals(accTime, "1972-11-24/17:31:47 
+0000");
+
+                                // Hmmm.... looks like one could examine both 
DOS time
+                                // and the Unix time together to hack a nice 
workaround to
+                                // get timestamps past 2106 in a 
reverse-compatible way.
+
+                                break;
+                            default:
+                                // X5455 time is good from epoch (1970) to 
2106.
+                                // Zip time is good from 1980 to 2107.
+                                if (year < 1980) {
+                                    assertEquals(zipTime, "1980-01-01/08:00:00 
+0000");
+                                } else {
+                                    assertEquals(zipTime, year + 
"-01-01/00:00:02 +0000");
+                                }
+                                assertEquals(modTime, year + "-01-01/00:00:01 
+0000");
+                                assertEquals(accTime, year + "-01-01/00:00:03 
+0000");
+                                break;
+                        }
+                    }
+                }
+            }
+        } finally {
+            if (zf != null) {
+                zf.close();
+            }
+        }
+    }
+
+
+    @Test
+    public void testMisc() throws Exception {
+        assertFalse(xf.equals(new Object()));
+        assertTrue(xf.toString().startsWith("0x5455 Zip Extra Field"));
+        assertTrue(!xf.toString().contains(" Modify:"));
+        assertTrue(!xf.toString().contains(" Access:"));
+        assertTrue(!xf.toString().contains(" Create:"));
+        Object o = xf.clone();
+        assertEquals(o.hashCode(), xf.hashCode());
+        assertTrue(xf.equals(o));
+
+        xf.setModifyJavaTime(new Date(1111));
+        xf.setAccessJavaTime(new Date(2222));
+        xf.setCreateJavaTime(new Date(3333));
+        xf.setFlags((byte) 7);
+        assertFalse(xf.equals(o));
+        assertTrue(xf.toString().startsWith("0x5455 Zip Extra Field"));
+        assertTrue(xf.toString().contains(" Modify:"));
+        assertTrue(xf.toString().contains(" Access:"));
+        assertTrue(xf.toString().contains(" Create:"));
+        o = xf.clone();
+        assertEquals(o.hashCode(), xf.hashCode());
+        assertTrue(xf.equals(o));
+    }
+
+    @Test
+    public void testGettersSetters() {
+        // X5455 is concerned with time, so let's
+        // get a timestamp to play with (Jan 1st, 2000).
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        cal.set(Calendar.YEAR, 2000);
+        cal.set(Calendar.MONTH, Calendar.JANUARY);
+        cal.set(Calendar.DATE, 1);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        Date timeMillis = cal.getTime();
+        ZipLong time = new ZipLong(timeMillis.getTime() / 1000);
+
+        // set too big
+        try {
+            // Java time is 1000 x larger (milliseconds).
+            xf.setModifyJavaTime(new Date(1000L * (MAX_TIME_SECONDS.getValue() 
+ 1L)));
+            fail("Time too big for 32 bits!");
+        } catch (IllegalArgumentException iae) {
+            // All is good.
+        }
+
+        // get/set modify time
+        xf.setModifyTime(time);
+        assertEquals(time, xf.getModifyTime());
+        assertEquals(timeMillis, xf.getModifyJavaTime());
+        xf.setModifyJavaTime(timeMillis);
+        assertEquals(time, xf.getModifyTime());
+        assertEquals(timeMillis, xf.getModifyJavaTime());
+        // Make sure milliseconds get zeroed out:
+        xf.setModifyJavaTime(new Date(timeMillis.getTime() + 123));
+        assertEquals(time, xf.getModifyTime());
+        assertEquals(timeMillis, xf.getModifyJavaTime());
+        // Null
+        xf.setModifyTime(null);
+        assertNull(xf.getModifyJavaTime());
+        xf.setModifyJavaTime(null);
+        assertNull(xf.getModifyTime());
+
+        // get/set access time
+        xf.setAccessTime(time);
+        assertEquals(time, xf.getAccessTime());
+        assertEquals(timeMillis, xf.getAccessJavaTime());
+        xf.setAccessJavaTime(timeMillis);
+        assertEquals(time, xf.getAccessTime());
+        assertEquals(timeMillis, xf.getAccessJavaTime());
+        // Make sure milliseconds get zeroed out:
+        xf.setAccessJavaTime(new Date(timeMillis.getTime() + 123));
+        assertEquals(time, xf.getAccessTime());
+        assertEquals(timeMillis, xf.getAccessJavaTime());
+        // Null
+        xf.setAccessTime(null);
+        assertNull(xf.getAccessJavaTime());
+        xf.setAccessJavaTime(null);
+        assertNull(xf.getAccessTime());
+
+        // get/set create time
+        xf.setCreateTime(time);
+        assertEquals(time, xf.getCreateTime());
+        assertEquals(timeMillis, xf.getCreateJavaTime());
+        xf.setCreateJavaTime(timeMillis);
+        assertEquals(time, xf.getCreateTime());
+        assertEquals(timeMillis, xf.getCreateJavaTime());
+        // Make sure milliseconds get zeroed out:
+        xf.setCreateJavaTime(new Date(timeMillis.getTime() + 123));
+        assertEquals(time, xf.getCreateTime());
+        assertEquals(timeMillis, xf.getCreateJavaTime());
+        // Null
+        xf.setCreateTime(null);
+        assertNull(xf.getCreateJavaTime());
+        xf.setCreateJavaTime(null);
+        assertNull(xf.getCreateTime());
+
+
+        // initialize for flags
+        xf.setModifyTime(time);
+        xf.setAccessTime(time);
+        xf.setCreateTime(time);
+
+        // get/set flags: 000
+        xf.setFlags((byte) 0);
+        assertEquals(0, xf.getFlags());
+        assertFalse(xf.isBit0_modifyTimePresent());
+        assertFalse(xf.isBit1_accessTimePresent());
+        assertFalse(xf.isBit2_createTimePresent());
+        // Local length=1, Central length=1 (flags only!)
+        assertEquals(1, xf.getLocalFileDataLength().getValue());
+        assertEquals(1, xf.getCentralDirectoryLength().getValue());
+
+        // get/set flags: 001
+        xf.setFlags((byte) 1);
+        assertEquals(1, xf.getFlags());
+        assertTrue(xf.isBit0_modifyTimePresent());
+        assertFalse(xf.isBit1_accessTimePresent());
+        assertFalse(xf.isBit2_createTimePresent());
+        // Local length=5, Central length=5 (flags + mod)
+        assertEquals(5, xf.getLocalFileDataLength().getValue());
+        assertEquals(5, xf.getCentralDirectoryLength().getValue());
+
+        // get/set flags: 010
+        xf.setFlags((byte) 2);
+        assertEquals(2, xf.getFlags());
+        assertFalse(xf.isBit0_modifyTimePresent());
+        assertTrue(xf.isBit1_accessTimePresent());
+        assertFalse(xf.isBit2_createTimePresent());
+        // Local length=5, Central length=1
+        assertEquals(5, xf.getLocalFileDataLength().getValue());
+        assertEquals(1, xf.getCentralDirectoryLength().getValue());
+
+        // get/set flags: 100
+        xf.setFlags((byte) 4);
+        assertEquals(4, xf.getFlags());
+        assertFalse(xf.isBit0_modifyTimePresent());
+        assertFalse(xf.isBit1_accessTimePresent());
+        assertTrue(xf.isBit2_createTimePresent());
+        // Local length=5, Central length=1
+        assertEquals(5, xf.getLocalFileDataLength().getValue());
+        assertEquals(1, xf.getCentralDirectoryLength().getValue());
+
+        // get/set flags: 111
+        xf.setFlags((byte) 7);
+        assertEquals(7, xf.getFlags());
+        assertTrue(xf.isBit0_modifyTimePresent());
+        assertTrue(xf.isBit1_accessTimePresent());
+        assertTrue(xf.isBit2_createTimePresent());
+        // Local length=13, Central length=5
+        assertEquals(13, xf.getLocalFileDataLength().getValue());
+        assertEquals(5, xf.getCentralDirectoryLength().getValue());
+
+        // get/set flags: 11111111
+        xf.setFlags((byte) -1);
+        assertEquals(-1, xf.getFlags());
+        assertTrue(xf.isBit0_modifyTimePresent());
+        assertTrue(xf.isBit1_accessTimePresent());
+        assertTrue(xf.isBit2_createTimePresent());
+        // Local length=13, Central length=5
+        assertEquals(13, xf.getLocalFileDataLength().getValue());
+        assertEquals(5, xf.getCentralDirectoryLength().getValue());
+    }
+
+    @Test
+    public void testGetHeaderId() {
+        assertEquals(X5455, xf.getHeaderId());
+    }
+
+    @Test
+    public void testParseReparse() throws ZipException {
+        /*
+         * Recall the spec:
+         *
+         * 0x5455        Short       tag for this extra block type ("UT")
+         * TSize         Short       total data size for this block
+         * Flags         Byte        info bits
+         * (ModTime)     Long        time of last modification (UTC/GMT)
+         * (AcTime)      Long        time of last access (UTC/GMT)
+         * (CrTime)      Long        time of original creation (UTC/GMT)
+         */
+        final byte[] NULL_FLAGS = {0};
+        final byte[] AC_CENTRAL = {2}; // central data only contains the AC 
flag and no actual data
+        final byte[] CR_CENTRAL = {4}; // central data only dontains the CR 
flag and no actual data
+
+        final byte[] MOD_ZERO = {1, 0, 0, 0, 0};
+        final byte[] MOD_MAX = {1, -1, -1, -1, -1};
+        final byte[] AC_ZERO = {2, 0, 0, 0, 0};
+        final byte[] AC_MAX = {2, -1, -1, -1, -1};
+        final byte[] CR_ZERO = {4, 0, 0, 0, 0};
+        final byte[] CR_MAX = {4, -1, -1, -1, -1};
+        final byte[] MOD_AC_ZERO = {3, 0, 0, 0, 0, 0, 0, 0, 0};
+        final byte[] MOD_AC_MAX = {3, -1, -1, -1, -1, -1, -1, -1, -1};
+        final byte[] MOD_AC_CR_ZERO = {7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        final byte[] MOD_AC_CR_MAX = {7, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
-1, -1, -1};
+
+        parseReparse(null, NULL_FLAGS, NULL_FLAGS);
+        parseReparse(ZERO_TIME, MOD_ZERO, MOD_ZERO);
+        parseReparse(MAX_TIME_SECONDS, MOD_MAX, MOD_MAX);
+        parseReparse(ZERO_TIME, AC_ZERO, AC_CENTRAL);
+        parseReparse(MAX_TIME_SECONDS, AC_MAX, AC_CENTRAL);
+        parseReparse(ZERO_TIME, CR_ZERO, CR_CENTRAL);
+        parseReparse(MAX_TIME_SECONDS, CR_MAX, CR_CENTRAL);
+        parseReparse(ZERO_TIME, MOD_AC_ZERO, MOD_ZERO);
+        parseReparse(MAX_TIME_SECONDS, MOD_AC_MAX, MOD_MAX);
+        parseReparse(ZERO_TIME, MOD_AC_CR_ZERO, MOD_ZERO);
+        parseReparse(MAX_TIME_SECONDS, MOD_AC_CR_MAX, MOD_MAX);
+
+        // As far as the spec is concerned (December 2012) all of these flags
+        // are spurious versions of 7 (a.k.a. binary 00000111).
+        parseReparse((byte) 15, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+        parseReparse((byte) 31, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+        parseReparse((byte) 63, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+        parseReparse((byte) 71, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+        parseReparse((byte) 127, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+        parseReparse((byte) -1, MAX_TIME_SECONDS, (byte) 7, MOD_AC_CR_MAX, 
MOD_MAX);
+    }
+
+    private void parseReparse(
+            final ZipLong time,
+            final byte[] expectedLocal,
+            final byte[] almostExpectedCentral
+    ) throws ZipException {
+        parseReparse(null, time, null, expectedLocal, almostExpectedCentral);
+    }
+
+    private void parseReparse(
+            final Byte providedFlagsByte,
+            final ZipLong time,
+            final Byte expectedFlagsByte,
+            final byte[] expectedLocal,
+            final byte[] almostExpectedCentral
+    ) throws ZipException {
+        final byte providedFlags = providedFlagsByte == null ? 
expectedLocal[0] : providedFlagsByte;
+        final byte expectedFlags = expectedFlagsByte == null ? 
expectedLocal[0] : expectedFlagsByte;
+
+        // We're responsible for expectedCentral's flags.  Too annoying to set 
in caller.
+        final byte[] expectedCentral = new byte[almostExpectedCentral.length];
+        System.arraycopy(almostExpectedCentral, 0, expectedCentral, 0, 
almostExpectedCentral.length);
+        expectedCentral[0] = expectedFlags;
+
+        xf.setFlags(providedFlags);
+        xf.setModifyTime(time);
+        xf.setAccessTime(time);
+        xf.setCreateTime(time);
+        byte[] result = xf.getLocalFileDataData();
+        assertTrue(Arrays.equals(expectedLocal, result));
+
+        // And now we re-parse:
+        xf.parseFromLocalFileData(result, 0, result.length);
+        assertEquals(expectedFlags, xf.getFlags());
+        if (isFlagSet(expectedFlags, MODIFY_TIME_BIT)) {
+            assertTrue(xf.isBit0_modifyTimePresent());
+            assertEquals(time, xf.getModifyTime());
+        }
+        if (isFlagSet(expectedFlags, ACCESS_TIME_BIT)) {
+            assertTrue(xf.isBit1_accessTimePresent());
+            assertEquals(time, xf.getAccessTime());
+        }
+        if (isFlagSet(expectedFlags, CREATE_TIME_BIT)) {
+            assertTrue(xf.isBit2_createTimePresent());
+            assertEquals(time, xf.getCreateTime());
+        }
+
+        // Do the same as above, but with Central Directory data:
+        xf.setFlags(providedFlags);
+        xf.setModifyTime(time);
+        xf.setAccessTime(time);
+        xf.setCreateTime(time);
+        result = xf.getCentralDirectoryData();
+        assertTrue(Arrays.equals(expectedCentral, result));
+
+        // And now we re-parse:
+        xf.parseFromCentralDirectoryData(result, 0, result.length);
+        assertEquals(expectedFlags, xf.getFlags());
+        // Central Directory never contains ACCESS or CREATE, but
+        // may contain MODIFY.
+        if (isFlagSet(expectedFlags, MODIFY_TIME_BIT)) {
+            assertTrue(xf.isBit0_modifyTimePresent());
+            assertEquals(time, xf.getModifyTime());
+        }
+    }
+
+    private static boolean isFlagSet(byte data, byte flag) { return (data & 
flag) == flag; }
+}

Added: 
commons/proper/compress/trunk/src/test/resources/COMPRESS-210_unix_time_zip_test.zip
URL: 
http://svn.apache.org/viewvc/commons/proper/compress/trunk/src/test/resources/COMPRESS-210_unix_time_zip_test.zip?rev=1430562&view=auto
==============================================================================
Binary file - no diff available.

Propchange: 
commons/proper/compress/trunk/src/test/resources/COMPRESS-210_unix_time_zip_test.zip
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream


Reply via email to