Skip to main content

grit_lib/git_date/
parse.rs

1//! Git-compatible date parsing (`parse_date_basic`, `parse_date`).
2
3use super::compat::{self, time_t, tm};
4use super::tm::{
5    empty_tm, get_time_sec, init_tm_unknown, is_date_known, match_string, maybeiso8601, nodate,
6    parse_timestamp_prefix, skip_alpha, tm_to_time_t, TIMESTAMP_MAX,
7};
8
9struct TzName {
10    name: &'static str,
11    offset_hours: i32,
12    dst: i32,
13}
14
15const TIMEZONE_NAMES: &[TzName] = &[
16    TzName {
17        name: "IDLW",
18        offset_hours: -12,
19        dst: 0,
20    },
21    TzName {
22        name: "NT",
23        offset_hours: -11,
24        dst: 0,
25    },
26    TzName {
27        name: "CAT",
28        offset_hours: -10,
29        dst: 0,
30    },
31    TzName {
32        name: "HST",
33        offset_hours: -10,
34        dst: 0,
35    },
36    TzName {
37        name: "HDT",
38        offset_hours: -10,
39        dst: 1,
40    },
41    TzName {
42        name: "YST",
43        offset_hours: -9,
44        dst: 0,
45    },
46    TzName {
47        name: "YDT",
48        offset_hours: -9,
49        dst: 1,
50    },
51    TzName {
52        name: "PST",
53        offset_hours: -8,
54        dst: 0,
55    },
56    TzName {
57        name: "PDT",
58        offset_hours: -8,
59        dst: 1,
60    },
61    TzName {
62        name: "MST",
63        offset_hours: -7,
64        dst: 0,
65    },
66    TzName {
67        name: "MDT",
68        offset_hours: -7,
69        dst: 1,
70    },
71    TzName {
72        name: "CST",
73        offset_hours: -6,
74        dst: 0,
75    },
76    TzName {
77        name: "CDT",
78        offset_hours: -6,
79        dst: 1,
80    },
81    TzName {
82        name: "EST",
83        offset_hours: -5,
84        dst: 0,
85    },
86    TzName {
87        name: "EDT",
88        offset_hours: -5,
89        dst: 1,
90    },
91    TzName {
92        name: "AST",
93        offset_hours: -3,
94        dst: 0,
95    },
96    TzName {
97        name: "ADT",
98        offset_hours: -3,
99        dst: 1,
100    },
101    TzName {
102        name: "WAT",
103        offset_hours: -1,
104        dst: 0,
105    },
106    TzName {
107        name: "GMT",
108        offset_hours: 0,
109        dst: 0,
110    },
111    TzName {
112        name: "UTC",
113        offset_hours: 0,
114        dst: 0,
115    },
116    TzName {
117        name: "Z",
118        offset_hours: 0,
119        dst: 0,
120    },
121    TzName {
122        name: "WET",
123        offset_hours: 0,
124        dst: 0,
125    },
126    TzName {
127        name: "BST",
128        offset_hours: 0,
129        dst: 1,
130    },
131    TzName {
132        name: "CET",
133        offset_hours: 1,
134        dst: 0,
135    },
136    TzName {
137        name: "MET",
138        offset_hours: 1,
139        dst: 0,
140    },
141    TzName {
142        name: "MEWT",
143        offset_hours: 1,
144        dst: 0,
145    },
146    TzName {
147        name: "MEST",
148        offset_hours: 1,
149        dst: 1,
150    },
151    TzName {
152        name: "CEST",
153        offset_hours: 1,
154        dst: 1,
155    },
156    TzName {
157        name: "MESZ",
158        offset_hours: 1,
159        dst: 1,
160    },
161    TzName {
162        name: "FWT",
163        offset_hours: 1,
164        dst: 0,
165    },
166    TzName {
167        name: "FST",
168        offset_hours: 1,
169        dst: 1,
170    },
171    TzName {
172        name: "EET",
173        offset_hours: 2,
174        dst: 0,
175    },
176    TzName {
177        name: "EEST",
178        offset_hours: 2,
179        dst: 1,
180    },
181    TzName {
182        name: "WAST",
183        offset_hours: 7,
184        dst: 0,
185    },
186    TzName {
187        name: "WADT",
188        offset_hours: 7,
189        dst: 1,
190    },
191    TzName {
192        name: "CCT",
193        offset_hours: 8,
194        dst: 0,
195    },
196    TzName {
197        name: "JST",
198        offset_hours: 9,
199        dst: 0,
200    },
201    TzName {
202        name: "EAST",
203        offset_hours: 10,
204        dst: 0,
205    },
206    TzName {
207        name: "EADT",
208        offset_hours: 10,
209        dst: 1,
210    },
211    TzName {
212        name: "GST",
213        offset_hours: 10,
214        dst: 0,
215    },
216    TzName {
217        name: "NZT",
218        offset_hours: 12,
219        dst: 0,
220    },
221    TzName {
222        name: "NZST",
223        offset_hours: 12,
224        dst: 0,
225    },
226    TzName {
227        name: "NZDT",
228        offset_hours: 12,
229        dst: 1,
230    },
231    TzName {
232        name: "IDLE",
233        offset_hours: 12,
234        dst: 0,
235    },
236];
237
238pub(crate) const MONTH_NAMES: [&str; 12] = [
239    "January",
240    "February",
241    "March",
242    "April",
243    "May",
244    "June",
245    "July",
246    "August",
247    "September",
248    "October",
249    "November",
250    "December",
251];
252
253pub(crate) const WEEKDAY_NAMES: [&str; 7] = [
254    "Sundays",
255    "Mondays",
256    "Tuesdays",
257    "Wednesdays",
258    "Thursdays",
259    "Fridays",
260    "Saturdays",
261];
262
263/// Format a parsed instant like Git's `date_string`.
264pub fn date_string(date: u64, offset: i32) -> String {
265    let mut sign = '+';
266    let mut o = offset;
267    if o < 0 {
268        o = -o;
269        sign = '-';
270    }
271    format!("{} {}{:02}{:02}", date, sign, o / 60, o % 60)
272}
273
274/// Git `parse_date` — returns canonical `date_string` output.
275pub fn parse_date(date: &str) -> Result<String, ()> {
276    let (ts, off) = parse_date_basic(date)?;
277    Ok(date_string(ts, off))
278}
279
280/// Git `parse_date_basic` — UTC seconds and timezone offset in **minutes** (signed).
281pub fn parse_date_basic(date: &str) -> Result<(u64, i32), ()> {
282    let bytes = date.as_bytes();
283    let mut tm = init_tm_unknown();
284    let mut offset: i32 = -1;
285    let mut tm_gmt = 0i32;
286    let mut i = 0usize;
287
288    if bytes.first() == Some(&b'@') {
289        if let Some((ts, off)) = match_object_header_date(&bytes[1..]) {
290            return Ok((ts, off));
291        }
292    }
293
294    while i < bytes.len() {
295        let c = bytes[i];
296        if c == 0 || c == b'\n' {
297            break;
298        }
299        let mut m = 0usize;
300        if c.is_ascii_alphabetic() {
301            m = match_alpha(&bytes[i..], &mut tm, &mut offset);
302        } else if c.is_ascii_digit() {
303            m = match_digit(&bytes[i..], &mut tm, &mut offset, &mut tm_gmt);
304        } else if (c == b'-' || c == b'+') && bytes.get(i + 1).is_some_and(|x| x.is_ascii_digit()) {
305            m = match_tz(&bytes[i..], &mut offset);
306        }
307        if m == 0 {
308            m = 1;
309        }
310        i += m;
311    }
312
313    let tts = tm_to_time_t(&tm);
314    if tts < 0 {
315        return Err(());
316    }
317    let mut ts = tts as u64;
318
319    if offset == -1 {
320        tm.tm_isdst = -1;
321        let temp_time = unsafe { compat::mktime(&mut tm) };
322        let tt = ts as i128;
323        let tloc = temp_time as i128;
324        offset = if tt > tloc {
325            ((tt - tloc) / 60) as i32
326        } else {
327            -(((tloc - tt) / 60) as i32)
328        };
329    }
330
331    if tm_gmt == 0 {
332        if offset > 0 && (offset as i64) * 60 > ts as i64 {
333            return Err(());
334        }
335        if offset < 0 && (-(offset as i128)) * 60 > (TIMESTAMP_MAX as i128 - ts as i128) {
336            return Err(());
337        }
338        // Git: *timestamp -= *offset * 60 (signed; negative offset adds to the instant).
339        let ts128 = ts as i128;
340        let adj = (offset as i128) * 60;
341        let new_ts = ts128 - adj;
342        if new_ts < 0 {
343            return Err(());
344        }
345        ts = new_ts as u64;
346    }
347
348    Ok((ts, offset))
349}
350
351fn match_object_header_date(date: &[u8]) -> Option<(u64, i32)> {
352    if date.is_empty() || !date[0].is_ascii_digit() {
353        return None;
354    }
355    let (stamp, mut rest) = parse_timestamp_prefix(date);
356    if rest >= date.len() || date[rest] != b' ' {
357        return None;
358    }
359    if stamp == u64::MAX {
360        return None;
361    }
362    rest += 1;
363    if rest >= date.len() || (date[rest] != b'+' && date[rest] != b'-') {
364        return None;
365    }
366    let sign = date[rest];
367    rest += 1;
368    if rest + 4 > date.len() {
369        return None;
370    }
371    let tz_digits = std::str::from_utf8(&date[rest..rest + 4]).ok()?;
372    let ofs_raw: i32 = tz_digits.parse().ok()?;
373    let mut ofs = (ofs_raw / 100) * 60 + (ofs_raw % 100);
374    if sign == b'-' {
375        ofs = -ofs;
376    }
377    let end = rest + 4;
378    if end < date.len() && date[end] != b'\n' && date[end] != 0 {
379        return None;
380    }
381    Some((stamp, ofs))
382}
383
384/// Git `match_tz` — writes offset in minutes; returns bytes consumed.
385fn match_tz(date: &[u8], offp: &mut i32) -> usize {
386    if date.is_empty() || (date[0] != b'+' && date[0] != b'-') {
387        return 0;
388    }
389    let (hour_ul, n) = parse_timestamp_prefix(&date[1..]);
390    let mut end = 1 + n;
391    let mut min: i32 = 0;
392    let mut hour: i32 = hour_ul as i32;
393    if n == 4 {
394        min = hour % 100;
395        hour /= 100;
396    } else if n != 2 {
397        min = 99;
398    } else if end < date.len() && date[end] == b':' {
399        let (m2, n2) = parse_timestamp_prefix(&date[end + 1..]);
400        if n2 == 0 {
401            min = 99;
402        } else {
403            min = m2 as i32;
404            end += 1 + n2;
405            if end - 1 != 5 {
406                min = 99;
407            }
408        }
409    }
410    if min < 60 && hour < 24 {
411        let mut off = hour * 60 + min;
412        if date[0] == b'-' {
413            off = -off;
414        }
415        *offp = off;
416    }
417    end
418}
419
420/// Git `strtol` for a leading signed decimal slice (`end+1` style).
421fn parse_long_prefix(s: &[u8]) -> (i64, usize) {
422    if s.is_empty() {
423        return (0, 0);
424    }
425    let mut i = 0usize;
426    let neg = s[0] == b'-';
427    if s[0] == b'+' || s[0] == b'-' {
428        i = 1;
429    }
430    let start = i;
431    while i < s.len() && s[i].is_ascii_digit() {
432        i += 1;
433    }
434    if i == start {
435        return (0, 0);
436    }
437    let Ok(slice) = std::str::from_utf8(&s[start..i]) else {
438        return (0, 0);
439    };
440    let Ok(v) = slice.parse::<i64>() else {
441        return (0, 0);
442    };
443    let v = if neg { -v } else { v };
444    (v, i)
445}
446
447fn parse_uint_suffix(s: &[u8]) -> (u64, usize) {
448    parse_timestamp_prefix(s)
449}
450
451/// Git `set_date` — `0` ok, `1` reject try, `-1` error.
452fn set_date(year: i32, month: i32, day: i32, now_tm: Option<&tm>, now: i64, tm: &mut tm) -> i32 {
453    if !(month > 0 && month < 13 && day > 0 && day < 32) {
454        return -1;
455    }
456    let Some(nt) = now_tm else {
457        tm.tm_mon = month - 1;
458        tm.tm_mday = day;
459        if year == -1 {
460            return 1;
461        }
462        if (1970..2100).contains(&year) {
463            tm.tm_year = year - 1900;
464        } else if (70..100).contains(&year) {
465            tm.tm_year = year;
466        } else if year < 38 {
467            tm.tm_year = year + 100;
468        } else {
469            return -1;
470        }
471        return 0;
472    };
473    let mut check = *tm;
474    check.tm_mon = month - 1;
475    check.tm_mday = day;
476    if year == -1 {
477        check.tm_year = nt.tm_year;
478    } else if (1970..2100).contains(&year) {
479        check.tm_year = year - 1900;
480    } else if (70..100).contains(&year) {
481        check.tm_year = year;
482    } else if year < 38 {
483        check.tm_year = year + 100;
484    } else {
485        return -1;
486    }
487    let specified = tm_to_time_t(&check);
488    if specified >= 0 && now + 10 * 24 * 3600 < specified {
489        return -1;
490    }
491    tm.tm_mon = check.tm_mon;
492    tm.tm_mday = check.tm_mday;
493    if year != -1 {
494        tm.tm_year = check.tm_year;
495    }
496    0
497}
498
499fn set_time(hour: i64, minute: i64, second: i64, tm: &mut tm) -> i32 {
500    if (0..=24).contains(&hour) && (0..60).contains(&minute) && (0..=60).contains(&second) {
501        tm.tm_hour = hour as i32;
502        tm.tm_min = minute as i32;
503        tm.tm_sec = second as i32;
504        0
505    } else {
506        -1
507    }
508}
509
510/// Git `match_multi_number` — `sep_i` is index of separator in `date`; returns bytes consumed from `date` start.
511pub(crate) fn match_multi_number(
512    num: u64,
513    date: &[u8],
514    sep_i: usize,
515    tm: &mut tm,
516    now_in: i64,
517) -> usize {
518    let Some(&c) = date.get(sep_i) else {
519        return 0;
520    };
521    if !matches!(c, b':' | b'-' | b'/' | b'.') {
522        return 0;
523    }
524
525    let (num2, n2) = parse_long_prefix(&date[sep_i + 1..]);
526    if n2 == 0 {
527        return 0;
528    }
529    let mut pos = sep_i + 1 + n2;
530    let mut num3: i64 = -1;
531    if pos < date.len() && date[pos] == c && pos + 1 < date.len() && date[pos + 1].is_ascii_digit()
532    {
533        let (n3, rel) = parse_long_prefix(&date[pos + 1..]);
534        num3 = n3;
535        pos += 1 + rel;
536    }
537
538    match c {
539        b':' => {
540            let mut n3 = num3;
541            if n3 < 0 {
542                n3 = 0;
543            }
544            if set_time(num as i64, num2, n3, tm) == 0 {
545                if pos < date.len()
546                    && date[pos] == b'.'
547                    && pos + 1 < date.len()
548                    && date[pos + 1].is_ascii_digit()
549                    && is_date_known(tm)
550                {
551                    let (_, rel) = parse_long_prefix(&date[pos + 1..]);
552                    pos += 1 + rel;
553                }
554            } else {
555                return 0;
556            }
557        }
558        b'-' | b'/' | b'.' => {
559            let now = if now_in == 0 { get_time_sec() } else { now_in };
560            let mut now_tm = empty_tm();
561            let refuse_future: Option<&tm> = if compat::gmtime(now as time_t, &mut now_tm) {
562                Some(&now_tm)
563            } else {
564                None
565            };
566
567            let y = num as i32;
568            let m = num2 as i32;
569            let d = if num3 < 0 { 0 } else { num3 as i32 };
570
571            if num > 70 {
572                if set_date(y, m, d, None, now, tm) == 0 {
573                    return pos;
574                }
575                if set_date(y, d, m, None, now, tm) == 0 {
576                    return pos;
577                }
578            }
579            if c != b'.' && set_date(d, y, m, refuse_future, now, tm) == 0 {
580                return pos;
581            }
582            if set_date(d, m, y, refuse_future, now, tm) == 0 {
583                return pos;
584            }
585            if c == b'.' && set_date(d, y, m, refuse_future, now, tm) == 0 {
586                return pos;
587            }
588            return 0;
589        }
590        _ => return 0,
591    }
592    pos
593}
594
595fn match_alpha(date: &[u8], tm: &mut tm, offset: &mut i32) -> usize {
596    for (i, name) in MONTH_NAMES.iter().enumerate() {
597        let m = match_string(date, name);
598        if m >= 3 {
599            tm.tm_mon = i as i32;
600            return m;
601        }
602    }
603
604    for (i, name) in WEEKDAY_NAMES.iter().enumerate() {
605        let m = match_string(date, name);
606        if m >= 3 {
607            tm.tm_wday = i as i32;
608            return m;
609        }
610    }
611
612    for tz in TIMEZONE_NAMES {
613        let m = match_string(date, tz.name);
614        if m >= 3 || m == tz.name.len() {
615            let off = tz.offset_hours + tz.dst;
616            if *offset == -1 {
617                *offset = 60 * off;
618            }
619            return m;
620        }
621    }
622
623    if match_string(date, "PM") == 2 {
624        tm.tm_hour = (tm.tm_hour % 12) + 12;
625        return 2;
626    }
627
628    if match_string(date, "AM") == 2 {
629        tm.tm_hour %= 12;
630        return 2;
631    }
632
633    if date.first() == Some(&b'T')
634        && date.get(1).is_some_and(|b| b.is_ascii_digit())
635        && tm.tm_hour == -1
636    {
637        tm.tm_min = 0;
638        tm.tm_sec = 0;
639        return 1;
640    }
641
642    skip_alpha(date)
643}
644
645fn match_digit(date: &[u8], tm: &mut tm, offset: &mut i32, tm_gmt: &mut i32) -> usize {
646    let (num, n) = parse_timestamp_prefix(date);
647    if n == 0 {
648        return 0;
649    }
650    let end = n;
651
652    if num >= 100_000_000 && nodate(tm) {
653        if compat::gmtime(num as time_t, tm) {
654            *tm_gmt = 1;
655            return end;
656        }
657    }
658
659    if let Some(&sep) = date.get(end) {
660        if matches!(sep, b':' | b'.' | b'/' | b'-')
661            && date.get(end + 1).is_some_and(|b| b.is_ascii_digit())
662        {
663            let m = match_multi_number(num, date, end, tm, 0);
664            if m != 0 {
665                return m;
666            }
667        }
668    }
669
670    let mut n_digits = 0usize;
671    loop {
672        n_digits += 1;
673        if n_digits >= date.len() || !date[n_digits].is_ascii_digit() {
674            break;
675        }
676    }
677
678    if n_digits == 8 || n_digits == 6 {
679        let num1 = (num / 10000) as i32;
680        let num2 = ((num % 10000) / 100) as i32;
681        let num3 = (num % 100) as i32;
682        if n_digits == 8 {
683            let _ = set_date(num1, num2, num3, None, get_time_sec(), tm);
684        } else if set_time(num1 as i64, num2 as i64, num3 as i64, tm) == 0
685            && date.get(end) == Some(&b'.')
686            && date.get(end + 1).is_some_and(|b| b.is_ascii_digit())
687        {
688            let (_, rel) = parse_uint_suffix(&date[end + 1..]);
689            return end + 1 + rel;
690        }
691        return end;
692    }
693
694    if maybeiso8601(tm) {
695        let mut num1 = num as u32;
696        let mut num2: u32 = 0;
697        if n_digits == 4 {
698            num1 = (num / 100) as u32;
699            num2 = (num % 100) as u32;
700        }
701        if (n_digits == 4 || n_digits == 2)
702            && !nodate(tm)
703            && set_time(num1 as i64, num2 as i64, 0, tm) == 0
704        {
705            return n_digits;
706        }
707        tm.tm_min = -1;
708        tm.tm_sec = -1;
709    }
710
711    if n_digits == 4 {
712        if num <= 1400 && *offset == -1 {
713            let minutes = (num % 100) as u32;
714            let hours = (num / 100) as u32;
715            *offset = (hours * 60 + minutes) as i32;
716        } else if num > 1900 && num < 2100 {
717            tm.tm_year = (num as i32) - 1900;
718        }
719        return n_digits;
720    }
721
722    if n_digits > 2 {
723        return n_digits;
724    }
725
726    if num > 0 && num < 32 && tm.tm_mday < 0 {
727        tm.tm_mday = num as i32;
728        return n_digits;
729    }
730
731    if n_digits == 2 && tm.tm_year < 0 {
732        if num < 10 && tm.tm_mday >= 0 {
733            tm.tm_year = (num as i32) + 100;
734            return n_digits;
735        }
736        if num >= 70 {
737            tm.tm_year = num as i32;
738            return n_digits;
739        }
740    }
741
742    if num > 0 && num < 13 && tm.tm_mon < 0 {
743        tm.tm_mon = (num as i32) - 1;
744    }
745
746    n_digits
747}