use std::time::SystemTime;
use prost_types::Timestamp;
const NANOS_PER_MILLI: i32 = 1_000_000;
pub(super) fn parse_generated_at(value: &str) -> Result<SystemTime, String> {
let invalid = || format!("generatedAt {value} is not an RFC 3339 time");
if !is_strict_rfc_3339(value.as_bytes()) {
return Err(invalid());
}
let timestamp = value
.to_ascii_uppercase()
.parse::<Timestamp>()
.map_err(|_| invalid())?;
SystemTime::try_from(truncate_timestamp_to_millis(timestamp)).map_err(|_| invalid())
}
pub(super) fn truncate_to_millis(time: SystemTime) -> SystemTime {
let truncated = truncate_timestamp_to_millis(Timestamp::from(time));
SystemTime::try_from(truncated).expect("truncating a SystemTime keeps it in range")
}
pub(super) fn truncate_timestamp_to_millis(mut timestamp: Timestamp) -> Timestamp {
timestamp.normalize();
timestamp.nanos -= timestamp.nanos.rem_euclid(NANOS_PER_MILLI);
timestamp
}
fn is_strict_rfc_3339(bytes: &[u8]) -> bool {
let is_digits = |range: std::ops::Range<usize>| {
bytes
.get(range)
.is_some_and(|digits| digits.iter().all(u8::is_ascii_digit))
};
let has = |index: usize, expected: &[u8]| {
bytes.get(index).is_some_and(|byte| expected.contains(byte))
};
if !(is_digits(0..4)
&& has(4, b"-")
&& is_digits(5..7)
&& has(7, b"-")
&& is_digits(8..10)
&& has(10, b"Tt")
&& is_digits(11..13)
&& has(13, b":")
&& is_digits(14..16)
&& has(16, b":")
&& is_digits(17..19))
{
return false;
}
let mut index = 19;
if has(index, b".") {
let fraction_start = index + 1;
index = fraction_start;
while bytes.get(index).is_some_and(u8::is_ascii_digit) {
index += 1;
}
if index == fraction_start {
return false;
}
}
let offset = &bytes[index..];
let is_offset_valid = match offset {
[b'Z' | b'z'] => true,
[b'+' | b'-', ..] => {
offset.len() == 6
&& offset[1..3].iter().all(u8::is_ascii_digit)
&& offset[3] == b':'
&& offset[4..6].iter().all(u8::is_ascii_digit)
&& number(&offset[1..3]) <= 23
&& number(&offset[4..6]) <= 59
}
_ => false,
};
let year = number(&bytes[0..4]);
let month = number(&bytes[5..7]);
let day = number(&bytes[8..10]);
is_offset_valid
&& (1..=12).contains(&month)
&& day >= 1
&& day <= days_in_month(year, month)
&& number(&bytes[11..13]) <= 23
&& number(&bytes[14..16]) <= 59
&& number(&bytes[17..19]) <= 59
}
fn number(digits: &[u8]) -> u32 {
digits
.iter()
.fold(0, |value, digit| value * 10 + u32::from(digit - b'0'))
}
fn days_in_month(year: u32, month: u32) -> u32 {
let is_leap_year =
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
match month {
2 if is_leap_year => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}