use ::chrono::{DateTime, Utc};
use vitaminc_protected::{OpaqueDebug, TimingSafeEq};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::plaintext::{depth, Encrypt, Int, Logical, Physical, Sink, TimeUnit};
use crate::typed::FromScalar;
#[derive(Default, OpaqueDebug, TimingSafeEq, Zeroize, ZeroizeOnDrop)]
pub struct Timestamp {
secs: i64,
nanos: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("a leap second has no encoding")]
pub struct LeapSecond;
impl Timestamp {
pub fn new(instant: DateTime<Utc>) -> Result<Self, LeapSecond> {
let nanos = instant.timestamp_subsec_nanos();
if nanos >= NANOS_PER_SECOND {
return Err(LeapSecond);
}
Ok(Self {
secs: instant.timestamp(),
nanos,
})
}
pub fn to_datetime(&self) -> DateTime<Utc> {
DateTime::from_timestamp(self.secs, self.nanos).unwrap_or_default()
}
fn count(&self) -> i128 {
i128::from(self.secs) * i128::from(NANOS_PER_SECOND) + i128::from(self.nanos)
}
}
const NANOS_PER_SECOND: u32 = 1_000_000_000;
impl TryFrom<DateTime<Utc>> for Timestamp {
type Error = LeapSecond;
fn try_from(instant: DateTime<Utc>) -> Result<Self, LeapSecond> {
Self::new(instant)
}
}
impl From<Timestamp> for DateTime<Utc> {
fn from(ts: Timestamp) -> Self {
ts.to_datetime()
}
}
impl Encrypt for Timestamp {
const DEPTH: Option<usize> = depth::SCALAR;
fn visit<S: Sink>(self, sink: S) -> Result<S::Ok, S::Error> {
(&self).visit(sink)
}
}
impl Encrypt for &Timestamp {
const DEPTH: Option<usize> = depth::SCALAR;
fn visit<S: Sink>(self, sink: S) -> Result<S::Ok, S::Error> {
sink.scalar(
Some(Logical::Timestamp(TimeUnit::Nanos)),
Physical::Int(Int::I128(self.count())),
)
}
}
impl FromScalar for Timestamp {
const EXPECTED: &'static str = "timestamp";
fn from_scalar(ty: Option<Logical>, value: Physical<'_>) -> Option<Self> {
DateTime::<Utc>::from_scalar(ty, value).and_then(|instant| Self::new(instant).ok())
}
}
#[cfg(test)]
mod tests {
use ::chrono::NaiveDate;
use super::*;
use crate::{plan, to_frame, EncryptError};
use vitaminc_protected::Controlled;
fn frame<P: Encrypt>(value: P) -> Vec<u8> {
to_frame(value).expect("frames").risky_ref().clone()
}
fn leap() -> DateTime<Utc> {
NaiveDate::from_ymd_opt(2016, 12, 31)
.and_then(|d| d.and_hms_nano_opt(23, 59, 59, 1_000_000_000))
.expect("a leap second")
.and_utc()
}
#[test]
fn a_leap_second_is_refused_when_built() {
assert_eq!(Timestamp::new(leap()).err(), Some(LeapSecond));
assert_eq!(Timestamp::try_from(leap()).err(), Some(LeapSecond));
assert_eq!(LeapSecond.to_string(), "a leap second has no encoding");
let last = NaiveDate::from_ymd_opt(2016, 12, 31)
.and_then(|d| d.and_hms_nano_opt(23, 59, 59, 999_999_999))
.expect("ok")
.and_utc();
let ts = Timestamp::new(last).expect("not a leap second");
assert_eq!(ts.to_datetime(), last);
assert_eq!(DateTime::<Utc>::from(ts), last);
}
#[test]
fn every_instant_comes_back_as_it_went_in() {
for instant in [
DateTime::<Utc>::MIN_UTC,
DateTime::<Utc>::MAX_UTC,
DateTime::<Utc>::UNIX_EPOCH,
DateTime::from_timestamp(-1, 999_999_999).expect("ok"),
DateTime::from_timestamp(1_700_000_000, 123_456_789).expect("ok"),
] {
let ts = Timestamp::new(instant).expect("not a leap second");
assert_eq!(ts.to_datetime(), instant, "{instant}");
}
assert_eq!(
Timestamp::default().to_datetime(),
DateTime::<Utc>::UNIX_EPOCH
);
}
#[test]
fn it_compares_in_constant_time_wipes_and_redacts() {
let at = |secs, nanos| {
Timestamp::new(DateTime::from_timestamp(secs, nanos).expect("ok")).expect("ok")
};
assert!(bool::from(at(5, 1).ts_eq(&at(5, 1))));
assert!(at(5, 1) == at(5, 1));
assert!(at(5, 1) != at(5, 2));
assert!(at(5, 1) != at(6, 1));
let mut ts = at(1_700_000_000, 7);
ts.zeroize();
assert!(ts == Timestamp::default());
let shown = format!("{:?}", at(1_700_000_000, 7));
assert!(!shown.contains("1700000000"), "{shown}");
}
#[test]
fn it_walks_as_the_datetime_it_holds() {
for instant in [
DateTime::from_timestamp(1, 5).expect("ok"),
DateTime::from_timestamp(-1, 999_999_999).expect("ok"),
DateTime::<Utc>::MIN_UTC,
DateTime::<Utc>::MAX_UTC,
] {
let ts = Timestamp::new(instant).expect("ok");
assert_eq!(frame(&ts), frame(instant), "{instant}");
assert_eq!(frame(ts), frame(instant), "{instant}");
}
assert_eq!(<Timestamp as Encrypt>::DEPTH, Some(0));
assert_eq!(<&Timestamp as Encrypt>::DEPTH, Some(0));
let decoded = Timestamp::from_scalar(
Some(Logical::Timestamp(TimeUnit::Nanos)),
Physical::Int(Int::I128(1_000_000_005)),
)
.expect("a timestamp");
assert_eq!(
decoded.to_datetime(),
DateTime::from_timestamp(1, 5).expect("ok")
);
assert!(Timestamp::from_scalar(None, Physical::Int(Int::I128(1))).is_none());
assert!(matches!(
plan::<_, (), _>(leap(), "aad"),
Err(EncryptError::Source(_))
));
}
}