use serde::{
ser::{Error as SerError, Serializer},
Deserialize, Serialize,
};
use std::collections::BTreeMap;
use time::{
error::Format as FormatError, error::Parse as ParseError,
format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "ciborium::Value")]
pub struct ValidityInfo {
pub signed: OffsetDateTime,
pub valid_from: OffsetDateTime,
pub valid_until: OffsetDateTime,
pub expected_update: Option<OffsetDateTime>,
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("When parsing a CBOR map, could not find required field: '{0:?}'")]
MissingField(ciborium::Value),
#[error("Expected to parse a CBOR map, received: '{0:?}'")]
NotAMap(ciborium::Value),
#[error("Expected to parse a CBOR text string, received: '{0:?}'")]
NotATextString(Box<ciborium::Value>),
#[error("Expected to parse a CBOR tag (number {0}), received: '{1:?}'")]
NotATag(u64, ciborium::Value),
#[error(transparent)]
OutOfRange(#[from] time::error::ComponentRange),
#[error("Failed to format date string as rfc3339 date: {0}")]
UnableToFormatDate(#[from] FormatError),
#[error("Failed to parse date string as rfc3339 date: {0}")]
UnableToParseDate(#[from] ParseError),
#[error("Could not serialize to cbor: {0}")]
CborErrorWithSource(coset::CoseError),
#[error("Could not serialize to cbor")]
CborError,
}
impl TryFrom<ValidityInfo> for ciborium::Value {
type Error = Error;
fn try_from(v: ValidityInfo) -> Result<ciborium::Value> {
macro_rules! insert_date {
($map:ident, $date:ident, $name:literal) => {
let key = ciborium::Value::Text(String::from($name));
let value = ciborium::Value::Tag(
0,
Box::new(ciborium::Value::Text(
$date
.replace_millisecond(0)?
.to_offset(UtcOffset::UTC)
.format(&Rfc3339)?,
)),
);
$map.push((key, value));
};
($map:ident, $struct: ident, $field:ident, $name:literal) => {
let date = $struct.$field;
insert_date!($map, date, $name)
};
}
let mut map = vec![];
insert_date!(map, v, signed, "signed");
insert_date!(map, v, valid_from, "validFrom");
insert_date!(map, v, valid_until, "validUntil");
if let Some(expected_update) = v.expected_update {
insert_date!(map, expected_update, "expectedUpdate");
}
Ok(ciborium::Value::Map(map))
}
}
impl TryFrom<ciborium::Value> for ValidityInfo {
type Error = Error;
fn try_from(v: ciborium::Value) -> Result<ValidityInfo> {
if let ciborium::Value::Map(map) = v.clone() {
let mut map: BTreeMap<String, ciborium::Value> = map
.into_iter()
.map(|(k, v)| {
let k = k.into_text().map_err(|_| Error::CborError)?;
Ok((k, v))
})
.collect::<Result<BTreeMap<_, _>>>()?;
macro_rules! extract_date {
($map:ident, $name:literal) => {{
let key = String::from($name);
$map.remove(&key)
.ok_or(Error::MissingField(key.into()))
.and_then(cbor_to_datetime)?
}};
}
let signed = extract_date!(map, "signed");
let valid_from = extract_date!(map, "validFrom");
let valid_until = extract_date!(map, "validUntil");
let expected_update_key = String::from("expectedUpdate");
let expected_update = map
.remove(&expected_update_key)
.map(cbor_to_datetime)
.transpose()?;
Ok(Self {
signed,
valid_from,
valid_until,
expected_update,
})
} else {
Err(Error::NotAMap(v))
}
}
}
impl Serialize for ValidityInfo {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
ciborium::Value::try_from(self.clone())
.map_err(S::Error::custom)?
.serialize(s)
}
}
fn cbor_to_datetime(v: ciborium::Value) -> Result<OffsetDateTime> {
if let ciborium::Value::Tag(0, inner) = v.clone() {
if let ciborium::Value::Text(date_str) = inner.as_ref() {
Ok(OffsetDateTime::parse(date_str, &Rfc3339)?)
} else {
Err(Error::NotATextString(inner))
}
} else {
Err(Error::NotATag(0, v))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn roundtrip() {
let cbor = hex::decode("A3667369676E6564C074323032302D30312D30315430303A30303A30305A6976616C696446726F6DC074323032302D30312D30315430303A30303A30305A6A76616C6964556E74696CC074323032302D30312D30315430303A30303A30305A").unwrap();
let validity_info: ValidityInfo = crate::cbor::from_slice(&cbor).unwrap();
let roundtripped = crate::cbor::to_vec(&validity_info).unwrap();
assert_eq!(cbor, roundtripped);
}
#[test]
fn trim() {
let cbor = hex::decode("A3667369676E6564C07818323032302D30312D30315430303A30303A30302E3130315A6976616C696446726F6DC0781B323032302D30312D30315430303A30303A30302E3131323231395A6A76616C6964556E74696CC0781E323032302D30312D30315430303A30303A30302E3939393939393939395A").unwrap();
let validity_info: ValidityInfo = crate::cbor::from_slice(&cbor).unwrap();
let roundtripped = crate::cbor::to_vec(&validity_info).unwrap();
let trimmed = hex::decode("A3667369676E6564C074323032302D30312D30315430303A30303A30305A6976616C696446726F6DC074323032302D30312D30315430303A30303A30305A6A76616C6964556E74696CC074323032302D30312D30315430303A30303A30305A").unwrap();
assert_eq!(trimmed, roundtripped);
}
}