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