Skip to main content

grit_lib/git_date/
tm.rs

1//! Git-compatible time conversion helpers (ported from Git's `date.c`).
2
3use libc::{time_t, tm};
4
5/// Unix timestamp as used by Git (`timestamp_t` is typically `uintmax_t`).
6pub type Timestamp = u64;
7
8/// Timezone in Git's signed HHMM encoding (e.g. +200 for +02:00, -500 for -05:00).
9pub type TzHhmm = i32;
10
11pub const TIMESTAMP_MAX: u64 = (((2100u64 - 1970) * 365 + 32) * 24 * 60 * 60).saturating_sub(1);
12
13/// Git's `tm_to_time_t` — like `mktime`, but without normalization of `tm_wday` / `tm_yday`.
14pub fn tm_to_time_t(tm: &tm) -> time_t {
15    const MDAYS: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
16    let year = tm.tm_year - 70;
17    if !(0..=129).contains(&year) {
18        return -1;
19    }
20    let month = tm.tm_mon;
21    if !(0..=11).contains(&month) {
22        return -1;
23    }
24    let mut day = tm.tm_mday;
25    if month < 2 || (year + 2) % 4 != 0 {
26        day -= 1;
27    }
28    if tm.tm_hour < 0 || tm.tm_min < 0 || tm.tm_sec < 0 {
29        return -1;
30    }
31    let secs =
32        (year as i64 * 365 + (year as i64 + 1) / 4 + MDAYS[month as usize] as i64 + day as i64)
33            * 24
34            * 60
35            * 60
36            + tm.tm_hour as i64 * 60 * 60
37            + tm.tm_min as i64 * 60
38            + tm.tm_sec as i64;
39    secs as time_t
40}
41
42pub fn date_overflows(t: u64) -> bool {
43    if t >= u64::MAX {
44        return true;
45    }
46    let sys: time_t = t as time_t;
47    (t as i128) != (sys as i128) || ((t < 1) != (sys < 1))
48}
49
50/// Apply Git's `tz` HHMM encoding to a UTC instant so `gmtime_r` yields wall-clock digits.
51pub fn gm_time_t(mut time: u64, tz: TzHhmm) -> Option<u64> {
52    let mut minutes = if tz < 0 { -tz } else { tz };
53    minutes = (minutes / 100) * 60 + (minutes % 100);
54    minutes = if tz < 0 { -minutes } else { minutes };
55    let adj = (minutes as i64) * 60;
56    if adj > 0 {
57        time = time.checked_add(adj as u64)?;
58    } else if adj < 0 {
59        let a = (-adj) as u64;
60        if time < a {
61            return None;
62        }
63        time -= a;
64    }
65    if date_overflows(time) {
66        return None;
67    }
68    Some(time)
69}
70
71/// `time_to_tm` — UTC `tm` for display with explicit `tz` offset metadata.
72pub unsafe fn time_to_tm(time: u64, tz: TzHhmm, out: *mut tm) -> Option<*mut tm> {
73    let t = gm_time_t(time, tz)?;
74    let tt = t as time_t;
75    let p = libc::gmtime_r(&tt, out);
76    if p.is_null() {
77        None
78    } else {
79        Some(p)
80    }
81}
82
83/// `time_to_tm_local` — `localtime_r` for the current `TZ` environment.
84pub unsafe fn time_to_tm_local(time: u64, out: *mut tm) -> Option<*mut tm> {
85    let tt = time as time_t;
86    let p = libc::localtime_r(&tt, out);
87    if p.is_null() {
88        None
89    } else {
90        Some(p)
91    }
92}
93
94/// Git's `local_time_tzoffset` — offset for `t` in the **local** zone, as HHMM encoding.
95pub unsafe fn local_time_tzoffset(t: time_t, tm_out: *mut tm) -> TzHhmm {
96    let p = libc::localtime_r(&t, tm_out);
97    if p.is_null() {
98        return 0;
99    }
100    let t_local = tm_to_time_t(&*tm_out);
101    if t_local == -1 {
102        return 0;
103    }
104    let (eastwest, offset) = if (t_local as i128) < (t as i128) {
105        (-1, (t as i128) - (t_local as i128))
106    } else {
107        (1, (t_local as i128) - (t as i128))
108    };
109    let mut offset_min = (offset / 60) as i32;
110    offset_min = (offset_min % 60) + ((offset_min / 60) * 100);
111    offset_min * eastwest
112}
113
114/// Git's `local_tzoffset` for a UTC instant.
115pub fn local_tzoffset(time: u64) -> TzHhmm {
116    if date_overflows(time) {
117        return 0;
118    }
119    let t = time as time_t;
120    let mut buf = std::mem::MaybeUninit::<tm>::uninit();
121    unsafe {
122        let tm_out = buf.as_mut_ptr();
123        local_time_tzoffset(t, tm_out)
124    }
125}
126
127/// Read `GIT_TEST_DATE_NOW` if set, else current time (seconds).
128pub fn get_time_sec() -> i64 {
129    if let Ok(s) = std::env::var("GIT_TEST_DATE_NOW") {
130        if let Ok(v) = s.parse::<i64>() {
131            return v;
132        }
133    }
134    std::time::SystemTime::now()
135        .duration_since(std::time::UNIX_EPOCH)
136        .map(|d| d.as_secs() as i64)
137        .unwrap_or(0)
138}
139
140/// Parse leading digits as base-10 (`strtoumax` / Git's `parse_timestamp`).
141pub fn parse_timestamp_prefix(s: &[u8]) -> (u64, usize) {
142    let mut i = 0usize;
143    while i < s.len() && s[i].is_ascii_digit() {
144        i += 1;
145    }
146    if i == 0 {
147        return (0, 0);
148    }
149    let n = std::str::from_utf8(&s[..i])
150        .ok()
151        .and_then(|x| x.parse::<u64>().ok())
152        .unwrap_or(0);
153    (n, i)
154}
155
156/// C `atoi` on a byte slice (optional leading sign for digits).
157pub fn atoi_bytes(s: &[u8]) -> i32 {
158    let s = trim_ascii_ws(s);
159    if s.is_empty() {
160        return 0;
161    }
162    let neg = s[0] == b'-';
163    let start = if s[0] == b'+' || s[0] == b'-' { 1 } else { 0 };
164    let mut v: i32 = 0;
165    let mut i = start;
166    while i < s.len() && s[i].is_ascii_digit() {
167        v = v.saturating_mul(10).saturating_add((s[i] - b'0') as i32);
168        i += 1;
169    }
170    if neg {
171        -v
172    } else {
173        v
174    }
175}
176
177fn trim_ascii_ws(s: &[u8]) -> &[u8] {
178    let mut a = 0;
179    let mut b = s.len();
180    while a < b && (s[a] == b' ' || s[a] == b'\t') {
181        a += 1;
182    }
183    while b > a && (s[b - 1] == b' ' || s[b - 1] == b'\t') {
184        b -= 1;
185    }
186    &s[a..b]
187}
188
189pub fn empty_tm() -> tm {
190    unsafe { std::mem::zeroed() }
191}
192
193pub fn init_tm_unknown() -> tm {
194    let mut t = unsafe { std::mem::zeroed::<tm>() };
195    t.tm_sec = -1;
196    t.tm_min = -1;
197    t.tm_hour = -1;
198    t.tm_mday = -1;
199    t.tm_mon = -1;
200    t.tm_year = -1;
201    t.tm_wday = -1;
202    t.tm_yday = -1;
203    t.tm_isdst = -1;
204    t
205}
206
207pub fn nodate(tm: &tm) -> bool {
208    (tm.tm_year & tm.tm_mon & tm.tm_mday & tm.tm_hour & tm.tm_min & tm.tm_sec) < 0
209}
210
211pub fn maybeiso8601(tm: &tm) -> bool {
212    tm.tm_hour == -1 && tm.tm_min == 0 && tm.tm_sec == 0
213}
214
215pub fn is_date_known(tm: &tm) -> bool {
216    tm.tm_year != -1 && tm.tm_mon != -1 && tm.tm_mday != -1
217}
218
219pub fn match_string(date: &[u8], pat: &str) -> usize {
220    let pb = pat.as_bytes();
221    let mut i = 0usize;
222    while i < date.len() && i < pb.len() {
223        let d = date[i];
224        let p = pb[i];
225        if d == p {
226            i += 1;
227            continue;
228        }
229        if d.eq_ignore_ascii_case(&p) {
230            i += 1;
231            continue;
232        }
233        if !d.is_ascii_alphanumeric() {
234            break;
235        }
236        return 0;
237    }
238    i
239}
240
241pub fn skip_alpha(date: &[u8]) -> usize {
242    let mut i = 1usize;
243    while i < date.len() && date[i].is_ascii_alphabetic() {
244        i += 1;
245    }
246    i
247}