use crate::mask::Mask;
const STAMP_LEN: usize = 19;
const STAMP_MASK: Mask = Mask {
len: STAMP_LEN,
separators: &[(4, b'-'), (7, b'-'), (10, b'-'), (13, b'-'), (16, b'-')],
filler: u8::is_ascii_digit,
};
pub fn log_rotation_stamp(filename: &str) -> Option<&str> {
let rest = filename.strip_prefix("freeswitch.log.")?;
let candidate = rest.get(..STAMP_LEN)?;
STAMP_MASK
.matches_at(candidate.as_bytes(), 0)
.then_some(candidate)
}
pub fn normalize_entry_timestamp(ts: &str) -> String {
if let (Some(date), Some(time)) = (ts.get(..10), ts.get(11..STAMP_LEN)) {
return format!("{date}-{}", time.replace(':', "-"));
}
loose_stamp(ts)
}
fn loose_stamp(input: &str) -> String {
let mut s = input.replace(['T', ':', ' '], "-");
while s.ends_with('-') {
s.pop();
}
s
}
fn pad_stamp(s: &str, defaults: [&str; 6]) -> String {
let parts: Vec<&str> = s.split('-').collect();
defaults
.iter()
.enumerate()
.map(|(i, default)| match parts.get(i) {
Some(p) if !p.is_empty() => *p,
_ => default,
})
.collect::<Vec<_>>()
.join("-")
}
pub fn stamp_lower_bound(input: &str) -> String {
pad_stamp(&loose_stamp(input), ["0000", "01", "01", "00", "00", "00"])
}
pub fn stamp_upper_bound(input: &str) -> String {
pad_stamp(&loose_stamp(input), ["9999", "12", "31", "23", "59", "59"])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stamp_from_rotated_name() {
assert_eq!(
log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07.1.xz"),
Some("2026-03-08-16-52-07"),
);
assert_eq!(
log_rotation_stamp("freeswitch.log.2026-03-08-16-52-07"),
Some("2026-03-08-16-52-07"),
);
}
#[test]
fn no_stamp_on_active_log_or_foreign_names() {
assert_eq!(log_rotation_stamp("freeswitch.log"), None);
assert_eq!(log_rotation_stamp("freeswitch.log.1.xz"), None);
assert_eq!(log_rotation_stamp("freeswitch.log.not-a-date-here!!"), None);
assert_eq!(log_rotation_stamp("other.log.2026-03-08-16-52-07"), None);
}
#[test]
fn stamp_rejects_multibyte_boundary() {
assert_eq!(
log_rotation_stamp("freeswitch.log.2026-03-08-16-52-é"),
None
);
}
#[test]
fn entry_timestamp_normalized() {
assert_eq!(
normalize_entry_timestamp("2026-03-08 16:52:07.123456"),
"2026-03-08-16-52-07",
);
assert_eq!(
normalize_entry_timestamp("2026-03-08 16:52:07"),
"2026-03-08-16-52-07",
);
}
#[test]
fn short_entry_timestamp_still_normalizes() {
assert_eq!(normalize_entry_timestamp("2026-03-08"), "2026-03-08");
assert_eq!(normalize_entry_timestamp("2026-03-08 "), "2026-03-08");
assert_eq!(normalize_entry_timestamp(""), "");
}
#[test]
fn multibyte_entry_timestamp_does_not_panic() {
assert_eq!(
normalize_entry_timestamp("2026-03-08é16:52:07"),
"2026-03-08é16-52-07"
);
assert_eq!(
normalize_entry_timestamp("2026-03-0é 16:52:07"),
"2026-03-0é-16-52-07"
);
assert_eq!(normalize_entry_timestamp("ééééééééé"), "ééééééééé");
}
#[test]
fn bounds_fill_a_partial_date_from_both_ends() {
for (input, lower, upper) in [
("2026", "2026-01-01-00-00-00", "2026-12-31-23-59-59"),
("2026-03", "2026-03-01-00-00-00", "2026-03-31-23-59-59"),
("2026-03-08", "2026-03-08-00-00-00", "2026-03-08-23-59-59"),
(
"2026-03-08T15:48",
"2026-03-08-15-48-00",
"2026-03-08-15-48-59",
),
(
"2026-03-08 15:48:07",
"2026-03-08-15-48-07",
"2026-03-08-15-48-07",
),
] {
assert_eq!(stamp_lower_bound(input), lower, "lower for {input}");
assert_eq!(stamp_upper_bound(input), upper, "upper for {input}");
}
}
#[test]
fn an_empty_bound_spans_everything() {
assert!(stamp_lower_bound("") < stamp_upper_bound(""));
assert!(stamp_lower_bound("") < normalize_entry_timestamp("2026-03-08 16:52:07.123456"));
}
#[test]
fn normalized_forms_compare_lexicographically() {
let entry = normalize_entry_timestamp("2026-03-08 16:52:07.123456");
let earlier = log_rotation_stamp("freeswitch.log.2026-03-08-00-00-00.1.xz").unwrap();
let later = log_rotation_stamp("freeswitch.log.2026-03-09-00-00-00.1.xz").unwrap();
assert!(entry.as_str() > earlier);
assert!(entry.as_str() < later);
}
}