Skip to main content

freeswitch_log_parser/
stamp.rs

1//! The `YYYY-MM-DD-HH-MM-SS` stamp form, which sorts lexicographically and so
2//! lets a caller order rotated files and window entries without a date library.
3
4/// The rotation stamp encoded in a `freeswitch.log.*` filename, or `None` for the
5/// active log and for any name that does not carry one.
6///
7/// Only the stamp is read; whatever logrotate appends after it (a sequence
8/// number, a compression suffix) is ignored.
9pub fn log_rotation_stamp(filename: &str) -> Option<&str> {
10    const STAMP_LEN: usize = 19;
11    let rest = filename.strip_prefix("freeswitch.log.")?;
12    let candidate = rest.get(..STAMP_LEN)?;
13    candidate
14        .bytes()
15        .enumerate()
16        .all(|(i, b)| match i {
17            4 | 7 | 10 | 13 | 16 => b == b'-',
18            _ => b.is_ascii_digit(),
19        })
20        .then_some(candidate)
21}
22
23/// Rewrite a log entry's `YYYY-MM-DD HH:MM:SS.ffffff` timestamp into the stamp
24/// form, dropping the sub-second part so it compares against a filename stamp.
25///
26/// Input too short to hold a full timestamp is normalized as best it can be
27/// rather than rejected, so a partially parsed entry still windows sanely.
28pub fn normalize_entry_timestamp(ts: &str) -> String {
29    const TS_LEN: usize = 19;
30    // `get` rather than a byte slice: a caller can hand this any string, and a
31    // multibyte codepoint straddling either bound would panic on `&ts[..n]`.
32    if let (Some(date), Some(time)) = (ts.get(..10), ts.get(11..TS_LEN)) {
33        return format!("{date}-{}", time.replace(':', "-"));
34    }
35    let mut s = ts.replace(['T', ':', ' '], "-");
36    while s.ends_with('-') {
37        s.pop();
38    }
39    s
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn stamp_from_rotated_name() {
48        assert_eq!(
49            log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07.1.xz"),
50            Some("2026-03-08-16-52-07"),
51        );
52        assert_eq!(
53            log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07"),
54            Some("2026-03-08-16-52-07"),
55        );
56    }
57
58    #[test]
59    fn no_stamp_on_active_log_or_foreign_names() {
60        assert_eq!(log_rotation_stamp("freeswitch.log"), None);
61        assert_eq!(log_rotation_stamp("freeswitch.log.1.xz"), None);
62        assert_eq!(log_rotation_stamp("freeswitch.log.not-a-date-here!!"), None);
63        assert_eq!(log_rotation_stamp("other.log.2026-03-08-16-52-07"), None);
64    }
65
66    #[test]
67    fn stamp_rejects_multibyte_boundary() {
68        // `get(..19)` returns None rather than panicking mid-codepoint.
69        assert_eq!(
70            log_rotation_stamp("freeswitch.log.2026-03-08-16-52-é"),
71            None
72        );
73    }
74
75    #[test]
76    fn entry_timestamp_normalized() {
77        assert_eq!(
78            normalize_entry_timestamp("2026-03-08 16:52:07.123456"),
79            "2026-03-08-16-52-07",
80        );
81        assert_eq!(
82            normalize_entry_timestamp("2026-03-08 16:52:07"),
83            "2026-03-08-16-52-07",
84        );
85    }
86
87    #[test]
88    fn short_entry_timestamp_still_normalizes() {
89        assert_eq!(normalize_entry_timestamp("2026-03-08"), "2026-03-08");
90        assert_eq!(normalize_entry_timestamp("2026-03-08 "), "2026-03-08");
91        assert_eq!(normalize_entry_timestamp(""), "");
92    }
93
94    #[test]
95    fn multibyte_entry_timestamp_does_not_panic() {
96        // A codepoint straddling either slice bound would panic on a byte slice.
97        assert_eq!(
98            normalize_entry_timestamp("2026-03-08é16:52:07"),
99            "2026-03-08é16-52-07"
100        );
101        assert_eq!(
102            normalize_entry_timestamp("2026-03-0é 16:52:07"),
103            "2026-03-0é-16-52-07"
104        );
105        assert_eq!(normalize_entry_timestamp("ééééééééé"), "ééééééééé");
106    }
107
108    #[test]
109    fn normalized_forms_compare_lexicographically() {
110        let entry = normalize_entry_timestamp("2026-03-08 16:52:07.123456");
111        let earlier = log_rotation_stamp("freeswitch.log.2026-03-08-00-00-00.1.xz").unwrap();
112        let later = log_rotation_stamp("freeswitch.log.2026-03-09-00-00-00.1.xz").unwrap();
113        assert!(entry.as_str() > earlier);
114        assert!(entry.as_str() < later);
115    }
116}