use jiff::Timestamp;
use std::{fmt::Display, ops::RangeInclusive};
use x509_parser::{prelude::X509Certificate, time::ASN1Time};
use crate::cached_string_repr::CachedStringRepr;
#[derive(Debug, Clone)]
pub struct Validity {
not_before: Timestamp,
not_after: Timestamp,
rendered: CachedStringRepr,
}
impl Validity {
fn new(value: &x509_parser::certificate::Validity) -> Result<Self, jiff::Error> {
let not_before = asn1_to_timestamp(value.not_before)?;
let not_after = asn1_to_timestamp(value.not_after)?;
Ok(Self {
not_before,
not_after,
rendered: Default::default(),
})
}
pub fn not_before_as_timestamp(&self) -> Timestamp {
self.not_before
}
pub fn not_after_as_timestamp(&self) -> Timestamp {
self.not_after
}
pub fn as_display_str(&self) -> &str {
self.rendered
.get_or_init(|| format!("{}..{}", self.not_before, self.not_after))
}
pub fn range(&self) -> RangeInclusive<Timestamp> {
RangeInclusive::new(self.not_before, self.not_after)
}
}
impl Display for Validity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_display_str().fmt(f)
}
}
fn asn1_to_timestamp(timestamp: ASN1Time) -> Result<Timestamp, jiff::Error> {
let secs = timestamp.timestamp();
Timestamp::from_second(secs)
}
impl<'a> TryFrom<&'a X509Certificate<'a>> for Validity {
type Error = jiff::Error;
fn try_from(cert: &'a X509Certificate<'a>) -> Result<Self, Self::Error> {
Self::new(cert.validity())
}
}
impl valuable::Valuable for Validity {
fn as_value(&self) -> valuable::Value<'_> {
valuable::Value::String(self.as_display_str())
}
fn visit(&self, visit: &mut dyn valuable::Visit) {
visit.visit_value(self.as_value());
}
}
#[cfg(test)]
mod tests {
use jiff::Timestamp;
use proptest::prelude::*;
use super::*;
fn make_validity(not_before_secs: i64, not_after_secs: i64) -> Validity {
Validity {
not_before: Timestamp::from_second(not_before_secs).unwrap(),
not_after: Timestamp::from_second(not_after_secs).unwrap(),
rendered: Default::default(),
}
}
#[test]
fn test_fixture() {
let not_before_secs = 1_000_000_000i64; let not_after_secs = 2_000_000_000i64;
let v = make_validity(not_before_secs, not_after_secs);
assert_eq!(
v.not_before_as_timestamp(),
Timestamp::from_second(not_before_secs).unwrap()
);
assert_eq!(
v.not_after_as_timestamp(),
Timestamp::from_second(not_after_secs).unwrap()
);
assert_eq!(
v.as_display_str(),
"2001-09-09T01:46:40Z..2033-05-18T03:33:20Z"
);
}
#[test]
fn test_timestamp_out_of_range() {
let jiff_beyond_max = Timestamp::MAX.as_second() + 1;
assert!(Timestamp::from_second(jiff_beyond_max).is_err());
let asn1_beyond_max = ASN1Time::from_timestamp(jiff_beyond_max).unwrap();
assert!(asn1_to_timestamp(asn1_beyond_max).is_err());
let jiff_before_min = Timestamp::MIN.as_second() - 1;
assert!(Timestamp::from_second(jiff_before_min).is_err());
let asn1_before_min = ASN1Time::from_timestamp(jiff_before_min).unwrap();
assert!(asn1_to_timestamp(asn1_before_min).is_err());
}
proptest! {
#[test]
fn prop_timestamp_in_validity_range(
not_before in 0i64..=3_000_000_000i64,
not_after in 0i64..=3_000_000_000i64,
ts_secs in 0i64..=4_000_000_000i64,
) {
let v = make_validity(not_before, not_after);
let ts = Timestamp::from_second(ts_secs).unwrap();
prop_assert_eq!(v.range().contains(&ts), not_before <= ts_secs && ts_secs <= not_after);
}
}
}