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