Skip to content

Commit ddcd036

Browse files
committed
Small update
1 parent 59142b8 commit ddcd036

1 file changed

Lines changed: 63 additions & 21 deletions

File tree

pycatfile/pycatfile.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import inspect
3939
import tempfile
4040
import configparser
41+
from zoneinfo import ZoneInfo
4142
from io import open, StringIO, BytesIO
4243
__enable_pywwwget__ = True
4344
pywwwget = False
@@ -1081,30 +1082,71 @@ def format_ns_local(ts_ns, fmt='%Y-%m-%d %H:%M:%S'):
10811082
ns_str = "%09d" % ns
10821083
return base + "." + ns_str
10831084

1084-
def get_unix_timestamp_zip(member):
1085+
WINDOWS_EPOCH_DELTA = 11644473600 # seconds between 1601-01-01 and 1970-01-01
1086+
1087+
def _filetime_to_unix_seconds(filetime: int) -> int:
1088+
# FILETIME is 100-ns intervals since 1601-01-01 UTC
1089+
return int(filetime // 10_000_000 - WINDOWS_EPOCH_DELTA)
1090+
1091+
def _parse_ext_timestamp_0x5455(extra_data: bytes) -> int | None:
1092+
# Layout: [flags:1][mtime?:4][atime?:4][ctime?:4] (only if flags bits set)
1093+
if len(extra_data) < 1:
1094+
return None
1095+
flags = extra_data[0]
1096+
off = 1
1097+
if flags & 0x01:
1098+
if off + 4 <= len(extra_data):
1099+
(mtime,) = struct.unpack_from("<I", extra_data, off)
1100+
return int(mtime)
1101+
return None
1102+
1103+
def _parse_ntfs_0x000a(extra_data: bytes) -> int | None:
1104+
# Layout: [reserved:4] then attributes:
1105+
# attr_tag(2), attr_size(2), attr_data(attr_size)
1106+
if len(extra_data) < 4:
1107+
return None
1108+
off = 4
1109+
while off + 4 <= len(extra_data):
1110+
attr_tag, attr_size = struct.unpack_from("<HH", extra_data, off)
1111+
off += 4
1112+
if off + attr_size > len(extra_data):
1113+
break
1114+
attr = extra_data[off:off + attr_size]
1115+
off += attr_size
1116+
1117+
# 0x0001 attribute contains 3 FILETIMEs: mtime, atime, ctime (each 8 bytes)
1118+
if attr_tag == 0x0001 and len(attr) >= 24:
1119+
(mtime_filetime,) = struct.unpack_from("<Q", attr, 0)
1120+
return _filetime_to_unix_seconds(mtime_filetime)
1121+
return None
1122+
1123+
def get_unix_timestamp_zip(member, fallback_tz: str = "America/Chicago") -> int:
10851124
extra = member.extra
10861125
i = 0
1087-
1088-
# 1. Try to find UTC Extra Fields
1089-
while i + 4 <= len(extra):
1090-
tag, length = struct.unpack('<HH', extra[i:i+4])
1091-
data = extra[i+4 : i+4+length]
1092-
1093-
# 0x5455: Info-ZIP (Unix)
1094-
if tag == 0x5455 and len(data) >= 5:
1095-
if data[0] & 1:
1096-
return struct.unpack('<I', data[1:5])[0]
1097-
1098-
# 0x000a: NTFS (Windows)
1099-
elif tag == 0x000a and len(data) >= 24:
1100-
ntfs_mtime = struct.unpack('<Q', data[8:16])[0]
1101-
return int((ntfs_mtime / 1e7) - 11644473600)
1102-
1103-
i += 4 + length
11041126

1105-
# 2. Fallback: Convert MS-DOS date_time to Unix integer
1106-
dt = datetime.datetime(*member.date_time)
1107-
return int(dt.replace(tzinfo=datetime.timezone.utc).timestamp())
1127+
# 1) Prefer UTC-capable extra fields
1128+
while i + 4 <= len(extra):
1129+
tag, length = struct.unpack_from("<HH", extra, i)
1130+
i += 4
1131+
data = extra[i:i+length]
1132+
i += length
1133+
1134+
if tag == 0x5455:
1135+
ts = _parse_ext_timestamp_0x5455(data)
1136+
if ts is not None:
1137+
return ts
1138+
1139+
elif tag == 0x000A:
1140+
ts = _parse_ntfs_0x000a(data)
1141+
if ts is not None:
1142+
return ts
1143+
1144+
# 2) Fallback: DOS local time -> interpret in fallback_tz -> UTC
1145+
# ZIP DOS timestamps are "local time" with no TZ info.
1146+
local_naive = datetime.datetime(*member.date_time)
1147+
local_dt = local_naive.replace(tzinfo=ZoneInfo(fallback_tz))
1148+
utc_dt = local_dt.astimezone(datetime.timezone.utc)
1149+
return int(utc_dt.timestamp())
11081150

11091151
def CheckSumSupport(checkfor, guaranteed=True):
11101152
if(guaranteed):

0 commit comments

Comments
 (0)