use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct UnixTimes {
pub mtime: Option<u64>,
pub atime: Option<u64>,
pub ctime: Option<u64>,
}
impl UnixTimes {
pub fn all(secs: u64) -> Self {
Self {
mtime: Some(secs),
atime: Some(secs),
ctime: Some(secs),
}
}
pub fn mtime_only(secs: u64) -> Self {
Self {
mtime: Some(secs),
atime: None,
ctime: None,
}
}
pub fn mtime_or_now(&self) -> u64 {
self.mtime.or(self.atime).or(self.ctime).unwrap_or_else(now)
}
pub fn atime_or_now(&self) -> u64 {
self.atime.or(self.mtime).or(self.ctime).unwrap_or_else(now)
}
pub fn ctime_or_now(&self) -> u64 {
self.ctime.or(self.mtime).or(self.atime).unwrap_or_else(now)
}
pub fn is_empty(&self) -> bool {
self.mtime.is_none() && self.atime.is_none() && self.ctime.is_none()
}
}
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn now_u32() -> u32 {
now() as u32
}
pub fn resolve_or_now(supplied: Option<UnixTimes>) -> UnixTimes {
match supplied {
Some(t) if !t.is_empty() => t,
_ => UnixTimes::all(now()),
}
}
fn ymd_hms(secs: u64) -> (i64, u32, u32, u32, u32, u32) {
let sec_of_day = secs % 86400;
let hour = (sec_of_day / 3600) as u32;
let minute = ((sec_of_day % 3600) / 60) as u32;
let second = (sec_of_day % 60) as u32;
let mut days = (secs / 86400) as i64;
days += 719468;
let era = if days >= 0 { days } else { days - 146096 } / 146097;
let doe = (days - era * 146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
(year, m, d, hour, minute, second)
}
fn secs_from_ymd_hms(year: i64, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u64 {
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u32;
let m = month;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146097 + doe as i64 - 719468;
(days as u64) * 86400 + hour as u64 * 3600 + minute as u64 * 60 + second as u64
}
fn is_leap(year: i64) -> bool {
(year % 4 == 0) && (year % 100 != 0 || year % 400 == 0)
}
fn days_in_month(year: i64, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap(year) {
29
} else {
28
}
}
_ => 0,
}
}
const DOS_EPOCH_SECS: u64 = 315_532_800;
pub fn unix_to_dos_datetime(secs: u64) -> (u16, u16) {
let secs = secs.max(DOS_EPOCH_SECS);
let (year, month, day, hour, minute, second) = ymd_hms(secs);
if year > 2107 {
return ((127 << 9) | (12 << 5) | 31, (23 << 11) | (59 << 5) | 29);
}
let year_since_1980 = (year - 1980) as u16;
let date = (year_since_1980 << 9) | ((month as u16 & 0x0F) << 5) | (day as u16 & 0x1F);
let time =
((hour as u16 & 0x1F) << 11) | ((minute as u16 & 0x3F) << 5) | ((second as u16 / 2) & 0x1F);
(date, time)
}
pub fn dos_datetime_to_unix(date: u16, time: u16) -> Option<u64> {
if date == 0 {
return None;
}
let day = (date & 0x1F) as u32;
let month = ((date >> 5) & 0x0F) as u32;
let year = ((date >> 9) & 0x7F) as i64 + 1980;
let second = ((time & 0x1F) as u32) * 2;
let minute = ((time >> 5) & 0x3F) as u32;
let hour = ((time >> 11) & 0x1F) as u32;
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return None;
}
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some(secs_from_ymd_hms(year, month, day, hour, minute, second))
}
pub fn unix_to_exfat_timestamp(secs: u64) -> u32 {
let (date, time) = unix_to_dos_datetime(secs);
((date as u32) << 16) | time as u32
}
pub fn exfat_timestamp_to_unix(ts: u32) -> Option<u64> {
dos_datetime_to_unix((ts >> 16) as u16, ts as u16)
}
const NTFS_EPOCH_OFFSET_SECS: u64 = 11_644_473_600;
pub fn unix_to_filetime(secs: u64) -> u64 {
secs.saturating_add(NTFS_EPOCH_OFFSET_SECS)
.saturating_mul(10_000_000)
}
pub fn filetime_to_unix(ft: u64) -> Option<u64> {
if ft == 0 {
return None;
}
let total_secs = ft / 10_000_000;
if total_secs < NTFS_EPOCH_OFFSET_SECS {
return None;
}
Some(total_secs - NTFS_EPOCH_OFFSET_SECS)
}
const MAC_EPOCH_DELTA: u64 = 2_082_844_800;
pub fn local_utc_offset_secs() -> i64 {
chrono::Local::now().offset().local_minus_utc() as i64
}
pub fn unix_to_mac_local(secs: u64) -> u32 {
let local = (secs as i64).saturating_add(local_utc_offset_secs()).max(0) as u64;
unix_to_mac_epoch(local)
}
pub fn mac_local_to_unix(mac_secs: u32) -> Option<u64> {
mac_epoch_to_unix(mac_secs).map(|local| {
(local as i64)
.saturating_sub(local_utc_offset_secs())
.max(0) as u64
})
}
pub fn mac_local_now() -> u32 {
unix_to_mac_local(now())
}
pub fn unix_to_mac_epoch(secs: u64) -> u32 {
u32::try_from(secs.saturating_add(MAC_EPOCH_DELTA)).unwrap_or(u32::MAX)
}
pub fn mac_epoch_to_unix(mac_secs: u32) -> Option<u64> {
if mac_secs == 0 || (mac_secs as u64) < MAC_EPOCH_DELTA {
return None;
}
Some(mac_secs as u64 - MAC_EPOCH_DELTA)
}
pub fn unix_to_prodos_datetime(secs: u64) -> (u16, u16) {
let (mut year, month, day, hour, minute, _second) = ymd_hms(secs);
if year < 1940 {
return (
((40u16) << 9) | (1u16 << 5) | 1u16, 0,
);
}
if year > 2039 {
year = 2039;
}
let year_bits = if year >= 2000 {
(year - 2000) as u16
} else {
(year - 1900) as u16
};
let date = ((year_bits & 0x7F) << 9) | ((month as u16 & 0x0F) << 5) | (day as u16 & 0x1F);
let time = ((hour as u16 & 0x1F) << 8) | (minute as u16 & 0x3F);
(date, time)
}
pub fn prodos_datetime_to_unix(date: u16, time: u16) -> Option<u64> {
if date == 0 {
return None;
}
let day = (date & 0x1F) as u32;
let month = ((date >> 5) & 0x0F) as u32;
let raw_year = ((date >> 9) & 0x7F) as i64;
let year = if raw_year < 40 {
2000 + raw_year
} else {
1900 + raw_year
};
if year < 1970 {
return None;
}
let minute = (time & 0x3F) as u32;
let hour = ((time >> 8) & 0x1F) as u32;
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return None;
}
if hour > 23 || minute > 59 {
return None;
}
Some(secs_from_ymd_hms(year, month, day, hour, minute, 0))
}
pub fn unix_to_ucsd_date(secs: u64) -> u16 {
let (year, month, day, _h, _m, _s) = ymd_hms(secs);
let year = year.clamp(1900, 1999);
let year_bits = (year - 1900) as u16;
(day as u16 & 0x1F) | ((month as u16 & 0x0F) << 5) | ((year_bits & 0x7F) << 9)
}
pub fn ucsd_date_to_unix(word: u16) -> Option<u64> {
if word == 0 {
return None;
}
let day = (word & 0x1F) as u32;
let month = ((word >> 5) & 0x0F) as u32;
let year = 1900 + ((word >> 9) & 0x7F) as i64;
if year < 1970 {
return None;
}
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return None;
}
Some(secs_from_ymd_hms(year, month, day, 0, 0, 0))
}
const ADFS_EPOCH_OFFSET_CS: u64 = 220_898_880_000;
pub fn unix_to_adfs_time(secs: u64, filetype: u16) -> (u32, u32) {
let cs = secs
.saturating_mul(100)
.saturating_add(ADFS_EPOCH_OFFSET_CS);
let load_addr = 0xFFF0_0000u32 | ((filetype as u32 & 0xFFF) << 8) | ((cs >> 32) as u32 & 0xFF);
let exec_addr = cs as u32;
(load_addr, exec_addr)
}
pub fn adfs_time_to_unix(load: u32, exec: u32) -> Option<u64> {
if load & 0xFFF0_0000 != 0xFFF0_0000 {
return None;
}
let cs = ((load as u64 & 0xFF) << 32) | exec as u64;
if cs < ADFS_EPOCH_OFFSET_CS {
return None;
}
Some((cs - ADFS_EPOCH_OFFSET_CS) / 100)
}
pub fn unix_to_lif_bcd(secs: u64) -> [u8; 6] {
let (year, month, day, hour, minute, second) = ymd_hms(secs);
let yy = (year.clamp(1970, 2069) % 100) as u32;
let bcd = |v: u32| (((v / 10) << 4) | (v % 10)) as u8;
[
bcd(yy),
bcd(month),
bcd(day),
bcd(hour),
bcd(minute),
bcd(second),
]
}
pub fn unix_to_os9_dat(secs: u64) -> [u8; 5] {
let (year, month, day, hour, minute, _s) = ymd_hms(secs);
let year = year.clamp(1900, 2155);
[
(year - 1900) as u8,
month as u8,
day as u8,
hour as u8,
minute as u8,
]
}
pub fn unix_to_os9_dcr(secs: u64) -> [u8; 3] {
let dat = unix_to_os9_dat(secs);
[dat[0], dat[1], dat[2]]
}
pub fn os9_dat_to_unix(dat: &[u8; 5]) -> Option<u64> {
if dat == &[0u8; 5] {
return None;
}
let year = 1900i64 + dat[0] as i64;
if year < 1970 {
return None;
}
let month = dat[1] as u32;
let day = dat[2] as u32;
let hour = dat[3] as u32;
let minute = dat[4] as u32;
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return None;
}
if hour > 23 || minute > 59 {
return None;
}
Some(secs_from_ymd_hms(year, month, day, hour, minute, 0))
}
pub fn os9_dcr_to_unix(dcr: &[u8; 3]) -> Option<u64> {
if dcr == &[0u8; 3] {
return None;
}
let year = 1900i64 + dcr[0] as i64;
if year < 1970 {
return None;
}
let month = dcr[1] as u32;
let day = dcr[2] as u32;
if month == 0 || month > 12 || day == 0 || day > days_in_month(year, month) {
return None;
}
Some(secs_from_ymd_hms(year, month, day, 0, 0, 0))
}
const QDOS_EPOCH_OFFSET_SECS: u64 = 283_996_800;
pub fn unix_to_qdos_date(secs: u64) -> u32 {
(secs.saturating_add(QDOS_EPOCH_OFFSET_SECS)) as u32
}
pub fn qdos_date_to_unix(ts: u32) -> Option<u64> {
if ts == 0 || (ts as u64) < QDOS_EPOCH_OFFSET_SECS {
return None;
}
Some(ts as u64 - QDOS_EPOCH_OFFSET_SECS)
}
#[cfg(test)]
mod tests {
use super::*;
const T_2020: u64 = 1_592_224_496;
#[test]
fn dos_datetime_past_2107_pins_to_the_last_moment() {
let leap_2108 = secs_from_ymd_hms(2108, 2, 29, 12, 0, 0);
let (d, t) = unix_to_dos_datetime(leap_2108);
assert_eq!(
dos_datetime_to_unix(d, t),
Some(secs_from_ymd_hms(2107, 12, 31, 23, 59, 58))
);
let (d, t) = unix_to_dos_datetime(u64::MAX / 4);
assert!(dos_datetime_to_unix(d, t).is_some());
}
#[test]
fn dos_datetime_round_trips_and_clamps() {
let (d, t) = unix_to_dos_datetime(T_2020);
assert_eq!(d, (40 << 9) | (6 << 5) | 15);
assert_eq!(t, (12 << 11) | (34 << 5) | 28);
assert_eq!(dos_datetime_to_unix(d, t), Some(T_2020));
assert_eq!(dos_datetime_to_unix(0, 0), None);
assert_eq!(unix_to_dos_datetime(0), ((1 << 5) | 1, 0));
assert_eq!(dos_datetime_to_unix(0x0001, 0), None);
}
#[test]
fn exfat_timestamp_wraps_dos_helpers() {
let ts = unix_to_exfat_timestamp(T_2020);
assert_eq!(exfat_timestamp_to_unix(ts), Some(T_2020));
assert_eq!(exfat_timestamp_to_unix(0), None);
}
#[test]
fn filetime_round_trips_and_rejects_zero() {
let ft = unix_to_filetime(T_2020);
assert_eq!(filetime_to_unix(ft), Some(T_2020));
assert_eq!(filetime_to_unix(0), None);
assert_eq!(filetime_to_unix(1), None);
}
#[test]
fn mac_epoch_round_trips() {
let m = unix_to_mac_epoch(T_2020);
assert_eq!(mac_epoch_to_unix(m), Some(T_2020));
assert_eq!(mac_epoch_to_unix(0), None);
assert_eq!(mac_epoch_to_unix(100), None);
}
#[test]
fn prodos_datetime_round_trips() {
let t = 1_592_224_440; let (d, t_word) = unix_to_prodos_datetime(t);
assert_eq!(prodos_datetime_to_unix(d, t_word), Some(t));
assert_eq!(prodos_datetime_to_unix(0, 0), None);
}
#[test]
fn prodos_year_convention_bounds() {
let (d, _) = unix_to_prodos_datetime(1_592_224_440);
assert_eq!((d >> 9) & 0x7F, 20);
let t_1990 = 645_453_240;
let (d90, t90) = unix_to_prodos_datetime(t_1990);
assert_eq!((d90 >> 9) & 0x7F, 90);
assert_eq!(prodos_datetime_to_unix(d90, t90), Some(t_1990));
}
#[test]
fn ucsd_date_round_trips_day_granular() {
let t = 1_592_179_200; let t_1990 = 645_408_000; let w = unix_to_ucsd_date(t_1990);
assert_eq!(ucsd_date_to_unix(w), Some(t_1990));
assert_eq!(ucsd_date_to_unix(0), None);
let _ = t;
}
#[test]
fn adfs_time_round_trips_with_filetype() {
let (load, exec) = unix_to_adfs_time(T_2020, 0xFFF);
assert_eq!(load & 0xFFF0_0000, 0xFFF0_0000);
assert_eq!((load >> 8) & 0xFFF, 0xFFF);
assert_eq!(adfs_time_to_unix(load, exec), Some(T_2020));
assert_eq!(adfs_time_to_unix(0x0000_8000, 0), None);
}
#[test]
fn os9_dat_and_dcr_round_trip() {
let t = 1_592_224_440; let dat = unix_to_os9_dat(t);
assert_eq!(dat, [120, 6, 15, 12, 34]);
assert_eq!(os9_dat_to_unix(&dat), Some(t));
let dcr = unix_to_os9_dcr(t);
assert_eq!(dcr, [120, 6, 15]);
assert_eq!(os9_dcr_to_unix(&dcr), Some(1_592_179_200));
assert_eq!(os9_dat_to_unix(&[0; 5]), None);
assert_eq!(os9_dcr_to_unix(&[0; 3]), None);
}
#[test]
fn qdos_date_round_trips() {
let q = unix_to_qdos_date(T_2020);
assert_eq!(qdos_date_to_unix(q), Some(T_2020));
assert_eq!(qdos_date_to_unix(0), None);
}
#[test]
fn encoders_preserve_mid_range_values_verbatim() {
assert_eq!(filetime_to_unix(unix_to_filetime(T_2020)), Some(T_2020));
assert_eq!(mac_epoch_to_unix(unix_to_mac_epoch(T_2020)), Some(T_2020));
assert_eq!(qdos_date_to_unix(unix_to_qdos_date(T_2020)), Some(T_2020));
let (l, e) = unix_to_adfs_time(T_2020, 0xFFF);
assert_eq!(adfs_time_to_unix(l, e), Some(T_2020));
}
}