use crate::error::Error;
pub(crate) const MAX_HOLDFOR_SECONDS: u64 = 999_999_999;
pub(crate) fn validate_hold_for_seconds(hold_for: u64) -> Result<(), Error> {
if hold_for == 0 {
return Err(Error::Protocol(
"HOLDFOR must be a positive decimal interval \
(RFC 4865 Section 5: hold-for-seconds = 1*9DIGIT)"
.into(),
));
}
if hold_for > MAX_HOLDFOR_SECONDS {
return Err(Error::Protocol(format!(
"HOLDFOR exceeds the RFC 4865 Section 5 9-digit limit: {hold_for}"
)));
}
Ok(())
}
pub(crate) fn validate_hold_until_datetime(hold_until: &str) -> Result<(), Error> {
if parse_rfc3339_to_utc_key(hold_until).is_none() {
return Err(Error::Protocol(format!(
"HOLDUNTIL must be a valid RFC 3339 date-time: {hold_until} \
(RFC 4865 Section 5 / RFC 3339 Section 5.6)"
)));
}
Ok(())
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
pub(crate) fn parse_rfc3339_to_utc_key(s: &str) -> Option<(i64, u32)> {
if s.len() < 20 {
return None;
}
let b = s.as_bytes();
let year: i32 = s.get(..4)?.parse().ok()?;
if b.get(4)? != &b'-' {
return None;
}
let month: u32 = s.get(5..7)?.parse().ok()?;
if !(1..=12).contains(&month) || b.get(7)? != &b'-' {
return None;
}
let day: u32 = s.get(8..10)?.parse().ok()?;
if day == 0 || day > days_in_month(year, month) {
return None;
}
if !matches!(b.get(10), Some(b'T' | b't')) {
return None;
}
let hour: i64 = s.get(11..13)?.parse().ok()?;
if !(0..=23).contains(&hour) || b.get(13)? != &b':' {
return None;
}
let minute: i64 = s.get(14..16)?.parse().ok()?;
if !(0..=59).contains(&minute) || b.get(16)? != &b':' {
return None;
}
let second: i64 = s.get(17..19)?.parse().ok()?;
if !(0..=60).contains(&second) {
return None;
}
let mut pos = 19;
let fractional_nanos = if b.get(pos) == Some(&b'.') {
pos += 1;
let frac_start = pos;
while matches!(b.get(pos), Some(d) if d.is_ascii_digit()) {
pos += 1;
}
if pos == frac_start {
return None;
}
let frac = &b[frac_start..pos];
let mut nanos_digits = [b'0'; 9];
for (idx, digit) in frac.iter().take(9).enumerate() {
nanos_digits[idx] = *digit;
}
std::str::from_utf8(&nanos_digits).ok()?.parse().ok()?
} else {
0
};
let tz_rest = s.get(pos..)?;
let tz_offset_secs: i64 = if tz_rest == "Z" || tz_rest == "z" {
0
} else if tz_rest.len() == 6 {
let sign = match tz_rest.as_bytes().first()? {
b'+' => 1i64,
b'-' => -1i64,
_ => return None,
};
let tz_h: i64 = tz_rest.get(1..3)?.parse().ok()?;
if !(0..=23).contains(&tz_h) || tz_rest.as_bytes().get(3)? != &b':' {
return None;
}
let tz_m: i64 = tz_rest.get(4..6)?.parse().ok()?;
if !(0..=59).contains(&tz_m) {
return None;
}
sign * (tz_h * 3600 + tz_m * 60)
} else {
return None;
};
let y = if month <= 2 {
i64::from(year) - 1
} else {
i64::from(year)
};
let m = if month <= 2 { month + 9 } else { month - 3 };
let era = (if y >= 0 { y } else { y - 399 }) / 400;
let yoe = (y - era * 400) as u64;
let doy = (153 * u64::from(m) + 2) / 5 + u64::from(day) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146_097 + doe as i64 - 719_468;
let utc_secs = days * 86400 + hour * 3600 + minute * 60 + second;
Some((utc_secs - tz_offset_secs, fractional_nanos))
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}