aven_core/
time_validation.rs1use 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}