Repository: commons-compress Updated Branches: refs/heads/master 1a874af88 -> 1239917ec
add support for NTFS timestamp field I need to create a real archive containing the extra field on a Windows box for a real test Project: http://git-wip-us.apache.org/repos/asf/commons-compress/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-compress/commit/1239917e Tree: http://git-wip-us.apache.org/repos/asf/commons-compress/tree/1239917e Diff: http://git-wip-us.apache.org/repos/asf/commons-compress/diff/1239917e Branch: refs/heads/master Commit: 1239917ec625a84ca27cf3769ff297b3a158e8d9 Parents: 1a874af Author: Stefan Bodewig <bode...@apache.org> Authored: Sun Dec 13 20:26:56 2015 +0100 Committer: Stefan Bodewig <bode...@apache.org> Committed: Sun Dec 13 20:26:56 2015 +0100 ---------------------------------------------------------------------- .../compress/archivers/zip/ExtraFieldUtils.java | 1 + .../compress/archivers/zip/X000A_NTFS.java | 385 +++++++++++++++++++ .../compress/archivers/zip/X000A_NTFSTest.java | 43 +++ 3 files changed, 429 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-compress/blob/1239917e/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java b/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java index a0c43de..715de4d 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/ExtraFieldUtils.java @@ -46,6 +46,7 @@ public class ExtraFieldUtils { register(UnicodePathExtraField.class); register(UnicodeCommentExtraField.class); register(Zip64ExtendedInformationExtraField.class); + register(X000A_NTFS.class); } /** http://git-wip-us.apache.org/repos/asf/commons-compress/blob/1239917e/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java b/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java new file mode 100644 index 0000000..7e44eca --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/zip/X000A_NTFS.java @@ -0,0 +1,385 @@ +/* + * 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.util.Date; +import java.util.zip.ZipException; + +/** + * NTFS extra field that was thought to store various attributes but + * in reality only stores timestamps. + * + * <pre> + * 4.5.5 -NTFS Extra Field (0x000a): + * + * The following is the layout of the NTFS attributes + * "extra" block. (Note: At this time the Mtime, Atime + * and Ctime values MAY be used on any WIN32 system.) + * + * Note: all fields stored in Intel low-byte/high-byte order. + * + * Value Size Description + * ----- ---- ----------- + * (NTFS) 0x000a 2 bytes Tag for this "extra" block type + * TSize 2 bytes Size of the total "extra" block + * Reserved 4 bytes Reserved for future use + * Tag1 2 bytes NTFS attribute tag value #1 + * Size1 2 bytes Size of attribute #1, in bytes + * (var) Size1 Attribute #1 data + * . + * . + * . + * TagN 2 bytes NTFS attribute tag value #N + * SizeN 2 bytes Size of attribute #N, in bytes + * (var) SizeN Attribute #N data + * + * For NTFS, values for Tag1 through TagN are as follows: + * (currently only one set of attributes is defined for NTFS) + * + * Tag Size Description + * ----- ---- ----------- + * 0x0001 2 bytes Tag for attribute #1 + * Size1 2 bytes Size of attribute #1, in bytes + * Mtime 8 bytes File last modification time + * Atime 8 bytes File last access time + * Ctime 8 bytes File creation time + * </pre> + */ +public class X000A_NTFS implements ZipExtraField { + private static final ZipShort HEADER_ID = new ZipShort(0x000a); + private static final ZipShort TIME_ATTR_TAG = new ZipShort(0x0001); + private static final ZipShort TIME_ATTR_SIZE = new ZipShort(3 * 8); + + private ZipEightByteInteger modifyTime = ZipEightByteInteger.ZERO; + private ZipEightByteInteger accessTime = ZipEightByteInteger.ZERO; + private ZipEightByteInteger createTime = ZipEightByteInteger.ZERO; + + /** + * 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(4 /* reserved */ + + 2 /* Tag#1 */ + + 2 /* Size#1 */ + + 3 * 8 /* time values */); + } + + /** + * 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.</p> + * + * @return a <code>ZipShort</code> for the length of the data of this extra field + */ + public ZipShort getCentralDirectoryLength() { + return getLocalFileDataLength(); + } + + /** + * 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 = 4; + System.arraycopy(TIME_ATTR_TAG.getBytes(), 0, data, pos, 2); + pos += 2; + System.arraycopy(TIME_ATTR_SIZE.getBytes(), 0, data, pos, 2); + pos += 2; + System.arraycopy(modifyTime.getBytes(), 0, data, pos, 8); + pos += 8; + System.arraycopy(accessTime.getBytes(), 0, data, pos, 8); + pos += 8; + System.arraycopy(createTime.getBytes(), 0, data, pos, 8); + 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() { + return getLocalFileDataData(); + } + + /** + * 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 { + final int len = offset + length; + + // skip reserved + offset += 4; + + while (offset + 4 <= len) { + ZipShort tag = new ZipShort(data, offset); + offset += 2; + if (tag.equals(TIME_ATTR_TAG)) { + readTimeAttr(data, offset, len - offset); + break; + } + ZipShort size = new ZipShort(data, offset); + offset += 2 + size.getValue(); + } + } + + /** + * 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); + } + + /** + * Returns the "File last modification time" of this zip entry as + * a ZipEightByteInteger object, or {@link + * ZipEightByteInteger#ZERO} if no such timestamp exists in the + * zip entry. + * + * @return File last modification time + */ + public ZipEightByteInteger getModifyTime() { return modifyTime; } + + /** + * Returns the "File last access time" of this zip entry as a + * ZipEightByteInteger object, or {@link ZipEightByteInteger#ZERO} + * if no such timestamp exists in the zip entry. + * + * @return File last access time + */ + public ZipEightByteInteger getAccessTime() { return accessTime; } + + /** + * Returns the "File creation time" of this zip entry as a + * ZipEightByteInteger object, or {@link ZipEightByteInteger#ZERO} + * if no such timestamp exists in the zip entry. + * + * @return File creation time + */ + public ZipEightByteInteger 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. + * + * @return modify time as java.util.Date or null. + */ + public Date getModifyJavaTime() { + return zipToDate(modifyTime); + } + + /** + * Returns the access time as a java.util.Date + * of this zip entry, or null if no such timestamp exists in the zip entry. + * + * @return access time as java.util.Date or null. + */ + public Date getAccessJavaTime() { + return zipToDate(accessTime); + } + + /** + * 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. + * + * @return create time as java.util.Date or null. + */ + public Date getCreateJavaTime() { + return zipToDate(createTime); + } + + /** + * Sets the File last modification time of this zip entry using a + * ZipEightByteInteger object. + * + * @param t ZipEightByteInteger of the modify time + */ + public void setModifyTime(ZipEightByteInteger t) { + modifyTime = t == null ? ZipEightByteInteger.ZERO : t; + } + + /** + * Sets the File last access time of this zip entry using a + * ZipEightByteInteger object. + * + * @param t ZipEightByteInteger of the access time + */ + public void setAccessTime(ZipEightByteInteger t) { + accessTime = t == null ? ZipEightByteInteger.ZERO : t; + } + + /** + * Sets the File creation time of this zip entry using a + * ZipEightByteInteger object. + * + * @param t ZipEightByteInteger of the create time + */ + public void setCreateTime(ZipEightByteInteger t) { + createTime = t == null ? ZipEightByteInteger.ZERO : t; + } + + /** + * Sets the modify time as a java.util.Date of this zip entry. + * + * @param d modify time as java.util.Date + */ + public void setModifyJavaTime(Date d) { setModifyTime(dateToZip(d)); } + + /** + * Sets the access time as a java.util.Date + * of this zip entry. + * + * @param d access time as java.util.Date + */ + public void setAccessJavaTime(Date d) { setAccessTime(dateToZip(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(dateToZip(d)); } + + /** + * 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("0x000A Zip Extra Field:") + .append(" Modify:[").append(getModifyJavaTime()).append("] ") + .append(" Access:[").append(getAccessJavaTime()).append("] ") + .append(" Create:[").append(getCreateJavaTime()).append("] "); + return buf.toString(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof X000A_NTFS) { + X000A_NTFS xf = (X000A_NTFS) o; + + return (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; + 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; + } + + /** + * Reset state back to newly constructed state. Helps us make sure + * parse() calls always generate clean results. + */ + private void reset() { + this.modifyTime = ZipEightByteInteger.ZERO; + this.accessTime = ZipEightByteInteger.ZERO; + this.createTime = ZipEightByteInteger.ZERO; + } + + private void readTimeAttr(byte[] data, int offset, int length) { + if (length >= 2 + 3 * 8) { + ZipShort tagValueLength = new ZipShort(data, offset); + if (TIME_ATTR_SIZE.equals(tagValueLength)) { + offset += 2; + modifyTime = new ZipEightByteInteger(data, offset); + offset += 8; + accessTime = new ZipEightByteInteger(data, offset); + offset += 8; + createTime = new ZipEightByteInteger(data, offset); + } + } + } + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724290%28v=vs.85%29.aspx + // A file time is a 64-bit value that represents the number of + // 100-nanosecond intervals that have elapsed since 12:00 + // A.M. January 1, 1601 Coordinated Universal Time (UTC). + // this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals + private static final long EPOCH_OFFSET = -116444736000000000L; + + private static ZipEightByteInteger dateToZip(final Date d) { + if (d == null) { return null; } + return new ZipEightByteInteger((d.getTime() * 10000l) - EPOCH_OFFSET); + } + + private static Date zipToDate(ZipEightByteInteger z) { + if (z == null || ZipEightByteInteger.ZERO.equals(z)) { return null; } + long l = (z.getLongValue() + EPOCH_OFFSET) / 10000l; + return new Date(l); + } + +} http://git-wip-us.apache.org/repos/asf/commons-compress/blob/1239917e/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java b/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java new file mode 100644 index 0000000..76f4317 --- /dev/null +++ b/src/test/java/org/apache/commons/compress/archivers/zip/X000A_NTFSTest.java @@ -0,0 +1,43 @@ +/* + * 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.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +public class X000A_NTFSTest { + + @Test + public void simpleRountrip() throws Exception { + X000A_NTFS xf = new X000A_NTFS(); + xf.setModifyJavaTime(new Date(0)); + // one second past midnight + xf.setAccessJavaTime(new Date(-11644473601000l)); + xf.setCreateJavaTime(null); + byte[] b = xf.getLocalFileDataData(); + + X000A_NTFS xf2 = new X000A_NTFS(); + xf2.parseFromLocalFileData(b, 0, b.length); + assertEquals(new Date(0), xf2.getModifyJavaTime()); + assertEquals(new Date(-11644473601000l), xf2.getAccessJavaTime()); + assertEquals(null, xf2.getCreateJavaTime()); + } +}