|
| 1 | +package software.coley.lljzip.util; |
| 2 | + |
| 3 | +import software.coley.lljzip.format.model.CentralDirectoryFileHeader; |
| 4 | +import software.coley.lljzip.format.model.LocalFileHeader; |
| 5 | + |
| 6 | +import javax.annotation.Nonnull; |
| 7 | +import javax.annotation.Nullable; |
| 8 | +import java.nio.file.attribute.FileTime; |
| 9 | +import java.util.concurrent.TimeUnit; |
| 10 | + |
| 11 | +/** |
| 12 | + * Utils for extracting more detailed timestamps from file headers. |
| 13 | + * |
| 14 | + * @author Matt Coley |
| 15 | + */ |
| 16 | +public class ExtraFieldTime { |
| 17 | + /** |
| 18 | + * @param header |
| 19 | + * File header to pull detailed time from. |
| 20 | + * |
| 21 | + * @return Time wrapper if values were found. Otherwise, {@code null}. |
| 22 | + */ |
| 23 | + @Nullable |
| 24 | + public static TimeWrapper read(@Nonnull CentralDirectoryFileHeader header) { |
| 25 | + int extraLen = header.getExtraFieldLength(); |
| 26 | + if (extraLen > 0 && extraLen < 0xFFFF) { |
| 27 | + ByteData extra = header.getExtraField(); |
| 28 | + return read(extra); |
| 29 | + } |
| 30 | + return null; |
| 31 | + } |
| 32 | + |
| 33 | + /** |
| 34 | + * @param header |
| 35 | + * File header to pull detailed time from. |
| 36 | + * |
| 37 | + * @return Time wrapper if values were found. Otherwise, {@code null}. |
| 38 | + */ |
| 39 | + @Nullable |
| 40 | + public static TimeWrapper read(@Nonnull LocalFileHeader header) { |
| 41 | + int extraLen = header.getExtraFieldLength(); |
| 42 | + if (extraLen > 0 && extraLen < 0xFFFF) { |
| 43 | + ByteData extra = header.getExtraField(); |
| 44 | + return read(extra); |
| 45 | + } |
| 46 | + return null; |
| 47 | + } |
| 48 | + |
| 49 | + @Nonnull |
| 50 | + private static TimeWrapper read(@Nonnull ByteData extra) { |
| 51 | + TimeWrapper wrapper = new TimeWrapper(); |
| 52 | + // Reimplementation of 'java.util.zip.ZipEntry#setExtra0(...)' |
| 53 | + int off = 0; |
| 54 | + int len = (int) extra.length(); |
| 55 | + while (off + 4 < len) { |
| 56 | + int tag = extra.getShort(off); |
| 57 | + int size = extra.getShort(off + 2); |
| 58 | + off += 4; |
| 59 | + if (off + size > len) |
| 60 | + break; |
| 61 | + if (tag == /* EXTID_NTFS */ 0xA) { |
| 62 | + if (size < 32) // reserved 4 bytes + tag 2 bytes + size 2 bytes |
| 63 | + break; // m[a|c]time 24 bytes |
| 64 | + int pos = off + 4; |
| 65 | + if (extra.getShort(pos) != 0x0001 || extra.getShort(pos + 2) != 24) |
| 66 | + break; |
| 67 | + long wtime; |
| 68 | + wtime = extra.getInt(pos + 4) | ((long) extra.getInt(pos + 8) << 32); |
| 69 | + if (wtime != Long.MIN_VALUE) { |
| 70 | + wrapper.modify = winTimeToFileTime(wtime).toMillis(); |
| 71 | + } |
| 72 | + wtime = extra.getInt(pos + 12) | ((long) extra.getInt(pos + 16) << 32); |
| 73 | + if (wtime != Long.MIN_VALUE) { |
| 74 | + wrapper.access = winTimeToFileTime(wtime).toMillis(); |
| 75 | + } |
| 76 | + wtime = extra.getInt(pos + 20) | ((long) extra.getInt(pos + 8) << 24); |
| 77 | + if (wtime != Long.MIN_VALUE) { |
| 78 | + wrapper.creation = winTimeToFileTime(wtime).toMillis(); |
| 79 | + } |
| 80 | + } else if (tag == /* EXTID_EXTT */ 0x5455) { |
| 81 | + int flag = extra.get(off); |
| 82 | + int localOff = 1; |
| 83 | + // The CEN-header extra field contains the modification |
| 84 | + // time only, or no timestamp at all. 'sz' is used to |
| 85 | + // flag its presence or absence. But if mtime is present |
| 86 | + // in LOC it must be present in CEN as well. |
| 87 | + if ((flag & 0x1) != 0 && (localOff + 4) <= size) { |
| 88 | + // get32S(extra, off + localOff) |
| 89 | + wrapper.modify = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 90 | + localOff += 4; |
| 91 | + } |
| 92 | + if ((flag & 0x2) != 0 && (localOff + 4) <= size) { |
| 93 | + wrapper.access = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 94 | + localOff += 4; |
| 95 | + } |
| 96 | + if ((flag & 0x4) != 0 && (localOff + 4) <= size) { |
| 97 | + wrapper.creation = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 98 | + localOff += 4; |
| 99 | + } |
| 100 | + } |
| 101 | + off += size; |
| 102 | + } |
| 103 | + return wrapper; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Conversion of windows time to {@link FileTime}. |
| 108 | + * |
| 109 | + * @param time |
| 110 | + * Input windows time value, in microseconds from the windows epoch. |
| 111 | + * |
| 112 | + * @return Mapped file time. |
| 113 | + */ |
| 114 | + @Nonnull |
| 115 | + public static FileTime winTimeToFileTime(long time) { |
| 116 | + return FileTime.from(time / 10 + -11644473600000000L /* windows epoch */, TimeUnit.MICROSECONDS); |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Conversion of unix time to {@link FileTime}. |
| 121 | + * |
| 122 | + * @param utime |
| 123 | + * Input unix time value in seconds. |
| 124 | + * |
| 125 | + * @return Mapped file time. |
| 126 | + */ |
| 127 | + @Nonnull |
| 128 | + public static FileTime unixTimeToFileTime(long utime) { |
| 129 | + return FileTime.from(utime, TimeUnit.SECONDS); |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Time wrapper for creation/access/modify times stored in {@link LocalFileHeader#getExtraField()} and |
| 134 | + * {@link CentralDirectoryFileHeader#getExtraField()}. |
| 135 | + */ |
| 136 | + public static class TimeWrapper { |
| 137 | + private long creation, access, modify; |
| 138 | + |
| 139 | + /** |
| 140 | + * @return Unix timestamp of creation time. |
| 141 | + */ |
| 142 | + public long getCreationMs() { |
| 143 | + return creation; |
| 144 | + } |
| 145 | + |
| 146 | + /** |
| 147 | + * @return Unix timestamp of access time. |
| 148 | + */ |
| 149 | + public long getAccessMs() { |
| 150 | + return access; |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * @return Unix timestamp of modification time. |
| 155 | + */ |
| 156 | + public long getModifyMs() { |
| 157 | + return modify; |
| 158 | + } |
| 159 | + } |
| 160 | +} |
0 commit comments