|
| 1 | +package software.coley.lljzip; |
| 2 | + |
| 3 | +import org.junit.jupiter.params.ParameterizedTest; |
| 4 | +import org.junit.jupiter.params.provider.ValueSource; |
| 5 | +import software.coley.lljzip.format.model.LocalFileHeader; |
| 6 | +import software.coley.lljzip.format.model.ZipArchive; |
| 7 | +import software.coley.lljzip.util.ByteData; |
| 8 | + |
| 9 | +import javax.annotation.Nonnull; |
| 10 | +import java.io.IOException; |
| 11 | +import java.nio.file.Path; |
| 12 | +import java.nio.file.Paths; |
| 13 | +import java.nio.file.attribute.FileTime; |
| 14 | +import java.util.concurrent.TimeUnit; |
| 15 | + |
| 16 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 17 | +import static org.junit.jupiter.api.Assertions.fail; |
| 18 | + |
| 19 | +/** |
| 20 | + * Tests ensuring that the extra field can be read for custom/detailed windows time values. |
| 21 | + * This generally means the extra-field is assigned correctly. |
| 22 | + * |
| 23 | + * @author Matt Coley |
| 24 | + */ |
| 25 | +public class WindowsExtraFieldTimeTests { |
| 26 | + @ParameterizedTest |
| 27 | + @ValueSource(strings = {"standard", "naive", "jvm"}) |
| 28 | + public void validity(@Nonnull String mode) { |
| 29 | + Path path = Paths.get("src/test/resources/content-with-windows-time.jar"); |
| 30 | + try { |
| 31 | + long timeCreate = 1000000000000L; |
| 32 | + long timeModify = 1200000000000L; |
| 33 | + long timeAccess = 1400000000000L; |
| 34 | + ZipArchive archive; |
| 35 | + switch (mode) { |
| 36 | + case "standard": |
| 37 | + archive = ZipIO.readStandard(path); |
| 38 | + break; |
| 39 | + case "naive": |
| 40 | + archive = ZipIO.readNaive(path); |
| 41 | + break; |
| 42 | + case "jvm": |
| 43 | + archive = ZipIO.readJvm(path); |
| 44 | + break; |
| 45 | + default: |
| 46 | + throw new IllegalStateException(); |
| 47 | + } |
| 48 | + Wrapper wrapper = read(archive); |
| 49 | + assertEquals(timeCreate, wrapper.creation); |
| 50 | + assertEquals(timeModify, wrapper.modify); |
| 51 | + assertEquals(timeAccess, wrapper.access); |
| 52 | + } catch (IOException ex) { |
| 53 | + fail(ex); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + @Nonnull |
| 58 | + private Wrapper read(@Nonnull ZipArchive archive) { |
| 59 | + LocalFileHeader header = archive.getLocalFiles().get(0); |
| 60 | + Wrapper wrapper = new Wrapper(); |
| 61 | + int extraLen = header.getExtraFieldLength(); |
| 62 | + if (extraLen > 0 && extraLen < 0xFFFF) { |
| 63 | + // Reimplementation of 'java.util.zip.ZipEntry#setExtra0(...)' |
| 64 | + ByteData extra = header.getExtraField(); |
| 65 | + int off = 0; |
| 66 | + int len = (int) extra.length(); |
| 67 | + while (off + 4 < len) { |
| 68 | + int tag = extra.getShort(off); |
| 69 | + int size = extra.getShort(off + 2); |
| 70 | + off += 4; |
| 71 | + if (off + size > len) |
| 72 | + break; |
| 73 | + if (tag == /* EXTID_NTFS */ 0xA) { |
| 74 | + if (size < 32) // reserved 4 bytes + tag 2 bytes + size 2 bytes |
| 75 | + break; // m[a|c]time 24 bytes |
| 76 | + int pos = off + 4; |
| 77 | + if (extra.getShort(pos) != 0x0001 || extra.getShort(pos + 2) != 24) |
| 78 | + break; |
| 79 | + long wtime; |
| 80 | + wtime = extra.getInt(pos + 4) | ((long) extra.getInt(pos + 8) << 32); |
| 81 | + if (wtime != Long.MIN_VALUE) { |
| 82 | + wrapper.modify = winTimeToFileTime(wtime).toMillis(); |
| 83 | + } |
| 84 | + wtime = extra.getInt(pos + 12) | ((long) extra.getInt(pos + 16) << 32); |
| 85 | + if (wtime != Long.MIN_VALUE) { |
| 86 | + wrapper.access = winTimeToFileTime(wtime).toMillis(); |
| 87 | + } |
| 88 | + wtime = extra.getInt(pos + 20) | ((long) extra.getInt(pos + 8) << 24); |
| 89 | + if (wtime != Long.MIN_VALUE) { |
| 90 | + wrapper.creation = winTimeToFileTime(wtime).toMillis(); |
| 91 | + } |
| 92 | + } else if (tag == /* EXTID_EXTT */ 0x5455) { |
| 93 | + int flag = extra.get(off); |
| 94 | + int localOff = 1; |
| 95 | + // The CEN-header extra field contains the modification |
| 96 | + // time only, or no timestamp at all. 'sz' is used to |
| 97 | + // flag its presence or absence. But if mtime is present |
| 98 | + // in LOC it must be present in CEN as well. |
| 99 | + if ((flag & 0x1) != 0 && (localOff + 4) <= size) { |
| 100 | + // get32S(extra, off + localOff) |
| 101 | + wrapper.modify = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 102 | + localOff += 4; |
| 103 | + } |
| 104 | + if ((flag & 0x2) != 0 && (localOff + 4) <= size) { |
| 105 | + wrapper.access = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 106 | + localOff += 4; |
| 107 | + } |
| 108 | + if ((flag & 0x4) != 0 && (localOff + 4) <= size) { |
| 109 | + wrapper.creation = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis(); |
| 110 | + localOff += 4; |
| 111 | + } |
| 112 | + } |
| 113 | + off += size; |
| 114 | + } |
| 115 | + } |
| 116 | + return wrapper; |
| 117 | + } |
| 118 | + |
| 119 | + @Nonnull |
| 120 | + public static FileTime winTimeToFileTime(long time) { |
| 121 | + return FileTime.from(time / 10 + -11644473600000000L /* windows epoch */, TimeUnit.MICROSECONDS); |
| 122 | + } |
| 123 | + |
| 124 | + @Nonnull |
| 125 | + public static FileTime unixTimeToFileTime(long utime) { |
| 126 | + return FileTime.from(utime, TimeUnit.SECONDS); |
| 127 | + } |
| 128 | + |
| 129 | + private static class Wrapper { |
| 130 | + private long creation, access, modify; |
| 131 | + } |
| 132 | +} |
0 commit comments