Skip to content

Commit 0119973

Browse files
committed
Expose extra-field time extraction utility logic
1 parent edf65fe commit 0119973

File tree

3 files changed

+169
-79
lines changed

3 files changed

+169
-79
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>software.coley</groupId>
88
<artifactId>lljzip</artifactId>
9-
<version>2.1.4</version>
9+
<version>2.2.0</version>
1010

1111
<name>LL Java ZIP</name>
1212
<description>Lower level ZIP support for Java</description>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
}

src/test/java/software/coley/lljzip/WindowsExtraFieldTimeTests.java

+8-78
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
import org.junit.jupiter.params.provider.ValueSource;
55
import software.coley.lljzip.format.model.LocalFileHeader;
66
import software.coley.lljzip.format.model.ZipArchive;
7-
import software.coley.lljzip.util.ByteData;
7+
import software.coley.lljzip.util.ExtraFieldTime;
88

99
import javax.annotation.Nonnull;
1010
import java.io.IOException;
1111
import java.nio.file.Path;
1212
import java.nio.file.Paths;
13-
import java.nio.file.attribute.FileTime;
14-
import java.util.concurrent.TimeUnit;
13+
import java.util.Objects;
1514

1615
import static org.junit.jupiter.api.Assertions.assertEquals;
1716
import static org.junit.jupiter.api.Assertions.fail;
@@ -45,88 +44,19 @@ public void validity(@Nonnull String mode) {
4544
default:
4645
throw new IllegalStateException();
4746
}
48-
Wrapper wrapper = read(archive);
49-
assertEquals(timeCreate, wrapper.creation);
50-
assertEquals(timeModify, wrapper.modify);
51-
assertEquals(timeAccess, wrapper.access);
47+
ExtraFieldTime.TimeWrapper wrapper = read(archive);
48+
assertEquals(timeCreate, wrapper.getCreationMs());
49+
assertEquals(timeModify, wrapper.getModifyMs());
50+
assertEquals(timeAccess, wrapper.getAccessMs());
5251
} catch (IOException ex) {
5352
fail(ex);
5453
}
5554
}
5655

5756
@Nonnull
58-
private Wrapper read(@Nonnull ZipArchive archive) {
57+
private ExtraFieldTime.TimeWrapper read(@Nonnull ZipArchive archive) {
5958
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);
59+
return Objects.requireNonNull(ExtraFieldTime.read(header), "Missing time data");
12260
}
12361

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-
}
13262
}

0 commit comments

Comments
 (0)