Skip to main content

aven_core/
time_validation.rs

1use anyhow::{Result, bail};
2
3use crate::queue::unix_seconds;
4
5pub fn validate_due_on_value(value: &str) -> Result<()> {
6    if value.is_empty() {
7        return Ok(());
8    }
9    if !is_iso_date(value) || chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_err() {
10        bail!("error invalid-due value={value} hint=\"use YYYY-MM-DD or empty\"");
11    }
12    Ok(())
13}
14
15pub fn validate_available_at_value(value: &str) -> Result<()> {
16    if value.is_empty() {
17        return Ok(());
18    }
19    if !is_canonical_utc_timestamp(value)
20        || timestamp_components(value).is_none_or(|(year, month, day, hour, minute, second)| {
21            month == 0
22                || month > 12
23                || day == 0
24                || day > days_in_month(year, month)
25                || hour > 23
26                || minute > 59
27                || second > 59
28        })
29        || unix_seconds(value).is_none()
30    {
31        bail!(
32            "error invalid-available-at value={value} hint=\"use YYYY-MM-DDTHH:MM:SSZ or empty\""
33        );
34    }
35    Ok(())
36}
37
38fn is_iso_date(value: &str) -> bool {
39    let bytes = value.as_bytes();
40    bytes.len() == 10
41        && bytes[4] == b'-'
42        && bytes[7] == b'-'
43        && bytes
44            .iter()
45            .enumerate()
46            .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit())
47}
48
49fn is_canonical_utc_timestamp(value: &str) -> bool {
50    let Some(value) = value.strip_suffix('Z') else {
51        return false;
52    };
53    if value.len() != 19 {
54        return false;
55    }
56    let Some((date, time)) = value.split_once('T') else {
57        return false;
58    };
59    is_iso_date(date) && is_hms(time)
60}
61
62fn is_hms(value: &str) -> bool {
63    let bytes = value.as_bytes();
64    bytes.len() == 8
65        && bytes[2] == b':'
66        && bytes[5] == b':'
67        && bytes
68            .iter()
69            .enumerate()
70            .all(|(index, byte)| matches!(index, 2 | 5) || byte.is_ascii_digit())
71}
72
73fn timestamp_components(value: &str) -> Option<(i64, u32, u32, u32, u32, u32)> {
74    let (date, time) = value.trim_end_matches('Z').split_once('T')?;
75    let mut date = date.split('-');
76    let year = date.next()?.parse().ok()?;
77    let month = date.next()?.parse().ok()?;
78    let day = date.next()?.parse().ok()?;
79    let mut time = time.split(':');
80    let hour = time.next()?.parse().ok()?;
81    let minute = time.next()?.parse().ok()?;
82    let second = time.next()?.parse().ok()?;
83    Some((year, month, day, hour, minute, second))
84}
85
86fn days_in_month(year: i64, month: u32) -> u32 {
87    match month {
88        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
89        4 | 6 | 9 | 11 => 30,
90        2 if leap_year(year) => 29,
91        2 => 28,
92        _ => 0,
93    }
94}
95
96fn leap_year(year: i64) -> bool {
97    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
98}