use base64::{engine::general_purpose as base64_engine, Engine as _};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize, Serializer};
pub fn get_epoch_baseline() -> chrono::NaiveDateTime {
#[allow(clippy::unwrap_used)] chrono::NaiveDateTime::parse_from_str("0001-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S").unwrap()
}
#[derive(Debug, Clone, Copy)]
pub enum TimestampMode {
Base64,
Iso8601,
}
#[derive(Debug)]
pub struct Timestamp {
pub mode: TimestampMode,
pub time: NaiveDateTime,
}
impl Timestamp {
pub fn new_base64(time: NaiveDateTime) -> Self {
Timestamp {
mode: TimestampMode::Base64,
time,
}
}
pub fn new_iso8601(time: NaiveDateTime) -> Self {
Timestamp {
mode: TimestampMode::Iso8601,
time,
}
}
}
impl From<Timestamp> for NaiveDateTime {
fn from(t: Timestamp) -> Self {
t.time
}
}
impl From<NaiveDateTime> for Timestamp {
fn from(t: NaiveDateTime) -> Self {
Timestamp::new_iso8601(t)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let t = String::deserialize(deserializer)?;
match chrono::NaiveDateTime::parse_from_str(&t, "%Y-%m-%dT%H:%M:%SZ") {
Ok(ndt) => Ok(Timestamp::new_iso8601(ndt)),
_ => {
let v = base64_engine::STANDARD
.decode(t)
.map_err(serde::de::Error::custom)?;
let mut a: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0];
#[allow(clippy::indexing_slicing)] a.copy_from_slice(&v[0..8]);
let ndt = get_epoch_baseline() + chrono::Duration::seconds(i64::from_le_bytes(a));
Ok(Timestamp::new_base64(ndt))
}
}
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.mode {
TimestampMode::Iso8601 => {
let s = self.time.format("%Y-%m-%dT%H:%M:%SZ").to_string();
serializer.serialize_str(&s)
}
TimestampMode::Base64 => {
let duration = self.time - get_epoch_baseline();
let seconds = duration.num_seconds();
let b = seconds.to_le_bytes();
let b64 = base64_engine::STANDARD.encode(b);
serializer.serialize_str(&b64)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize)]
struct Test<T>(T);
#[test]
fn test_deserialize_timestamp_iso8601() {
let ts_str = "2023-10-05T12:34:56Z";
let ts: Timestamp = quick_xml::de::from_str(&format!("{}", ts_str)).unwrap();
assert_eq!(
ts.time,
NaiveDateTime::parse_from_str("2023-10-05T12:34:56", "%Y-%m-%dT%H:%M:%S").unwrap()
);
match ts.mode {
TimestampMode::Iso8601 => (),
_ => panic!("Expected Iso8601 mode"),
}
}
#[test]
fn test_deserialize_timestamp_base64() {
let ts_str = "AQAAAAAAAAA="; let ts: Timestamp = quick_xml::de::from_str(&format!("{}", ts_str)).unwrap();
assert_eq!(ts.time, get_epoch_baseline() + chrono::Duration::seconds(1));
match ts.mode {
TimestampMode::Base64 => (),
_ => panic!("Expected Base64 mode"),
}
}
#[test]
fn test_serialize_timestamp_iso8601() {
let ts = Timestamp {
mode: TimestampMode::Iso8601,
time: NaiveDateTime::parse_from_str("2023-10-05T12:34:56", "%Y-%m-%dT%H:%M:%S").unwrap(),
};
let serialized = quick_xml::se::to_string(&Test(ts)).unwrap();
assert_eq!(serialized, "<Test>2023-10-05T12:34:56Z</Test>");
}
#[test]
fn test_serialize_timestamp_base64() {
let ts = Timestamp {
mode: TimestampMode::Base64,
time: get_epoch_baseline() + chrono::Duration::seconds(1),
};
let serialized = quick_xml::se::to_string(&Test(ts)).unwrap();
assert_eq!(serialized, "<Test>AQAAAAAAAAA=</Test>");
}
}