Skip to main content

ad_time/protocols/
common.rs

1use crate::time_src::TimeSourceError;
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4/// Parse KerberosTime or LDAP GeneralizedTime (e.g. "20240115000000.0Z" or "20240115103000Z")
5pub fn parse_generalized_time(s: &str) -> Result<SystemTime, TimeSourceError> {
6    // Expected format: "YYYYMMDDHHmmssZ" (15 chars + Z = 16, or sometimes without Z = 15).
7    if !s.is_ascii() {
8        return Err(TimeSourceError::Parse("GeneralizedTime not ASCII".into()));
9    }
10    let s = s.trim_end_matches('Z');
11
12    // Handle fractional seconds (e.g., .0, .123)
13    let (s, _fraction) = if let Some(dot_idx) = s.find('.') {
14        (&s[0..dot_idx], &s[dot_idx + 1..])
15    } else {
16        (s, "")
17    };
18
19    if s.len() < 14 {
20        return Err(TimeSourceError::Parse(format!(
21            "GeneralizedTime too short: {:?}",
22            s
23        )));
24    }
25
26    let year: i64 = s[0..4]
27        .parse()
28        .map_err(|_| TimeSourceError::Parse("invalid year".into()))?;
29    let month: i64 = s[4..6]
30        .parse()
31        .map_err(|_| TimeSourceError::Parse("invalid month".into()))?;
32    let day: i64 = s[6..8]
33        .parse()
34        .map_err(|_| TimeSourceError::Parse("invalid day".into()))?;
35    let hour: i64 = s[8..10]
36        .parse()
37        .map_err(|_| TimeSourceError::Parse("invalid hour".into()))?;
38    let min: i64 = s[10..12]
39        .parse()
40        .map_err(|_| TimeSourceError::Parse("invalid min".into()))?;
41    let sec: i64 = s[12..14]
42        .parse()
43        .map_err(|_| TimeSourceError::Parse("invalid sec".into()))?;
44
45    if !(0..24).contains(&hour) || !(0..60).contains(&min) || !(0..60).contains(&sec) {
46        return Err(TimeSourceError::Parse(format!(
47            "GeneralizedTime time-of-day out of range: {:02}:{:02}:{:02}",
48            hour, min, sec
49        )));
50    }
51
52    let days = civil_to_days(year, month, day)?;
53    let unix_secs = days * 86400 + hour * 3600 + min * 60 + sec;
54
55    if unix_secs < 0 {
56        return Err(TimeSourceError::Parse(
57            "GeneralizedTime predates Unix epoch".into(),
58        ));
59    }
60    Ok(UNIX_EPOCH + Duration::from_secs(unix_secs as u64))
61}
62
63/// Days since Unix epoch (1970-01-01) from civil date. Valid for 1970–2199.
64pub fn civil_to_days(y: i64, m: i64, d: i64) -> Result<i64, TimeSourceError> {
65    if y < 1970 || !(1..=12).contains(&m) || !(1..=31).contains(&d) {
66        return Err(TimeSourceError::Parse(format!(
67            "invalid date {}-{:02}-{:02}",
68            y, m, d
69        )));
70    }
71    // Algorithm from Howard Hinnant (public domain).
72    let y = if m <= 2 { y - 1 } else { y };
73    let era = y / 400;
74    let yoe = y - era * 400;
75    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
76    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
77    Ok(era * 146097 + doe - 719468)
78}
79
80/// Convert SystemTime to microseconds since Unix epoch.
81pub fn system_time_to_us(t: SystemTime) -> Result<i64, TimeSourceError> {
82    t.duration_since(UNIX_EPOCH)
83        .map(|d| d.as_micros() as i64)
84        .map_err(|_| TimeSourceError::Parse("time before unix epoch".into()))
85}
86
87pub fn filetime_to_system_time(filetime: u64) -> Result<SystemTime, TimeSourceError> {
88    const FILETIME_TO_UNIX_SECS: u64 = 11_644_473_600;
89    const EPOCH_OFFSET_100NS: u64 = FILETIME_TO_UNIX_SECS * 10_000_000;
90    if filetime < EPOCH_OFFSET_100NS {
91        return Err(TimeSourceError::Parse(format!(
92            "FILETIME {} predates Unix epoch",
93            filetime
94        )));
95    }
96    let unix_100ns = filetime - EPOCH_OFFSET_100NS;
97    let secs = unix_100ns / 10_000_000;
98    let nanos = ((unix_100ns % 10_000_000) * 100) as u32;
99    Ok(UNIX_EPOCH + Duration::new(secs, nanos))
100}
101
102pub fn map_io_err(e: std::io::Error, op: &str) -> TimeSourceError {
103    use std::io::ErrorKind::*;
104    match e.kind() {
105        TimedOut | WouldBlock => TimeSourceError::Timeout,
106        ConnectionRefused => TimeSourceError::Refused,
107        _ => TimeSourceError::Protocol(format!("{}: {}", op, e)),
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use proptest::prelude::*;
115
116    proptest! {
117        #[test]
118        fn parse_generalized_time_never_panics(s in ".*") {
119            // Must not panic on any string input. Err is fine.
120            let _ = parse_generalized_time(&s);
121        }
122
123        #[test]
124        fn parse_generalized_time_valid_range_accepted(
125            year in 1970u32..2100,
126            month in 1u32..=12,
127            day in 1u32..=28,   // 28 safe across all months
128            hour in 0u32..=23,
129            min in 0u32..=59,
130            sec in 0u32..=59,
131        ) {
132            let s = format!("{:04}{:02}{:02}{:02}{:02}{:02}Z", year, month, day, hour, min, sec);
133            prop_assert!(parse_generalized_time(&s).is_ok(), "valid date rejected: {}", s);
134        }
135
136        #[test]
137        fn parse_generalized_time_out_of_range_rejected(
138            hour in 24u32..=99,
139        ) {
140            let s = format!("20240115{:02}0000Z", hour);
141            prop_assert!(parse_generalized_time(&s).is_err());
142        }
143
144        #[test]
145        fn civil_to_days_monotone(
146            y in 1970i64..2100,
147            m in 1i64..=11,  // m+1 always valid
148            d in 1i64..=27,  // d+1 always valid
149        ) {
150            let d1 = civil_to_days(y, m, d).unwrap();
151            let d2 = civil_to_days(y, m, d + 1).unwrap();
152            prop_assert!(d1 < d2, "day+1 must produce larger day count");
153        }
154
155        #[test]
156        fn civil_to_days_pre_epoch_rejected(y in 1900i64..=1969) {
157            prop_assert!(civil_to_days(y, 6, 15).is_err());
158        }
159    }
160}