    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 *   https://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.archivers.zip;
    020
    021import java.nio.file.attribute.FileTime;
    022import java.util.Date;
    023import java.util.Objects;
    024import java.util.zip.ZipException;
    025
    026import org.apache.commons.io.file.attribute.FileTimes;
    027
    028/**
    029 * NTFS extra field that was thought to store various attributes but in reality only stores timestamps.
    030 *
    031 * <pre>
    032 *    4.5.5 -NTFS Extra Field (0x000a):
    033 *
    034 *       The following is the layout of the NTFS attributes
    035 *       "extra" block. (Note: At this time the Mtime, Atime
    036 *       and Ctime values MAY be used on any WIN32 system.)
    037 *
    038 *       Note: all fields stored in Intel low-byte/high-byte order.
    039 *
    040 *         Value      Size       Description
    041 *         -----      ----       -----------
    042 * (NTFS)  0x000a     2 bytes    Tag for this "extra" block type
    043 *         TSize      2 bytes    Size of the total "extra" block
    044 *         Reserved   4 bytes    Reserved for future use
    045 *         Tag1       2 bytes    NTFS attribute tag value #1
    046 *         Size1      2 bytes    Size of attribute #1, in bytes
    047 *         (var)      Size1      Attribute #1 data
    048 *          .
    049 *          .
    050 *          .
    051 *          TagN       2 bytes    NTFS attribute tag value #N
    052 *          SizeN      2 bytes    Size of attribute #N, in bytes
    053 *          (var)      SizeN      Attribute #N data
    054 *
    055 *        For NTFS, values for Tag1 through TagN are as follows:
    056 *        (currently only one set of attributes is defined for NTFS)
    057 *
    058 *          Tag        Size       Description
    059 *          -----      ----       -----------
    060 *          0x0001     2 bytes    Tag for attribute #1
    061 *          Size1      2 bytes    Size of attribute #1, in bytes
    062 *          Mtime      8 bytes    File last modification time
    063 *          Atime      8 bytes    File last access time
    064 *          Ctime      8 bytes    File creation time
    065 * </pre>
    066 *
    067 * @since 1.11
    068 * @NotThreadSafe
    069 */
    070public class X000A_NTFS implements ZipExtraField {
    071
    072    /**
    073     * The header ID for this extra field.
    074     *
    075     * @since 1.23
    076     */
    077    public static final ZipShort HEADER_ID = new ZipShort(0x000a);
    078
    079    private static final ZipShort TIME_ATTR_TAG = new ZipShort(0x0001);
    080    private static final ZipShort TIME_ATTR_SIZE = new ZipShort(3 * 8);
    081
    082    private static ZipEightByteInteger dateToZip(final Date d) {
    083        if (d == null) {
    084            return null;
    085        }
    086        return new ZipEightByteInteger(FileTimes.toNtfsTime(d));
    087    }
    088
    089    private static ZipEightByteInteger fileTimeToZip(final FileTime time) {
    090        if (time == null) {
    091            return null;
    092        }
    093        return new ZipEightByteInteger(FileTimes.toNtfsTime(time));
    094    }
    095
    096    private static Date zipToDate(final ZipEightByteInteger z) {
    097        if (z == null || ZipEightByteInteger.ZERO.equals(z)) {
    098            return null;
    099        }
    100        return FileTimes.ntfsTimeToDate(z.getLongValue());
    101    }
    102
    103    private static FileTime zipToFileTime(final ZipEightByteInteger z) {
    104        if (z == null || ZipEightByteInteger.ZERO.equals(z)) {
    105            return null;
    106        }
    107        return FileTimes.ntfsTimeToFileTime(z.getLongValue());
    108    }
    109
    110    private ZipEightByteInteger modifyTime = ZipEightByteInteger.ZERO;
    111
    112    private ZipEightByteInteger accessTime = ZipEightByteInteger.ZERO;
    113
    114    private ZipEightByteInteger createTime = ZipEightByteInteger.ZERO;
    115
    116    @Override
    117    public boolean equals(final Object o) {
    118        if (o instanceof X000A_NTFS) {
    119            final X000A_NTFS xf = (X000A_NTFS) o;
    120
    121            return Objects.equals(modifyTime, xf.modifyTime) && Objects.equals(accessTime, xf.accessTime) && Objects.equals(createTime, xf.createTime);
    122        }
    123        return false;
    124    }
    125
    126    /**
    127     * Gets the access time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    128     *
    129     * @return access time as a {@link FileTime} or null.
    130     * @since 1.23
    131     */
    132    public FileTime getAccessFileTime() {
    133        return zipToFileTime(accessTime);
    134    }
    135
    136    /**
    137     * Gets the access time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    138     *
    139     * @return access time as java.util.Date or null.
    140     */
    141    public Date getAccessJavaTime() {
    142        return zipToDate(accessTime);
    143    }
    144
    145    /**
    146     * Gets the "File last access time" of this ZIP entry as a ZipEightByteInteger object, or {@link ZipEightByteInteger#ZERO} if no such timestamp exists in
    147     * the ZIP entry.
    148     *
    149     * @return File last access time
    150     */
    151    public ZipEightByteInteger getAccessTime() {
    152        return accessTime;
    153    }
    154
    155    /**
    156     * Gets the actual data to put into central directory data - without Header-ID or length specifier.
    157     *
    158     * @return the central directory data
    159     */
    160    @Override
    161    public byte[] getCentralDirectoryData() {
    162        return getLocalFileDataData();
    163    }
    164
    165    /**
    166     * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
    167     *
    168     * <p>
    169     * For X5455 the central length is often smaller than the local length, because central cannot contain access or create timestamps.
    170     * </p>
    171     *
    172     * @return a {@code ZipShort} for the length of the data of this extra field
    173     */
    174    @Override
    175    public ZipShort getCentralDirectoryLength() {
    176        return getLocalFileDataLength();
    177    }
    178
    179    /**
    180     * Gets the create time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    181     *
    182     * @return create time as a {@link FileTime} or null.
    183     * @since 1.23
    184     */
    185    public FileTime getCreateFileTime() {
    186        return zipToFileTime(createTime);
    187    }
    188
    189    /**
    190     * Gets the create time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    191     *
    192     * @return create time as java.util.Date or null.
    193     */
    194    public Date getCreateJavaTime() {
    195        return zipToDate(createTime);
    196    }
    197
    198    /**
    199     * Gets the "File creation time" of this ZIP entry as a ZipEightByteInteger object, or {@link ZipEightByteInteger#ZERO} if no such timestamp exists in the
    200     * ZIP entry.
    201     *
    202     * @return File creation time
    203     */
    204    public ZipEightByteInteger getCreateTime() {
    205        return createTime;
    206    }
    207
    208    /**
    209     * Gets the Header-ID.
    210     *
    211     * @return the value for the header id for this extrafield
    212     */
    213    @Override
    214    public ZipShort getHeaderId() {
    215        return HEADER_ID;
    216    }
    217
    218    /**
    219     * Gets the actual data to put into local file data - without Header-ID or length specifier.
    220     *
    221     * @return get the data
    222     */
    223    @Override
    224    public byte[] getLocalFileDataData() {
    225        final byte[] data = new byte[getLocalFileDataLength().getValue()];
    226        int pos = 4;
    227        System.arraycopy(TIME_ATTR_TAG.getBytes(), 0, data, pos, 2);
    228        pos += 2;
    229        System.arraycopy(TIME_ATTR_SIZE.getBytes(), 0, data, pos, 2);
    230        pos += 2;
    231        System.arraycopy(modifyTime.getBytes(), 0, data, pos, 8);
    232        pos += 8;
    233        System.arraycopy(accessTime.getBytes(), 0, data, pos, 8);
    234        pos += 8;
    235        System.arraycopy(createTime.getBytes(), 0, data, pos, 8);
    236        return data;
    237    }
    238
    239    /**
    240     * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
    241     *
    242     * @return a {@code ZipShort} for the length of the data of this extra field
    243     */
    244    @Override
    245    public ZipShort getLocalFileDataLength() {
    246        return new ZipShort(4 /* reserved */
    247                + 2 /* Tag#1 */
    248                + 2 /* Size#1 */
    249                + 3 * 8 /* time values */);
    250    }
    251
    252    /**
    253     * Gets the modify time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    254     *
    255     * @return modify time as a {@link FileTime} or null.
    256     * @since 1.23
    257     */
    258    public FileTime getModifyFileTime() {
    259        return zipToFileTime(modifyTime);
    260    }
    261
    262    /**
    263     * Gets the modify time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry.
    264     *
    265     * @return modify time as java.util.Date or null.
    266     */
    267    public Date getModifyJavaTime() {
    268        return zipToDate(modifyTime);
    269    }
    270
    271    /**
    272     * Gets the "File last modification time" of this ZIP entry as a ZipEightByteInteger object, or {@link ZipEightByteInteger#ZERO} if no such timestamp exists
    273     * in the ZIP entry.
    274     *
    275     * @return File last modification time
    276     */
    277    public ZipEightByteInteger getModifyTime() {
    278        return modifyTime;
    279    }
    280
    281    @Override
    282    public int hashCode() {
    283        int hc = -123;
    284        if (modifyTime != null) {
    285            hc ^= modifyTime.hashCode();
    286        }
    287        if (accessTime != null) {
    288            // Since accessTime is often same as modifyTime,
    289            // this prevents them from XOR negating each other.
    290            hc ^= Integer.rotateLeft(accessTime.hashCode(), 11);
    291        }
    292        if (createTime != null) {
    293            hc ^= Integer.rotateLeft(createTime.hashCode(), 22);
    294        }
    295        return hc;
    296    }
    297
    298    /**
    299     * Doesn't do anything special since this class always uses the same parsing logic for both central directory and local file data.
    300     */
    301    @Override
    302    public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, final int length) throws ZipException {
    303        reset();
    304        parseFromLocalFileData(buffer, offset, length);
    305    }
    306
    307    /**
    308     * Populate data from this array as if it was in local file data.
    309     *
    310     * @param data   an array of bytes
    311     * @param offset the start offset
    312     * @param length the number of bytes in the array from offset
    313     * @throws java.util.zip.ZipException on error
    314     */
    315    @Override
    316    public void parseFromLocalFileData(final byte[] data, int offset, final int length) throws ZipException {
    317        final int len = offset + length;
    318
    319        // skip reserved
    320        offset += 4;
    321
    322        while (offset + 4 <= len) {
    323            final ZipShort tag = new ZipShort(data, offset);
    324            offset += 2;
    325            if (tag.equals(TIME_ATTR_TAG)) {
    326                readTimeAttr(data, offset, len - offset);
    327                break;
    328            }
    329            final ZipShort size = new ZipShort(data, offset);
    330            offset += 2 + size.getValue();
    331        }
    332    }
    333
    334    private void readTimeAttr(final byte[] data, int offset, final int length) {
    335        if (length >= 2 + 3 * 8) {
    336            final ZipShort tagValueLength = new ZipShort(data, offset);
    337            if (TIME_ATTR_SIZE.equals(tagValueLength)) {
    338                offset += 2;
    339                modifyTime = new ZipEightByteInteger(data, offset);
    340                offset += 8;
    341                accessTime = new ZipEightByteInteger(data, offset);
    342                offset += 8;
    343                createTime = new ZipEightByteInteger(data, offset);
    344            }
    345        }
    346    }
    347
    348    /**
    349     * Reset state back to newly constructed state. Helps us make sure parse() calls always generate clean results.
    350     */
    351    private void reset() {
    352        this.modifyTime = ZipEightByteInteger.ZERO;
    353        this.accessTime = ZipEightByteInteger.ZERO;
    354        this.createTime = ZipEightByteInteger.ZERO;
    355    }
    356
    357    /**
    358     * Sets the access time.
    359     *
    360     * @param time access time as a {@link FileTime}
    361     * @since 1.23
    362     */
    363    public void setAccessFileTime(final FileTime time) {
    364        setAccessTime(fileTimeToZip(time));
    365    }
    366
    367    /**
    368     * Sets the access time as a java.util.Date of this ZIP entry.
    369     *
    370     * @param d access time as java.util.Date
    371     */
    372    public void setAccessJavaTime(final Date d) {
    373        setAccessTime(dateToZip(d));
    374    }
    375
    376    /**
    377     * Sets the File last access time of this ZIP entry using a ZipEightByteInteger object.
    378     *
    379     * @param t ZipEightByteInteger of the access time
    380     */
    381    public void setAccessTime(final ZipEightByteInteger t) {
    382        accessTime = t == null ? ZipEightByteInteger.ZERO : t;
    383    }
    384
    385    /**
    386     * Sets the create time.
    387     *
    388     * @param time create time as a {@link FileTime}
    389     * @since 1.23
    390     */
    391    public void setCreateFileTime(final FileTime time) {
    392        setCreateTime(fileTimeToZip(time));
    393    }
    394
    395    /**
    396     * <p>
    397     * Sets the create time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
    398     * </p>
    399     * <p>
    400     * 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
    401     * flags is also set.
    402     * </p>
    403     *
    404     * @param d create time as java.util.Date
    405     */
    406    public void setCreateJavaTime(final Date d) {
    407        setCreateTime(dateToZip(d));
    408    }
    409
    410    /**
    411     * Sets the File creation time of this ZIP entry using a ZipEightByteInteger object.
    412     *
    413     * @param t ZipEightByteInteger of the create time
    414     */
    415    public void setCreateTime(final ZipEightByteInteger t) {
    416        createTime = t == null ? ZipEightByteInteger.ZERO : t;
    417    }
    418
    419    /**
    420     * Sets the modify time.
    421     *
    422     * @param time modify time as a {@link FileTime}
    423     * @since 1.23
    424     */
    425    public void setModifyFileTime(final FileTime time) {
    426        setModifyTime(fileTimeToZip(time));
    427    }
    428
    429    /**
    430     * Sets the modify time as a java.util.Date of this ZIP entry.
    431     *
    432     * @param d modify time as java.util.Date
    433     */
    434    public void setModifyJavaTime(final Date d) {
    435        setModifyTime(dateToZip(d));
    436    }
    437
    438    /**
    439     * Sets the File last modification time of this ZIP entry using a ZipEightByteInteger object.
    440     *
    441     * @param t ZipEightByteInteger of the modify time
    442     */
    443    public void setModifyTime(final ZipEightByteInteger t) {
    444        modifyTime = t == null ? ZipEightByteInteger.ZERO : t;
    445    }
    446
    447    /**
    448     * Returns a String representation of this class useful for debugging purposes.
    449     *
    450     * @return A String representation of this class useful for debugging purposes.
    451     */
    452    @Override
    453    public String toString() {
    454        // @formatter:off
    455        return new StringBuilder()
    456            .append("0x000A Zip Extra Field:")
    457            .append(" Modify:[")
    458            .append(getModifyFileTime())
    459            .append("] ")
    460            .append(" Access:[")
    461            .append(getAccessFileTime())
    462            .append("] ")
    463            .append(" Create:[")
    464            .append(getCreateFileTime())
    465            .append("] ")
    466            .toString();
    467        // @formatter:on
    468    }
    469}