use crate::cbor::value::Value;
#[allow(unused_imports)]
use crate::nostd_prelude::*;
use super::eval::Verdict;
pub(super) fn match_tcbdate(reference: &Value, evidence: &Value) -> Verdict {
let ref_norm = normalize(reference);
let ev_norm = normalize(evidence);
match (ref_norm, ev_norm) {
(Some(Tcbdate::Instant(r)), Some(Tcbdate::Instant(e))) => bool_verdict(r == e),
(Some(Tcbdate::Period { lo, hi }), Some(Tcbdate::Instant(e))) => {
let lo_ok = lo.map(|l| e >= l).unwrap_or(true);
let hi_ok = hi.map(|h| e <= h).unwrap_or(true);
bool_verdict(lo_ok && hi_ok)
}
(
Some(Tcbdate::Period { lo: rlo, hi: rhi }),
Some(Tcbdate::Period { lo: elo, hi: ehi }),
) => bool_verdict(rlo == elo && rhi == ehi),
(Some(Tcbdate::Instant(_)), Some(Tcbdate::Period { .. })) => Verdict::Fail,
_ => bool_verdict(reference == evidence),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tcbdate {
Instant(i64),
Period { lo: Option<i64>, hi: Option<i64> },
}
fn normalize(v: &Value) -> Option<Tcbdate> {
if let Some(p) = normalize_period(v) {
return Some(p);
}
normalize_instant(v).map(Tcbdate::Instant)
}
fn normalize_instant(v: &Value) -> Option<i64> {
match v {
Value::Integer(n) => i64::try_from(*n).ok(),
Value::Float(f) => float_to_i64(*f),
Value::Tag(1, inner) => match inner.as_ref() {
Value::Integer(n) => i64::try_from(*n).ok(),
Value::Float(f) => float_to_i64(*f),
_ => None,
},
Value::Text(s) => rfc3339_to_epoch_seconds(s),
Value::Tag(0, inner) => match inner.as_ref() {
Value::Text(s) => rfc3339_to_epoch_seconds(s),
_ => None,
},
Value::Tag(1001, inner) => etime_basetime(inner.as_ref()),
_ => None,
}
}
fn etime_basetime(body: &Value) -> Option<i64> {
let entries = match body {
Value::Map(m) => m,
_ => return None,
};
for (k, v) in entries {
let key = match k {
Value::Integer(n) => *n,
_ => continue,
};
if key == 1 {
return match v {
Value::Integer(n) => i64::try_from(*n).ok(),
Value::Float(f) => float_to_i64(*f),
_ => None,
};
}
}
None
}
fn normalize_period(v: &Value) -> Option<Tcbdate> {
let body = match v {
Value::Tag(1003, inner) => inner.as_ref(),
_ => return None,
};
let items = match body {
Value::Array(a) => a,
_ => return None,
};
if items.len() < 2 || items.len() > 3 {
return None;
}
let start = bound(&items[0])?;
let end = bound(&items[1])?;
let duration = if items.len() == 3 {
match &items[2] {
Value::Integer(n) => Some(i64::try_from(*n).ok()?),
Value::Float(f) => Some(float_to_i64(*f)?),
Value::Tag(1002, inner) => match inner.as_ref() {
Value::Integer(n) => Some(i64::try_from(*n).ok()?),
Value::Float(f) => Some(float_to_i64(*f)?),
_ => None,
},
Value::Null => None,
_ => return None,
}
} else {
None
};
let lo = start;
let hi = match (end, start, duration) {
(Some(_), _, _) => end,
(None, Some(l), Some(d)) => Some(l.saturating_add(d)),
_ => None,
};
Some(Tcbdate::Period { lo, hi })
}
fn bound(v: &Value) -> Option<Option<i64>> {
if matches!(v, Value::Null) {
return Some(None);
}
normalize_instant(v).map(Some)
}
fn float_to_i64(f: f64) -> Option<i64> {
if f.is_nan() || f.is_infinite() || f < (i64::MIN as f64) || f > (i64::MAX as f64) {
None
} else {
Some(f as i64)
}
}
fn bool_verdict(b: bool) -> Verdict {
if b {
Verdict::Pass
} else {
Verdict::Fail
}
}
fn rfc3339_to_epoch_seconds(s: &str) -> Option<i64> {
let b = s.as_bytes();
if b.len() < 20 {
return None;
}
let year = parse_n_digits(&b[0..4])?;
if b[4] != b'-' {
return None;
}
let month = parse_n_digits(&b[5..7])?;
if b[7] != b'-' {
return None;
}
let day = parse_n_digits(&b[8..10])?;
if b[10] != b'T' && b[10] != b't' {
return None;
}
let hour = parse_n_digits(&b[11..13])?;
if b[13] != b':' {
return None;
}
let minute = parse_n_digits(&b[14..16])?;
if b[16] != b':' {
return None;
}
let second = parse_n_digits(&b[17..19])?;
if !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| hour > 23
|| minute > 59
|| second > 60
{
return None;
}
let second = if second == 60 { 59 } else { second };
let mut i = 19usize;
if i < b.len() && b[i] == b'.' {
i += 1;
let start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == start {
return None;
}
}
let offset_secs: i64 = if i < b.len() && (b[i] == b'Z' || b[i] == b'z') {
if i + 1 != b.len() {
return None;
}
0
} else if i + 6 == b.len() && (b[i] == b'+' || b[i] == b'-') {
let sign: i64 = if b[i] == b'+' { 1 } else { -1 };
let oh = parse_n_digits(&b[i + 1..i + 3])?;
if b[i + 3] != b':' {
return None;
}
let om = parse_n_digits(&b[i + 4..i + 6])?;
if oh > 23 || om > 59 {
return None;
}
sign * (oh * 3600 + om * 60)
} else {
return None;
};
let (y, m) = if month <= 2 {
(year - 1, month + 12)
} else {
(year, month)
};
let era = if y >= 0 { y / 400 } else { (y - 399) / 400 };
let yoe = y - era * 400;
let doy = (153 * (m - 3) + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days_since_epoch = era * 146097 + doe - 719468;
let total = days_since_epoch * 86400 + hour * 3600 + minute * 60 + second - offset_secs;
Some(total)
}
fn parse_n_digits(b: &[u8]) -> Option<i64> {
let mut n: i64 = 0;
for &byte in b {
if !byte.is_ascii_digit() {
return None;
}
n = n * 10 + i64::from(byte - b'0');
}
Some(n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_unix_epoch_origin() {
assert_eq!(rfc3339_to_epoch_seconds("1970-01-01T00:00:00Z"), Some(0));
}
#[test]
fn parses_year_2025() {
assert_eq!(
rfc3339_to_epoch_seconds("2025-01-01T00:00:00Z"),
Some(1735689600)
);
}
#[test]
fn parses_leap_day_advances_one_day() {
let leap = rfc3339_to_epoch_seconds("2024-02-29T00:00:00Z").unwrap();
let next = rfc3339_to_epoch_seconds("2024-03-01T00:00:00Z").unwrap();
assert_eq!(next - leap, 86400);
}
#[test]
fn parses_positive_offset() {
assert_eq!(
rfc3339_to_epoch_seconds("2025-01-01T05:00:00+05:00"),
rfc3339_to_epoch_seconds("2025-01-01T00:00:00Z")
);
}
#[test]
fn parses_lowercase_t_and_z() {
assert_eq!(
rfc3339_to_epoch_seconds("2025-01-01t00:00:00z"),
rfc3339_to_epoch_seconds("2025-01-01T00:00:00Z")
);
}
#[test]
fn parses_fractional_seconds_truncated() {
assert_eq!(
rfc3339_to_epoch_seconds("2025-01-01T00:00:00.999Z"),
rfc3339_to_epoch_seconds("2025-01-01T00:00:00Z")
);
}
#[test]
fn rejects_short_string() {
assert_eq!(rfc3339_to_epoch_seconds("2025"), None);
}
#[test]
fn rejects_missing_offset() {
assert_eq!(rfc3339_to_epoch_seconds("2025-01-01T00:00:00"), None);
}
#[test]
fn rejects_out_of_range_month() {
assert_eq!(rfc3339_to_epoch_seconds("2025-13-01T00:00:00Z"), None);
}
#[test]
fn normalize_bare_int_is_instant() {
assert_eq!(
normalize(&Value::Integer(1735689600)),
Some(Tcbdate::Instant(1735689600))
);
}
#[test]
fn normalize_bare_text_is_instant() {
assert_eq!(
normalize(&Value::Text("2025-01-01T00:00:00Z".into())),
Some(Tcbdate::Instant(1735689600))
);
}
#[test]
fn normalize_tag_0_text_is_instant() {
let v = Value::Tag(0, Box::new(Value::Text("2025-01-01T00:00:00Z".into())));
assert_eq!(normalize(&v), Some(Tcbdate::Instant(1735689600)));
}
#[test]
fn normalize_tag_1_int_is_instant() {
let v = Value::Tag(1, Box::new(Value::Integer(1735689600)));
assert_eq!(normalize(&v), Some(Tcbdate::Instant(1735689600)));
}
#[test]
fn normalize_etime_with_key_1_is_instant() {
let body = Value::Map(vec![(Value::Integer(1), Value::Integer(1735689600))]);
let v = Value::Tag(1001, Box::new(body));
assert_eq!(normalize(&v), Some(Tcbdate::Instant(1735689600)));
}
#[test]
fn normalize_etime_without_key_1_is_none() {
let body = Value::Map(vec![(Value::Integer(2), Value::Integer(0))]);
let v = Value::Tag(1001, Box::new(body));
assert_eq!(normalize(&v), None);
}
#[test]
fn normalize_period_with_explicit_bounds() {
let body = Value::Array(vec![Value::Integer(1704067200), Value::Integer(1735689600)]);
let v = Value::Tag(1003, Box::new(body));
assert_eq!(
normalize(&v),
Some(Tcbdate::Period {
lo: Some(1704067200),
hi: Some(1735689600),
})
);
}
#[test]
fn normalize_period_with_open_start() {
let body = Value::Array(vec![Value::Null, Value::Integer(1735689600)]);
let v = Value::Tag(1003, Box::new(body));
assert_eq!(
normalize(&v),
Some(Tcbdate::Period {
lo: None,
hi: Some(1735689600),
})
);
}
#[test]
fn normalize_period_with_duration_derives_end() {
let body = Value::Array(vec![Value::Integer(1000), Value::Null, Value::Integer(500)]);
let v = Value::Tag(1003, Box::new(body));
assert_eq!(
normalize(&v),
Some(Tcbdate::Period {
lo: Some(1000),
hi: Some(1500),
})
);
}
#[test]
fn normalize_period_with_text_bounds() {
let body = Value::Array(vec![
Value::Text("2024-01-01T00:00:00Z".into()),
Value::Text("2025-01-01T00:00:00Z".into()),
]);
let v = Value::Tag(1003, Box::new(body));
assert_eq!(
normalize(&v),
Some(Tcbdate::Period {
lo: Some(1704067200),
hi: Some(1735689600),
})
);
}
#[test]
fn instant_text_equals_instant_int() {
let r = Value::Text("2025-01-01T00:00:00Z".into());
let e = Value::Integer(1735689600);
assert_eq!(match_tcbdate(&r, &e), Verdict::Pass);
}
#[test]
fn instant_etime_equals_instant_tag_0() {
let r = Value::Tag(
1001,
Box::new(Value::Map(vec![(
Value::Integer(1),
Value::Integer(1735689600),
)])),
);
let e = Value::Tag(0, Box::new(Value::Text("2025-01-01T00:00:00Z".into())));
assert_eq!(match_tcbdate(&r, &e), Verdict::Pass);
}
#[test]
fn instant_inequality_fails() {
let r = Value::Text("2025-01-01T00:00:00Z".into());
let e = Value::Integer(1735689601);
assert_eq!(match_tcbdate(&r, &e), Verdict::Fail);
}
#[test]
fn period_contains_instant_passes() {
let r = Value::Tag(
1003,
Box::new(Value::Array(vec![
Value::Integer(1704067200), Value::Integer(1735689600), ])),
);
let e = Value::Text("2024-06-01T00:00:00Z".into());
assert_eq!(match_tcbdate(&r, &e), Verdict::Pass);
}
#[test]
fn period_excludes_instant_fails() {
let r = Value::Tag(
1003,
Box::new(Value::Array(vec![
Value::Integer(1704067200),
Value::Integer(1735689600),
])),
);
let e = Value::Text("2026-01-01T00:00:00Z".into());
assert_eq!(match_tcbdate(&r, &e), Verdict::Fail);
}
#[test]
fn period_open_bounds_treat_as_infinity() {
let r = Value::Tag(
1003,
Box::new(Value::Array(vec![Value::Null, Value::Integer(1735689600)])),
);
assert_eq!(
match_tcbdate(&r, &Value::Integer(-1_000_000)),
Verdict::Pass
);
assert_eq!(
match_tcbdate(&r, &Value::Integer(1735689601)),
Verdict::Fail
);
}
#[test]
fn period_equals_period_passes() {
let r = Value::Tag(
1003,
Box::new(Value::Array(vec![Value::Integer(0), Value::Integer(100)])),
);
let e = r.clone();
assert_eq!(match_tcbdate(&r, &e), Verdict::Pass);
}
#[test]
fn period_differs_from_period_fails() {
let r = Value::Tag(
1003,
Box::new(Value::Array(vec![Value::Integer(0), Value::Integer(100)])),
);
let e = Value::Tag(
1003,
Box::new(Value::Array(vec![Value::Integer(0), Value::Integer(200)])),
);
assert_eq!(match_tcbdate(&r, &e), Verdict::Fail);
}
#[test]
fn instant_vs_period_fails_explicit() {
let r = Value::Integer(0);
let e = Value::Tag(
1003,
Box::new(Value::Array(vec![Value::Integer(0), Value::Integer(100)])),
);
assert_eq!(match_tcbdate(&r, &e), Verdict::Fail);
}
#[test]
fn unrecognised_falls_back_to_cbor_equality() {
let r = Value::Tag(9999, Box::new(Value::Integer(7)));
let e = r.clone();
assert_eq!(match_tcbdate(&r, &e), Verdict::Pass);
}
}