Skip to content

Commit

Permalink
Expose extra-field time extraction utility logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Sep 2, 2023
1 parent edf65fe commit 0119973
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 79 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>software.coley</groupId>
<artifactId>lljzip</artifactId>
<version>2.1.4</version>
<version>2.2.0</version>

<name>LL Java ZIP</name>
<description>Lower level ZIP support for Java</description>
Expand Down
160 changes: 160 additions & 0 deletions src/main/java/software/coley/lljzip/util/ExtraFieldTime.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package software.coley.lljzip.util;

import software.coley.lljzip.format.model.CentralDirectoryFileHeader;
import software.coley.lljzip.format.model.LocalFileHeader;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.TimeUnit;

/**
* Utils for extracting more detailed timestamps from file headers.
*
* @author Matt Coley
*/
public class ExtraFieldTime {
/**
* @param header
* File header to pull detailed time from.
*
* @return Time wrapper if values were found. Otherwise, {@code null}.
*/
@Nullable
public static TimeWrapper read(@Nonnull CentralDirectoryFileHeader header) {
int extraLen = header.getExtraFieldLength();
if (extraLen > 0 && extraLen < 0xFFFF) {
ByteData extra = header.getExtraField();
return read(extra);
}
return null;
}

/**
* @param header
* File header to pull detailed time from.
*
* @return Time wrapper if values were found. Otherwise, {@code null}.
*/
@Nullable
public static TimeWrapper read(@Nonnull LocalFileHeader header) {
int extraLen = header.getExtraFieldLength();
if (extraLen > 0 && extraLen < 0xFFFF) {
ByteData extra = header.getExtraField();
return read(extra);
}
return null;
}

@Nonnull
private static TimeWrapper read(@Nonnull ByteData extra) {
TimeWrapper wrapper = new TimeWrapper();
// Reimplementation of 'java.util.zip.ZipEntry#setExtra0(...)'
int off = 0;
int len = (int) extra.length();
while (off + 4 < len) {
int tag = extra.getShort(off);
int size = extra.getShort(off + 2);
off += 4;
if (off + size > len)
break;
if (tag == /* EXTID_NTFS */ 0xA) {
if (size < 32) // reserved 4 bytes + tag 2 bytes + size 2 bytes
break; // m[a|c]time 24 bytes
int pos = off + 4;
if (extra.getShort(pos) != 0x0001 || extra.getShort(pos + 2) != 24)
break;
long wtime;
wtime = extra.getInt(pos + 4) | ((long) extra.getInt(pos + 8) << 32);
if (wtime != Long.MIN_VALUE) {
wrapper.modify = winTimeToFileTime(wtime).toMillis();
}
wtime = extra.getInt(pos + 12) | ((long) extra.getInt(pos + 16) << 32);
if (wtime != Long.MIN_VALUE) {
wrapper.access = winTimeToFileTime(wtime).toMillis();
}
wtime = extra.getInt(pos + 20) | ((long) extra.getInt(pos + 8) << 24);
if (wtime != Long.MIN_VALUE) {
wrapper.creation = winTimeToFileTime(wtime).toMillis();
}
} else if (tag == /* EXTID_EXTT */ 0x5455) {
int flag = extra.get(off);
int localOff = 1;
// The CEN-header extra field contains the modification
// time only, or no timestamp at all. 'sz' is used to
// flag its presence or absence. But if mtime is present
// in LOC it must be present in CEN as well.
if ((flag & 0x1) != 0 && (localOff + 4) <= size) {
// get32S(extra, off + localOff)
wrapper.modify = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
if ((flag & 0x2) != 0 && (localOff + 4) <= size) {
wrapper.access = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
if ((flag & 0x4) != 0 && (localOff + 4) <= size) {
wrapper.creation = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
}
off += size;
}
return wrapper;
}

/**
* Conversion of windows time to {@link FileTime}.
*
* @param time
* Input windows time value, in microseconds from the windows epoch.
*
* @return Mapped file time.
*/
@Nonnull
public static FileTime winTimeToFileTime(long time) {
return FileTime.from(time / 10 + -11644473600000000L /* windows epoch */, TimeUnit.MICROSECONDS);
}

/**
* Conversion of unix time to {@link FileTime}.
*
* @param utime
* Input unix time value in seconds.
*
* @return Mapped file time.
*/
@Nonnull
public static FileTime unixTimeToFileTime(long utime) {
return FileTime.from(utime, TimeUnit.SECONDS);
}

/**
* Time wrapper for creation/access/modify times stored in {@link LocalFileHeader#getExtraField()} and
* {@link CentralDirectoryFileHeader#getExtraField()}.
*/
public static class TimeWrapper {
private long creation, access, modify;

/**
* @return Unix timestamp of creation time.
*/
public long getCreationMs() {
return creation;
}

/**
* @return Unix timestamp of access time.
*/
public long getAccessMs() {
return access;
}

/**
* @return Unix timestamp of modification time.
*/
public long getModifyMs() {
return modify;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import org.junit.jupiter.params.provider.ValueSource;
import software.coley.lljzip.format.model.LocalFileHeader;
import software.coley.lljzip.format.model.ZipArchive;
import software.coley.lljzip.util.ByteData;
import software.coley.lljzip.util.ExtraFieldTime;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.TimeUnit;
import java.util.Objects;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -45,88 +44,19 @@ public void validity(@Nonnull String mode) {
default:
throw new IllegalStateException();
}
Wrapper wrapper = read(archive);
assertEquals(timeCreate, wrapper.creation);
assertEquals(timeModify, wrapper.modify);
assertEquals(timeAccess, wrapper.access);
ExtraFieldTime.TimeWrapper wrapper = read(archive);
assertEquals(timeCreate, wrapper.getCreationMs());
assertEquals(timeModify, wrapper.getModifyMs());
assertEquals(timeAccess, wrapper.getAccessMs());
} catch (IOException ex) {
fail(ex);
}
}

@Nonnull
private Wrapper read(@Nonnull ZipArchive archive) {
private ExtraFieldTime.TimeWrapper read(@Nonnull ZipArchive archive) {
LocalFileHeader header = archive.getLocalFiles().get(0);
Wrapper wrapper = new Wrapper();
int extraLen = header.getExtraFieldLength();
if (extraLen > 0 && extraLen < 0xFFFF) {
// Reimplementation of 'java.util.zip.ZipEntry#setExtra0(...)'
ByteData extra = header.getExtraField();
int off = 0;
int len = (int) extra.length();
while (off + 4 < len) {
int tag = extra.getShort(off);
int size = extra.getShort(off + 2);
off += 4;
if (off + size > len)
break;
if (tag == /* EXTID_NTFS */ 0xA) {
if (size < 32) // reserved 4 bytes + tag 2 bytes + size 2 bytes
break; // m[a|c]time 24 bytes
int pos = off + 4;
if (extra.getShort(pos) != 0x0001 || extra.getShort(pos + 2) != 24)
break;
long wtime;
wtime = extra.getInt(pos + 4) | ((long) extra.getInt(pos + 8) << 32);
if (wtime != Long.MIN_VALUE) {
wrapper.modify = winTimeToFileTime(wtime).toMillis();
}
wtime = extra.getInt(pos + 12) | ((long) extra.getInt(pos + 16) << 32);
if (wtime != Long.MIN_VALUE) {
wrapper.access = winTimeToFileTime(wtime).toMillis();
}
wtime = extra.getInt(pos + 20) | ((long) extra.getInt(pos + 8) << 24);
if (wtime != Long.MIN_VALUE) {
wrapper.creation = winTimeToFileTime(wtime).toMillis();
}
} else if (tag == /* EXTID_EXTT */ 0x5455) {
int flag = extra.get(off);
int localOff = 1;
// The CEN-header extra field contains the modification
// time only, or no timestamp at all. 'sz' is used to
// flag its presence or absence. But if mtime is present
// in LOC it must be present in CEN as well.
if ((flag & 0x1) != 0 && (localOff + 4) <= size) {
// get32S(extra, off + localOff)
wrapper.modify = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
if ((flag & 0x2) != 0 && (localOff + 4) <= size) {
wrapper.access = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
if ((flag & 0x4) != 0 && (localOff + 4) <= size) {
wrapper.creation = unixTimeToFileTime(extra.getInt(off + localOff)).toMillis();
localOff += 4;
}
}
off += size;
}
}
return wrapper;
}

@Nonnull
public static FileTime winTimeToFileTime(long time) {
return FileTime.from(time / 10 + -11644473600000000L /* windows epoch */, TimeUnit.MICROSECONDS);
return Objects.requireNonNull(ExtraFieldTime.read(header), "Missing time data");
}

@Nonnull
public static FileTime unixTimeToFileTime(long utime) {
return FileTime.from(utime, TimeUnit.SECONDS);
}

private static class Wrapper {
private long creation, access, modify;
}
}

0 comments on commit 0119973

Please sign in to comment.