identity_credential/revocation/status_list_2021/
status_list.rsuse flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use identity_core::convert::Base;
use identity_core::convert::BaseEncoding;
use std::io::Write;
use thiserror::Error;
const MINIMUM_LIST_SIZE: usize = 16 * 1024 * 8;
#[derive(Debug, Error, PartialEq, Eq, Clone, strum::IntoStaticStr)]
pub enum StatusListError {
#[error("The requested entry is not in the list.")]
IndexOutOfBounds,
#[error("\"{0}\" is not a valid encoded status list.")]
InvalidEncoding(String),
#[error("A StatusList2021 must have at least {MINIMUM_LIST_SIZE} entries.")]
InvalidListSize,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct StatusList2021(Box<[u8]>);
impl Default for StatusList2021 {
fn default() -> Self {
StatusList2021::new(MINIMUM_LIST_SIZE).unwrap()
}
}
impl StatusList2021 {
pub fn new(num_entries: usize) -> Result<Self, StatusListError> {
if num_entries < MINIMUM_LIST_SIZE {
return Err(StatusListError::InvalidListSize);
}
let size = num_entries / 8 + (num_entries % 8 != 0) as usize;
let store = vec![0; size];
Ok(StatusList2021(store.into_boxed_slice()))
}
#[allow(clippy::len_without_is_empty)]
pub const fn len(&self) -> usize {
self.0.len() * 8
}
const fn get_unchecked(&self, index: usize) -> bool {
let (i, offset) = Self::entry_index_to_store_index(index);
self.0[i] & (0b1000_0000 >> offset) != 0
}
fn set_unchecked(&mut self, index: usize, value: bool) {
let (i, offset) = Self::entry_index_to_store_index(index);
if value {
self.0[i] |= 0b1000_0000 >> offset
} else {
self.0[i] &= 0b0111_1111 >> offset
}
}
pub fn get(&self, index: usize) -> Result<bool, StatusListError> {
(index < self.len())
.then_some(self.get_unchecked(index))
.ok_or(StatusListError::IndexOutOfBounds)
}
pub fn set(&mut self, index: usize, value: bool) -> Result<(), StatusListError> {
if index < self.len() {
self.set_unchecked(index, value);
Ok(())
} else {
Err(StatusListError::IndexOutOfBounds)
}
}
pub fn try_from_encoded_str(s: &str) -> Result<Self, StatusListError> {
let compressed_status_list =
BaseEncoding::decode(s, Base::Base64).or(Err(StatusListError::InvalidEncoding(s.to_owned())))?;
let status_list = {
use std::io::Read;
let mut decompressor = GzDecoder::new(&compressed_status_list[..]);
let mut status_list = vec![];
decompressor
.read_to_end(&mut status_list)
.or(Err(StatusListError::InvalidEncoding(s.to_owned())))?;
StatusList2021(status_list.into_boxed_slice())
};
Ok(status_list)
}
pub fn into_encoded_str(self) -> String {
let compressed_status_list = {
let mut compressor = GzEncoder::new(vec![], Compression::best());
compressor.write_all(&self.0).unwrap();
compressor.finish().unwrap()
};
BaseEncoding::encode(&compressed_status_list[..], Base::Base64)
}
const fn entry_index_to_store_index(index: usize) -> (usize, usize) {
(index / 8, index % 8)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_status_list() {
let mut status_list = StatusList2021::default();
status_list.set(131071, true).unwrap();
assert!(status_list.get(131071).unwrap());
assert_eq!(status_list.set(131072, true), Err(StatusListError::IndexOutOfBounds));
}
#[test]
fn status_list_too_short_fails() {
assert_eq!(StatusList2021::new(100), Err(StatusListError::InvalidListSize));
}
#[test]
fn status_list_entry_access() {
let mut status_list = StatusList2021::default();
status_list.set(42, true).unwrap();
assert!(status_list.get(42).unwrap());
status_list.set(42, false).unwrap();
assert_eq!(status_list, StatusList2021::default());
}
#[test]
fn status_list_encode_decode() {
let mut status_list = StatusList2021::default();
status_list.set(42, true).unwrap();
status_list.set(420, true).unwrap();
status_list.set(4200, true).unwrap();
let encoded = status_list.clone().into_encoded_str();
let decoded = StatusList2021::try_from_encoded_str(&encoded).unwrap();
assert_eq!(decoded, status_list);
}
}