use core::fmt;
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use crate::jose::{JwsError, JwsHeader, Signer, base64url};
use crate::vc::{Jwt, JwtClaims};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Status {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_list: Option<StatusListRef>,
#[serde(flatten)]
pub additional: IndexMap<String, Value>,
}
impl Status {
pub fn new() -> Self {
Self::default()
}
pub fn status_list(reference: StatusListRef) -> Self {
Self {
status_list: Some(reference),
additional: IndexMap::new(),
}
}
#[must_use]
pub fn with_mechanism(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.additional.insert(name.into(), value.into());
self
}
pub fn is_empty(&self) -> bool {
self.status_list.is_none() && self.additional.is_empty()
}
pub fn mechanisms(&self) -> impl Iterator<Item = StatusMechanism<'_>> {
self.status_list
.iter()
.map(StatusMechanism::StatusList)
.chain(
self.additional
.iter()
.map(|(name, value)| StatusMechanism::Other { name, value }),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StatusMechanism<'a> {
StatusList(&'a StatusListRef),
Other {
name: &'a str,
value: &'a Value,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StatusListRef {
pub idx: u64,
pub uri: String,
}
impl StatusListRef {
pub fn new(uri: impl Into<String>, idx: u64) -> Self {
Self {
idx,
uri: uri.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StatusType(pub u8);
impl StatusType {
pub const VALID: Self = Self(0);
pub const INVALID: Self = Self(1);
pub const SUSPENDED: Self = Self(2);
pub const fn value(self) -> u8 {
self.0
}
pub const fn is_valid(self) -> bool {
self.0 == 0
}
pub const fn name(self) -> Option<&'static str> {
match self.0 {
0 => Some("VALID"),
1 => Some("INVALID"),
2 => Some("SUSPENDED"),
_ => None,
}
}
}
impl fmt::Display for StatusType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.name() {
Some(name) => write!(f, "{name} ({:#04x})", self.0),
None => write!(f, "{:#04x}", self.0),
}
}
}
impl From<u8> for StatusType {
fn from(value: u8) -> Self {
Self(value)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StatusListError {
#[error("`bits` is {0}, not 1, 2, 4 or 8")]
InvalidBits(u8),
#[error("status value {value} does not fit in {bits} bits")]
InvalidStatusValue {
value: u8,
bits: u8,
},
#[error("index {idx} is out of range of a status list of {len} entries")]
IndexOutOfRange {
idx: u64,
len: usize,
},
#[error("invalid status list: {0}")]
Compression(String),
#[error("the status list decompresses to more than {limit} bytes")]
TooLarge {
limit: usize,
},
#[error("invalid JWS: {0}")]
Jws(#[from] JwsError),
#[error("header `typ` is {0:?}, not `statuslist+jwt`")]
UnexpectedTyp(Option<String>),
#[error("the payload is not a status list token: {0}")]
Payload(String),
#[error("status list token `sub` {sub:?} is not the referenced {uri:?}")]
SubjectMismatch {
sub: String,
uri: String,
},
#[error("status list token expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
Expired {
expired_at: DateTime<Utc>,
now: DateTime<Utc>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusList {
bits: u8,
bytes: Vec<u8>,
aggregation_uri: Option<String>,
}
impl StatusList {
pub const MAX_BYTES: usize = 1 << 24;
pub fn new(bits: u8, size: usize) -> Result<Self, StatusListError> {
check_bits(bits)?;
let bytes = vec![0; (size * usize::from(bits)).div_ceil(8)];
Ok(Self {
bits,
bytes,
aggregation_uri: None,
})
}
pub fn from_bytes(bits: u8, bytes: Vec<u8>) -> Result<Self, StatusListError> {
check_bits(bits)?;
Ok(Self {
bits,
bytes,
aggregation_uri: None,
})
}
#[must_use]
pub fn with_aggregation_uri(mut self, uri: impl Into<String>) -> Self {
self.aggregation_uri = Some(uri.into());
self
}
pub const fn bits(&self) -> u8 {
self.bits
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn aggregation_uri(&self) -> Option<&str> {
self.aggregation_uri.as_deref()
}
pub fn len(&self) -> usize {
self.bytes.len() * 8 / usize::from(self.bits)
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn get(&self, index: u64) -> Option<StatusType> {
let (byte, shift) = self.position(index)?;
let mask = self.mask();
Some(StatusType((self.bytes.get(byte)? >> shift) & mask))
}
pub fn set(&mut self, index: u64, status: StatusType) -> Result<(), StatusListError> {
let mask = self.mask();
if status.0 & !mask != 0 {
return Err(StatusListError::InvalidStatusValue {
value: status.0,
bits: self.bits,
});
}
let len = self.len();
let out_of_range = || StatusListError::IndexOutOfRange { idx: index, len };
let (byte, shift) = self.position(index).ok_or_else(out_of_range)?;
let slot = self.bytes.get_mut(byte).ok_or_else(out_of_range)?;
*slot = (*slot & !(mask << shift)) | (status.0 << shift);
Ok(())
}
fn position(&self, index: u64) -> Option<(usize, u8)> {
let bits = u64::from(self.bits);
let bit = index.checked_mul(bits)?;
let byte = usize::try_from(bit / 8).ok()?;
let shift = u8::try_from(bit % 8).ok()?;
(byte < self.bytes.len()).then_some((byte, shift))
}
const fn mask(&self) -> u8 {
if self.bits == 8 {
0xff
} else {
(1 << self.bits) - 1
}
}
pub fn to_lst(&self) -> String {
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&self.bytes, 10);
base64url::encode(&compressed)
}
pub fn from_lst(bits: u8, lst: &str, max_bytes: usize) -> Result<Self, StatusListError> {
check_bits(bits)?;
let compressed = base64url::decode(lst)
.map_err(|e| StatusListError::Compression(format!("`lst` is not base64url: {e}")))?;
let bytes = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(&compressed, max_bytes)
.map_err(|e| match e.status {
miniz_oxide::inflate::TINFLStatus::HasMoreOutput => {
StatusListError::TooLarge { limit: max_bytes }
}
status => {
StatusListError::Compression(format!("`lst` is not ZLIB data: {status:?}"))
}
})?;
Ok(Self {
bits,
bytes,
aggregation_uri: None,
})
}
}
fn check_bits(bits: u8) -> Result<(), StatusListError> {
match bits {
1 | 2 | 4 | 8 => Ok(()),
_ => Err(StatusListError::InvalidBits(bits)),
}
}
#[derive(Serialize, Deserialize)]
struct StatusListJson {
bits: u8,
lst: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
aggregation_uri: Option<String>,
}
impl Serialize for StatusList {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StatusListJson {
bits: self.bits,
lst: self.to_lst(),
aggregation_uri: self.aggregation_uri.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for StatusList {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let json = StatusListJson::deserialize(deserializer)?;
let mut list = Self::from_lst(json.bits, &json.lst, Self::MAX_BYTES)
.map_err(serde::de::Error::custom)?;
list.aggregation_uri = json.aggregation_uri;
Ok(list)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StatusListToken {
pub sub: String,
#[serde(deserialize_with = "crate::vc::required_numeric_date")]
pub iat: i64,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::vc::numeric_date"
)]
pub exp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<u64>,
pub status_list: StatusList,
#[serde(flatten)]
pub additional: IndexMap<String, Value>,
}
impl StatusListToken {
pub const TYP: &'static str = "statuslist+jwt";
pub const MEDIA_TYPE: &'static str = "application/statuslist+jwt";
pub fn new(sub: impl Into<String>, iat: DateTime<Utc>, status_list: StatusList) -> Self {
Self {
sub: sub.into(),
iat: JwtClaims::numeric_date(iat),
exp: None,
ttl: None,
status_list,
additional: IndexMap::new(),
}
}
#[must_use]
pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
self.exp = Some(JwtClaims::numeric_date(time));
self
}
#[must_use]
pub fn with_ttl(mut self, seconds: u64) -> Self {
self.ttl = Some(seconds);
self
}
#[must_use]
pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.additional.insert(name.into(), value.into());
self
}
pub fn decode(jwt: &Jwt) -> Result<(JwsHeader, Self), StatusListError> {
let jws = jwt.decode()?;
if jws.header().typ.as_deref() != Some(Self::TYP) {
return Err(StatusListError::UnexpectedTyp(jws.header().typ.clone()));
}
let token = serde_json::from_slice(jws.payload())
.map_err(|e| StatusListError::Payload(e.to_string()))?;
Ok((jws.header().clone(), token))
}
pub async fn sign(&self, signer: &dyn Signer) -> Result<Jwt, StatusListError> {
let mut header = JwsHeader::new(signer.alg()).with_typ(Self::TYP);
header.kid = signer.kid().map(str::to_owned);
let payload = serde_json::to_vec(self).expect("a token serializes");
let jwt = crate::jose::encode_compact(&header, &payload, signer).await?;
Ok(Jwt::new(jwt).expect("a freshly encoded JWS decodes"))
}
pub fn status_of(
&self,
reference: &StatusListRef,
now: DateTime<Utc>,
) -> Result<StatusType, StatusListError> {
if self.sub != reference.uri {
return Err(StatusListError::SubjectMismatch {
sub: self.sub.clone(),
uri: reference.uri.clone(),
});
}
if let Some(exp) = self.exp {
let expired_at = JwtClaims::timestamp(exp).unwrap_or(DateTime::<Utc>::MIN_UTC);
if expired_at <= now {
return Err(StatusListError::Expired { expired_at, now });
}
}
self.status_list
.get(reference.idx)
.ok_or(StatusListError::IndexOutOfRange {
idx: reference.idx,
len: self.status_list.len(),
})
}
}