freeswitch_log_parser/
stamp.rs1use crate::mask::Mask;
5
6const STAMP_LEN: usize = 19;
8
9const STAMP_MASK: Mask = Mask {
10 len: STAMP_LEN,
11 separators: &[(4, b'-'), (7, b'-'), (10, b'-'), (13, b'-'), (16, b'-')],
12 filler: u8::is_ascii_digit,
13};
14
15pub fn log_rotation_stamp(filename: &str) -> Option<&str> {
21 let rest = filename.strip_prefix("freeswitch.log.")?;
22 let candidate = rest.get(..STAMP_LEN)?;
23 STAMP_MASK
24 .matches_at(candidate.as_bytes(), 0)
25 .then_some(candidate)
26}
27
28pub fn normalize_entry_timestamp(ts: &str) -> String {
34 if let (Some(date), Some(time)) = (ts.get(..10), ts.get(11..STAMP_LEN)) {
37 return format!("{date}-{}", time.replace(':', "-"));
38 }
39 loose_stamp(ts)
40}
41
42fn loose_stamp(input: &str) -> String {
45 let mut s = input.replace(['T', ':', ' '], "-");
46 while s.ends_with('-') {
47 s.pop();
48 }
49 s
50}
51
52fn pad_stamp(s: &str, defaults: [&str; 6]) -> String {
55 let parts: Vec<&str> = s.split('-').collect();
56 defaults
57 .iter()
58 .enumerate()
59 .map(|(i, default)| match parts.get(i) {
60 Some(p) if !p.is_empty() => *p,
61 _ => default,
62 })
63 .collect::<Vec<_>>()
64 .join("-")
65}
66
67pub fn stamp_lower_bound(input: &str) -> String {
70 pad_stamp(&loose_stamp(input), ["0000", "01", "01", "00", "00", "00"])
71}
72
73pub fn stamp_upper_bound(input: &str) -> String {
79 pad_stamp(&loose_stamp(input), ["9999", "12", "31", "23", "59", "59"])
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn stamp_from_rotated_name() {
88 assert_eq!(
89 log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07.1.xz"),
90 Some("2026-03-08-16-52-07"),
91 );
92 assert_eq!(
93 log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07"),
94 Some("2026-03-08-16-52-07"),
95 );
96 }
97
98 #[test]
99 fn no_stamp_on_active_log_or_foreign_names() {
100 assert_eq!(log_rotation_stamp("freeswitch.log"), None);
101 assert_eq!(log_rotation_stamp("freeswitch.log.1.xz"), None);
102 assert_eq!(log_rotation_stamp("freeswitch.log.not-a-date-here!!"), None);
103 assert_eq!(log_rotation_stamp("other.log.2026-03-08-16-52-07"), None);
104 }
105
106 #[test]
107 fn stamp_rejects_multibyte_boundary() {
108 assert_eq!(
110 log_rotation_stamp("freeswitch.log.2026-03-08-16-52-é"),
111 None
112 );
113 }
114
115 #[test]
116 fn entry_timestamp_normalized() {
117 assert_eq!(
118 normalize_entry_timestamp("2026-03-08 16:52:07.123456"),
119 "2026-03-08-16-52-07",
120 );
121 assert_eq!(
122 normalize_entry_timestamp("2026-03-08 16:52:07"),
123 "2026-03-08-16-52-07",
124 );
125 }
126
127 #[test]
128 fn short_entry_timestamp_still_normalizes() {
129 assert_eq!(normalize_entry_timestamp("2026-03-08"), "2026-03-08");
130 assert_eq!(normalize_entry_timestamp("2026-03-08 "), "2026-03-08");
131 assert_eq!(normalize_entry_timestamp(""), "");
132 }
133
134 #[test]
135 fn multibyte_entry_timestamp_does_not_panic() {
136 assert_eq!(
138 normalize_entry_timestamp("2026-03-08é16:52:07"),
139 "2026-03-08é16-52-07"
140 );
141 assert_eq!(
142 normalize_entry_timestamp("2026-03-0é 16:52:07"),
143 "2026-03-0é-16-52-07"
144 );
145 assert_eq!(normalize_entry_timestamp("ééééééééé"), "ééééééééé");
146 }
147
148 #[test]
149 fn bounds_fill_a_partial_date_from_both_ends() {
150 for (input, lower, upper) in [
151 ("2026", "2026-01-01-00-00-00", "2026-12-31-23-59-59"),
152 ("2026-03", "2026-03-01-00-00-00", "2026-03-31-23-59-59"),
153 ("2026-03-08", "2026-03-08-00-00-00", "2026-03-08-23-59-59"),
154 (
155 "2026-03-08T15:48",
156 "2026-03-08-15-48-00",
157 "2026-03-08-15-48-59",
158 ),
159 (
160 "2026-03-08 15:48:07",
161 "2026-03-08-15-48-07",
162 "2026-03-08-15-48-07",
163 ),
164 ] {
165 assert_eq!(stamp_lower_bound(input), lower, "lower for {input}");
166 assert_eq!(stamp_upper_bound(input), upper, "upper for {input}");
167 }
168 }
169
170 #[test]
171 fn an_empty_bound_spans_everything() {
172 assert!(stamp_lower_bound("") < stamp_upper_bound(""));
173 assert!(stamp_lower_bound("") < normalize_entry_timestamp("2026-03-08 16:52:07.123456"));
174 }
175
176 #[test]
177 fn normalized_forms_compare_lexicographically() {
178 let entry = normalize_entry_timestamp("2026-03-08 16:52:07.123456");
179 let earlier = log_rotation_stamp("freeswitch.log.2026-03-08-00-00-00.1.xz").unwrap();
180 let later = log_rotation_stamp("freeswitch.log.2026-03-09-00-00-00.1.xz").unwrap();
181 assert!(entry.as_str() > earlier);
182 assert!(entry.as_str() < later);
183 }
184}