use alloc::string::String;
use alloc::vec::Vec;
use super::Error;
use crate::der::{Reader, encode_sequence, encode_string, tag};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Time {
repr: String,
}
impl Time {
pub fn utc(year: u64, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Time {
let mut repr = String::with_capacity(13);
push2(&mut repr, (year % 100) as u8);
push2(&mut repr, month);
push2(&mut repr, day);
push2(&mut repr, hour);
push2(&mut repr, minute);
push2(&mut repr, second);
repr.push('Z');
Time { repr }
}
pub fn from_unix(secs: u64) -> Time {
let days = (secs / 86_400) as i64;
let tod = secs % 86_400;
let (year, month, day) = civil_from_days(days);
Time::utc(
year as u64,
month,
day,
(tod / 3600) as u8,
((tod % 3600) / 60) as u8,
(tod % 60) as u8,
)
}
pub fn to_unix(&self) -> u64 {
self.to_unix_checked().unwrap_or(0)
}
pub fn to_unix_checked(&self) -> Option<u64> {
let (y, m, d, hh, mm, ss) = self.components()?;
let days = days_from_civil(y as i64, m, d);
if days < 0 {
return None;
}
Some((days as u64) * 86_400 + (hh as u64) * 3600 + (mm as u64) * 60 + ss as u64)
}
pub fn as_str(&self) -> &str {
&self.repr
}
pub(crate) fn from_repr(s: &str) -> Time {
Time {
repr: String::from(s),
}
}
pub(crate) fn to_der(&self) -> Vec<u8> {
encode_string(tag::UTC_TIME, &self.repr)
}
pub(crate) fn to_generalized_time(&self) -> Vec<u8> {
let b = self.repr.as_bytes();
if b.len() == 15 {
return encode_string(tag::GENERALIZED_TIME, &self.repr);
}
if b.len() == 13
&& let Some(yy) = two(b, 0)
{
let prefix = if yy < 50 { "20" } else { "19" };
let mut s = String::with_capacity(15);
s.push_str(prefix);
s.push_str(core::str::from_utf8(b).unwrap_or(""));
return encode_string(tag::GENERALIZED_TIME, &s);
}
encode_string(tag::GENERALIZED_TIME, &self.repr)
}
pub(crate) fn to_der_choice(&self) -> Vec<u8> {
let b = self.repr.as_bytes();
if b.len() == 15 {
if let Some((y, _m, _d, _h, _mi, _s)) = self.components()
&& (1950..=2049).contains(&y)
{
let yy = y % 100;
let mut s = alloc::string::String::with_capacity(13);
s.push(((yy / 10) as u8 + b'0') as char);
s.push(((yy % 10) as u8 + b'0') as char);
s.push_str(core::str::from_utf8(&b[4..15]).unwrap_or(""));
return encode_string(tag::UTC_TIME, &s);
}
return encode_string(tag::GENERALIZED_TIME, &self.repr);
}
if b.len() == 13 {
return encode_string(tag::UTC_TIME, &self.repr);
}
encode_string(tag::UTC_TIME, &self.repr)
}
fn components(&self) -> Option<(u16, u8, u8, u8, u8, u8)> {
let b = self.repr.as_bytes();
if b.last() != Some(&b'Z') {
return None;
}
let (year, off) = match b.len() {
13 => {
let yy = two(b, 0)?;
let year = if yy < 50 {
2000 + yy as u16
} else {
1900 + yy as u16
};
(year, 2)
}
15 => {
let year = digit(b, 0)? as u16 * 1000
+ digit(b, 1)? as u16 * 100
+ digit(b, 2)? as u16 * 10
+ digit(b, 3)? as u16;
(year, 4)
}
_ => return None,
};
let month = two(b, off)?;
let day = two(b, off + 2)?;
let hour = two(b, off + 4)?;
let minute = two(b, off + 6)?;
let second = two(b, off + 8)?;
if !(1..=12).contains(&month) {
return None;
}
if !(1..=days_in_month(year, month)).contains(&day) {
return None;
}
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some((year, month, day, hour, minute, second))
}
}
fn is_leap_year(year: u16) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
fn days_in_month(year: u16, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
fn digit(b: &[u8], i: usize) -> Option<u8> {
let c = *b.get(i)?;
c.is_ascii_digit().then(|| c - b'0')
}
fn two(b: &[u8], i: usize) -> Option<u8> {
Some(digit(b, i)? * 10 + digit(b, i + 1)?)
}
fn push2(s: &mut String, v: u8) {
s.push((b'0' + (v / 10) % 10) as char);
s.push((b'0' + v % 10) as char);
}
fn days_from_civil(year: i64, month: u8, day: u8) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let m = month as i64;
let d = day as i64;
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(days: i64) -> (i64, u8, u8) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
(if m <= 2 { y + 1 } else { y }, m, d)
}
#[derive(Clone, Debug)]
pub struct Validity {
pub not_before: Time,
pub not_after: Time,
}
impl Validity {
pub fn new(not_before: Time, not_after: Time) -> Self {
Validity {
not_before,
not_after,
}
}
pub fn accepts(&self, now: &Time) -> bool {
match (
self.not_before.components(),
self.not_after.components(),
now.components(),
) {
(Some(nb), Some(na), Some(n)) => nb <= n && n <= na,
_ => false,
}
}
pub(crate) fn to_der(&self) -> Vec<u8> {
encode_sequence(&[self.not_before.to_der(), self.not_after.to_der()].concat())
}
pub(crate) fn decode(reader: &mut Reader) -> Result<Self, Error> {
let mut seq = reader.read_sequence()?;
let not_before = read_time(&mut seq)?;
let not_after = read_time(&mut seq)?;
Ok(Validity {
not_before,
not_after,
})
}
}
pub(super) fn read_time(reader: &mut Reader) -> Result<Time, Error> {
let (t, value) = reader.read_any()?;
let expected_len = match t {
tag::UTC_TIME => 13,
tag::GENERALIZED_TIME => 15,
_ => return Err(Error::Malformed),
};
if value.len() != expected_len {
return Err(Error::Malformed);
}
let s = core::str::from_utf8(value).map_err(|_| Error::Malformed)?;
Ok(Time::from_repr(s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utc_formatting() {
assert_eq!(Time::utc(2024, 1, 31, 12, 0, 0).as_str(), "240131120000Z");
assert_eq!(Time::utc(2005, 3, 9, 8, 7, 6).as_str(), "050309080706Z");
}
#[test]
fn from_unix_epoch_and_known_dates() {
assert_eq!(Time::from_unix(0).as_str(), "700101000000Z");
assert_eq!(Time::from_unix(1_609_459_200).as_str(), "210101000000Z");
assert_eq!(Time::from_unix(1_709_251_199).as_str(), "240229235959Z");
}
#[test]
fn to_unix_roundtrips() {
for &s in &[0u64, 1_609_459_200, 1_709_251_199] {
assert_eq!(Time::from_unix(s).to_unix(), s, "roundtrip fails for {s}");
}
assert_eq!(Time::from_repr("20210101000000Z").to_unix(), 1_609_459_200);
assert_eq!(Time::from_repr("20500101000000Z").to_unix(), 2_524_608_000);
}
#[test]
fn validity_accepts_window() {
let v = Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
);
assert!(v.accepts(&Time::utc(2026, 5, 26, 12, 0, 0)));
assert!(v.accepts(&Time::utc(2024, 1, 1, 0, 0, 0))); assert!(v.accepts(&Time::utc(2034, 1, 1, 0, 0, 0))); assert!(!v.accepts(&Time::utc(2023, 12, 31, 23, 59, 59))); assert!(!v.accepts(&Time::utc(2034, 1, 1, 0, 0, 1))); }
#[test]
fn to_der_choice_picks_utctime_or_generalized() {
let utc = Time::utc(2024, 1, 1, 0, 0, 0).to_der_choice();
assert_eq!(utc[0], tag::UTC_TIME);
assert_eq!(utc[1] as usize, 13);
let g = Time::from_repr("20500101000000Z").to_der_choice();
assert_eq!(g[0], tag::GENERALIZED_TIME);
assert_eq!(g[1] as usize, 15);
let utc2 = Time::from_repr("20240101000000Z").to_der_choice();
assert_eq!(utc2[0], tag::UTC_TIME);
assert_eq!(utc2[1] as usize, 13);
}
#[test]
fn components_rejects_out_of_range_fields() {
assert!(Time::from_repr("240001000000Z").components().is_none());
assert!(Time::from_repr("241301000000Z").components().is_none());
assert!(Time::from_repr("240132000000Z").components().is_none());
assert!(Time::from_repr("240101250000Z").components().is_none());
assert!(Time::from_repr("240101006000Z").components().is_none());
assert!(Time::from_repr("240101000060Z").components().is_none());
assert!(Time::from_repr("240431000000Z").components().is_none());
assert!(Time::from_repr("240229000000Z").components().is_some()); assert!(Time::from_repr("250229000000Z").components().is_none()); assert!(Time::from_repr("20240229000000Z").components().is_some());
assert!(Time::from_repr("19000229000000Z").components().is_none());
assert!(Time::from_repr("20000229000000Z").components().is_some());
let v = Validity::new(
Time::from_repr("240101000000Z"),
Time::from_repr("241301000000Z"), );
assert!(!v.accepts(&Time::utc(2026, 5, 26, 12, 0, 0)));
}
#[test]
fn utctime_century_rule_and_generalized() {
let v = Validity::new(
Time::from_repr("240101000000Z"),
Time::from_repr("490101000000Z"),
);
assert!(v.accepts(&Time::utc(2030, 6, 1, 0, 0, 0)));
assert!(v.accepts(&Time::from_repr("20300601000000Z")));
assert!(!v.accepts(&Time::from_repr("20500101000000Z")));
}
#[test]
fn to_generalized_time_no_panic_on_non_digit_year() {
let g = Time::from_repr("--0101000000Z").to_generalized_time();
assert_eq!(g[0], tag::GENERALIZED_TIME);
assert_eq!(g[1] as usize, 13);
}
#[test]
fn read_time_binds_format_to_tag() {
let ok_utc = encode_string(tag::UTC_TIME, "240101000000Z");
let t = read_time(&mut Reader::new(&ok_utc)).unwrap();
assert_eq!(t.to_unix_checked(), Some(1_704_067_200)); let ok_gen = encode_string(tag::GENERALIZED_TIME, "20240101000000Z");
let t = read_time(&mut Reader::new(&ok_gen)).unwrap();
assert_eq!(t.to_unix_checked(), Some(1_704_067_200));
let bad_gen = encode_string(tag::GENERALIZED_TIME, "240101000000Z");
assert!(read_time(&mut Reader::new(&bad_gen)).is_err());
let bad_utc = encode_string(tag::UTC_TIME, "20240101000000Z");
assert!(read_time(&mut Reader::new(&bad_utc)).is_err());
let other = encode_string(tag::UTF8_STRING, "240101000000Z");
assert!(read_time(&mut Reader::new(&other)).is_err());
}
#[test]
fn validity_decode_rejects_tag_format_mismatch() {
let body = [
encode_string(tag::UTC_TIME, "240101000000Z"),
encode_string(tag::GENERALIZED_TIME, "340101000000Z"),
]
.concat();
let der = encode_sequence(&body);
assert!(Validity::decode(&mut Reader::new(&der)).is_err());
let body = [
encode_string(tag::UTC_TIME, "240101000000Z"),
encode_string(tag::GENERALIZED_TIME, "20340101000000Z"),
]
.concat();
let der = encode_sequence(&body);
let v = Validity::decode(&mut Reader::new(&der)).unwrap();
assert!(v.accepts(&Time::utc(2026, 5, 26, 12, 0, 0)));
}
#[test]
fn to_unix_checked_distinguishes_malformed_from_epoch() {
assert_eq!(Time::from_repr("garbage").to_unix_checked(), None);
assert_eq!(Time::from_repr("241301000000Z").to_unix_checked(), None); assert_eq!(Time::from_repr("garbage").to_unix(), 0);
let t = Time::from_repr("20210101000000Z");
assert_eq!(t.to_unix_checked(), Some(1_609_459_200));
assert_eq!(t.to_unix(), 1_609_459_200);
}
}