From 7d3225a4890064ccc2a83ab275ae53b09994859f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 5 Sep 2024 14:36:10 +0200 Subject: [PATCH 1/5] Update Bitstring Status List implementation to Candidate Recommendation Draft 10 June 2024. --- crates/status/Cargo.toml | 2 +- .../files/local-status-list-credential.jsonld | 4 +- .../files/status_list_revocable_1.jsonld | 8 + .../files/status_list_revocable_3.jsonld | 8 + crates/status/examples/status_list.rs | 18 +- crates/status/examples/status_list_client.rs | 1 + crates/status/src/impl/any.rs | 44 ++- .../src/impl/bitstring_status_list/mod.rs | 356 +++++++++++++----- .../syntax/entry_set/mod.rs | 82 +++- .../syntax/status_list/mod.rs | 51 +-- .../status/src/impl/token_status_list/json.rs | 5 + .../status/src/impl/token_status_list/mod.rs | 30 +- crates/status/src/lib.rs | 33 +- 13 files changed, 476 insertions(+), 166 deletions(-) diff --git a/crates/status/Cargo.toml b/crates/status/Cargo.toml index fedd35da1..a8ad24c1e 100644 --- a/crates/status/Cargo.toml +++ b/crates/status/Cargo.toml @@ -31,7 +31,7 @@ parking_lot = "0.12.1" flate2 = "1.0.28" [dev-dependencies] -ssi-jwk = { workspace = true, features = ["secp256r1"] } +ssi-jws = { workspace = true, features = ["secp256r1"] } ssi-dids.workspace = true ssi-data-integrity = { workspace = true, features = ["w3c"] } tokio = { version = "1.0", features = ["macros", "rt"] } diff --git a/crates/status/examples/files/local-status-list-credential.jsonld b/crates/status/examples/files/local-status-list-credential.jsonld index fc58e4e77..a0268f97b 100644 --- a/crates/status/examples/files/local-status-list-credential.jsonld +++ b/crates/status/examples/files/local-status-list-credential.jsonld @@ -15,9 +15,9 @@ "proof": { "@context": "https://w3id.org/security/suites/jws-2020/v1", "type": "JsonWebSignature2020", - "created": "2024-04-15T14:53:55.745Z", + "created": "2024-09-05T12:10:49.127Z", "verificationMethod": "did:jwk:eyJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiRnlHbnZLcXJ5cWlzX3gwWHJTbENTbnpFeGlrSk5XLUgzY0hQWnRfZjBmUSIsInkiOiJKZGJUM1BJWllIdHdzNVprZUlLakM5RnhJaGZwYV9rbHVJZms5ekZwczZzIn0#0", "proofPurpose": "assertionMethod", - "jws": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..VcTAx3Rnt6rHn6EQTOdA1T6bywuySmiq3ik8Ldhbb1TETNFGnrWR80210yikaZKJGtduEdPquQFNTZZ0xvRyiw" + "jws": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..jxE8WqQwJ8FDmkk3njwU80kuVkPsmL_5b5hcMJQO2Tz5D-GqZ_TsTlXMDg2cxPiS1IcK11Gtz2xvK4d0i8U1PA" } } \ No newline at end of file diff --git a/crates/status/examples/files/status_list_revocable_1.jsonld b/crates/status/examples/files/status_list_revocable_1.jsonld index fa14f261f..b63363982 100644 --- a/crates/status/examples/files/status_list_revocable_1.jsonld +++ b/crates/status/examples/files/status_list_revocable_1.jsonld @@ -17,5 +17,13 @@ "credentialSubject": { "id": "did:example:6789", "type": "Person" + }, + "proof": { + "@context": "https://w3id.org/security/suites/jws-2020/v1", + "type": "JsonWebSignature2020", + "created": "2024-09-05T12:29:55.727Z", + "verificationMethod": "did:jwk:eyJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiRnlHbnZLcXJ5cWlzX3gwWHJTbENTbnpFeGlrSk5XLUgzY0hQWnRfZjBmUSIsInkiOiJKZGJUM1BJWllIdHdzNVprZUlLakM5RnhJaGZwYV9rbHVJZms5ekZwczZzIn0#0", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..ymlXI3xL878kA_6F160NKkPM1UfdRCe9L-KKD3P0MSuvkV6uJc41VB1-jKdF_Nb8XM8MJLSTVJp3M6aC2Jbp2g" } } \ No newline at end of file diff --git a/crates/status/examples/files/status_list_revocable_3.jsonld b/crates/status/examples/files/status_list_revocable_3.jsonld index 0bcb04c40..4edc31222 100644 --- a/crates/status/examples/files/status_list_revocable_3.jsonld +++ b/crates/status/examples/files/status_list_revocable_3.jsonld @@ -16,5 +16,13 @@ "credentialSubject": { "id": "did:example:6789", "type": "Person" + }, + "proof": { + "@context": "https://w3id.org/security/suites/jws-2020/v1", + "type": "JsonWebSignature2020", + "created": "2024-09-05T12:30:26.917Z", + "verificationMethod": "did:jwk:eyJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiRnlHbnZLcXJ5cWlzX3gwWHJTbENTbnpFeGlrSk5XLUgzY0hQWnRfZjBmUSIsInkiOiJKZGJUM1BJWllIdHdzNVprZUlLakM5RnhJaGZwYV9rbHVJZms5ekZwczZzIn0#0", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..XElrILKSLhlRVpzIw_06OxLP4JN-CJKOrHKECr4wOmro8QpmDad9jeSeIvbU9zrt49nIel3c9ztzmBJgfeOv9g" } } \ No newline at end of file diff --git a/crates/status/examples/status_list.rs b/crates/status/examples/status_list.rs index 097b2b6d1..b89551914 100644 --- a/crates/status/examples/status_list.rs +++ b/crates/status/examples/status_list.rs @@ -22,6 +22,7 @@ use ssi_dids::{VerificationMethodDIDResolver, DIDJWK}; use ssi_jwk::JWK; use ssi_status::{ any::AnyStatusMap, bitstring_status_list, EncodedStatusMap, FromBytes, FromBytesOptions, + StatusSizeError, }; use ssi_verification_methods::{ReferenceOrOwned, SingleSecretSigner}; use std::{ @@ -62,6 +63,9 @@ enum Command { /// Input media type. #[clap(short = 't', long)] media_type: String, + + /// Status size in bits. + status_size: Option, }, /// Create a new status list. @@ -112,6 +116,7 @@ impl Command { Self::Read { filename, media_type, + status_size, } => { let source = filename.map(Source::File).unwrap_or_default(); let bytes = match source.read() { @@ -133,7 +138,7 @@ impl Command { .decode() .map_err(|e| Error::Decode(source, e))?; - let list: Vec<_> = status_list.iter().collect(); + let list: Vec<_> = status_list.iter(status_size)?.collect(); println!("{}", serde_json::to_string_pretty(&list).unwrap()); Ok(()) @@ -152,7 +157,7 @@ async fn create_bitstring_status_list( list: Vec, key: Option, ) -> Result, Error> { - let mut status_list = bitstring_status_list::StatusList::new( + let mut status_list = bitstring_status_list::SizedStatusList::new( bitstring_status_list::StatusSize::default(), bitstring_status_list::TimeToLive::default(), // list.into_iter().map(|v| v.0).collect(), @@ -164,11 +169,7 @@ async fn create_bitstring_status_list( let credential = bitstring_status_list::BitstringStatusListCredential::new( Some(id), - status_list.to_credential_subject( - None, - bitstring_status_list::StatusPurpose::Revocation, - Vec::new(), - ), + status_list.to_credential_subject(None, bitstring_status_list::StatusPurpose::Revocation), ); match key { @@ -212,6 +213,9 @@ enum Error { #[error("unable to read key: {0}")] Key(#[from] KeyError), + + #[error(transparent)] + StatusSize(#[from] StatusSizeError), } #[derive(Debug, thiserror::Error)] diff --git a/crates/status/examples/status_list_client.rs b/crates/status/examples/status_list_client.rs index 5d7d19b0c..cefe8ec8a 100644 --- a/crates/status/examples/status_list_client.rs +++ b/crates/status/examples/status_list_client.rs @@ -112,6 +112,7 @@ async fn run(args: Args) -> Result<(), Error> { let status = status_list .get_entry(&entry) + .unwrap() .ok_or(Error::MissingEntry(entry.key()))?; match status { diff --git a/crates/status/src/impl/any.rs b/crates/status/src/impl/any.rs index 1cdaeb923..db0f42f59 100644 --- a/crates/status/src/impl/any.rs +++ b/crates/status/src/impl/any.rs @@ -11,6 +11,7 @@ use crate::{ }, token_status_list::{self, StatusListToken}, EncodedStatusMap, FromBytes, FromBytesOptions, StatusMap, StatusMapEntry, StatusMapEntrySet, + StatusSizeError, }; pub enum AnyStatusMap { @@ -108,16 +109,22 @@ pub enum AnyDecodedStatusMap { } impl AnyDecodedStatusMap { - pub fn iter(&self) -> AnyDecodedStatusMapIter { + pub fn iter( + &self, + status_size: Option, + ) -> Result { match self { - Self::BitstringStatusList(m) => AnyDecodedStatusMapIter::BitstringStatusList(m.iter()), - Self::TokenStatusList(m) => AnyDecodedStatusMapIter::TokenStatusList(m.iter()), + Self::BitstringStatusList(m) => Ok(AnyDecodedStatusMapIter::BitstringStatusList( + m.iter(status_size.ok_or(StatusSizeError::Missing)?.try_into()?), + )), + Self::TokenStatusList(m) => Ok(AnyDecodedStatusMapIter::TokenStatusList(m.iter())), } } } impl StatusMap for AnyDecodedStatusMap { type Key = usize; + type StatusSize = u8; type Status = u8; fn time_to_live(&self) -> Option { @@ -127,23 +134,22 @@ impl StatusMap for AnyDecodedStatusMap { } } - fn get_by_key(&self, key: Self::Key) -> Option { + fn get_by_key( + &self, + status_size: Option, + key: Self::Key, + ) -> Result, StatusSizeError> { match self { - Self::BitstringStatusList(m) => m.get_by_key(key), - Self::TokenStatusList(m) => m.get_by_key(key), + Self::BitstringStatusList(m) => { + m.get_by_key(status_size.map(TryInto::try_into).transpose()?, key) + } + Self::TokenStatusList(m) => { + m.get_by_key(status_size.map(TryInto::try_into).transpose()?, key) + } } } } -impl<'a> IntoIterator for &'a AnyDecodedStatusMap { - type IntoIter = AnyDecodedStatusMapIter<'a>; - type Item = u8; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - pub enum AnyDecodedStatusMapIter<'a> { BitstringStatusList(bitstring_status_list::BitStringIter<'a>), TokenStatusList(token_status_list::BitStringIter<'a>), @@ -239,6 +245,7 @@ pub enum AnyStatusMapEntryRef<'a> { impl<'a> StatusMapEntry for AnyStatusMapEntryRef<'a> { type Key = usize; + type StatusSize = u8; fn status_list_url(&self) -> &Uri { match self { @@ -253,4 +260,11 @@ impl<'a> StatusMapEntry for AnyStatusMapEntryRef<'a> { Self::TokenStatusList(e) => e.key(), } } + + fn status_size(&self) -> Option { + match self { + Self::BitstringStatusList(e) => e.status_size().map(Into::into), + Self::TokenStatusList(e) => e.status_size().map(Into::into), + } + } } diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index c4f4e722a..c51263778 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -1,4 +1,4 @@ -//! W3C Bitstring Status List v1.0 (Working Draft 06 April 2024) +//! W3C Bitstring Status List v1.0 (Candidate Recommendation Draft 10 June 2024) //! //! A privacy-preserving, space-efficient, and high-performance mechanism for //! publishing status information such as suspension or revocation of Verifiable @@ -10,7 +10,7 @@ use iref::UriBuf; use serde::{Deserialize, Serialize}; use std::{hash::Hash, str::FromStr, time::Duration}; -use crate::{Overflow, StatusMap}; +use crate::{Overflow, StatusMap, StatusSizeError}; mod syntax; pub use syntax::*; @@ -32,6 +32,12 @@ impl StatusMessage { #[error("invalid status size `{0}`")] pub struct InvalidStatusSize(u8); +impl From for StatusSizeError { + fn from(_value: InvalidStatusSize) -> Self { + Self::Invalid + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] pub struct StatusSize(u8); @@ -39,7 +45,7 @@ impl TryFrom for StatusSize { type Error = InvalidStatusSize; fn try_from(value: u8) -> Result { - if value <= 8 { + if (1..=8).contains(&value) { Ok(Self(value)) } else { Err(InvalidStatusSize(value)) @@ -47,6 +53,12 @@ impl TryFrom for StatusSize { } } +impl From for u8 { + fn from(value: StatusSize) -> Self { + value.0 + } +} + impl Default for StatusSize { fn default() -> Self { Self::DEFAULT @@ -68,6 +80,14 @@ impl StatusSize { } } + fn last_of(&self, index: usize) -> Offset { + let bit_offset = self.0 as usize * index + self.0 as usize - 1; + Offset { + byte: bit_offset / 8, + bit: bit_offset % 8, + } + } + fn mask(&self) -> u8 { if self.0 == 8 { 0xff @@ -238,28 +258,19 @@ impl FromStr for StatusPurpose { } } -/// Bit-string as defined by the W3C Bitstring Status List specification. -/// -/// Bits are indexed from most significant to least significant. -/// ```text -/// | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... | n-8 | n-7 | n-6 | n-5 | n-4 | n-3 | n-2 | n-1 | -/// | byte 0 | ... | byte k-1 | -/// ``` -/// -/// See: #[derive(Debug, Clone)] -pub struct BitString { +pub struct SizedBitString { + inner: BitString, status_size: StatusSize, - bytes: Vec, len: usize, } -impl BitString { - /// Creates a new empty bit-string. +impl SizedBitString { + /// Creates a new empty sized status list. pub fn new(status_size: StatusSize) -> Self { Self { + inner: BitString::new(), status_size, - bytes: Vec::new(), len: 0, } } @@ -302,8 +313,8 @@ impl BitString { /// (in number of statuses). pub fn with_capacity(status_size: StatusSize, capacity: usize) -> Self { Self { + inner: BitString::with_capacity(status_size, capacity), status_size, - bytes: Vec::with_capacity((capacity * status_size.0 as usize).div_ceil(8)), len: 0, } } @@ -312,12 +323,16 @@ impl BitString { pub fn from_bytes(status_size: StatusSize, bytes: Vec) -> Self { let len = bytes.len() * 8usize / status_size.0 as usize; Self { + inner: BitString::from_bytes(bytes), status_size, - bytes, len, } } + pub fn status_size(&self) -> StatusSize { + self.status_size + } + /// Checks if the list is empty. pub fn is_empty(&self) -> bool { self.len == 0 @@ -334,15 +349,147 @@ impl BitString { return None; } + self.inner.get(self.status_size, index) + } + + /// Push a new value into the bit-string. + /// + /// Returns the index of the newly inserted value in the list, + /// or an error if the value is too large w.r.t. `status_size`. + pub fn push(&mut self, value: u8) -> Result { + let masked_value = value & self.status_size.mask(); + if masked_value != value { + return Err(Overflow::Value(value)); + } + + let index = self.len; let offset = self.status_size.offset_of(index); + let (high_shift, low_shift) = offset.left_shift(self.status_size); - Some(self.get_at(offset.byte, high_shift, low_shift)) + if offset.byte == self.inner.0.len() { + self.inner + .0 + .push(masked_value.overflowing_signed_shl(high_shift).0); + } else { + self.inner.0[offset.byte] |= masked_value.overflowing_signed_shl(high_shift).0 + } + + if let Some(low_shift) = low_shift { + self.inner.0.push(masked_value.overflowing_shl(low_shift).0); + } + + self.len += 1; + Ok(index) + } + + /// Sets the value at the given index. + /// + /// Returns the previous value, or an `Overflow` error if either the index + /// is out of bounds or the value is too large. + pub fn set(&mut self, index: usize, value: u8) -> Result { + if index >= self.len { + return Err(Overflow::Index(index)); + } + + self.inner.set(self.status_size, index, value) + } + + /// Returns an iterator over all the statuses stored in this bit-string. + pub fn iter(&self) -> BitStringIter { + self.inner.iter(self.status_size) + } + + /// Encodes the bit-string. + pub fn encode(&self) -> EncodedList { + self.inner.encode() + } + + pub fn into_unsized(self) -> BitString { + self.inner + } +} + +/// Bit-string as defined by the W3C Bitstring Status List specification. +/// +/// Bits are indexed from most significant to least significant. +/// ```text +/// | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... | n-8 | n-7 | n-6 | n-5 | n-4 | n-3 | n-2 | n-1 | +/// | byte 0 | ... | byte k-1 | +/// ``` +/// +/// See: +#[derive(Debug, Default, Clone)] +pub struct BitString(Vec); + +impl BitString { + /// Creates a new empty bit-string. + pub fn new() -> Self { + Self::default() } - fn get_at(&self, byte_offset: usize, high_shift: i32, low_shift: Option) -> u8 { + /// Creates a new bit-string of the given length, using `f` to initialize + /// every status. + /// + /// The `f` function is called with the index of the initialized status. + pub fn new_with( + status_size: StatusSize, + len: usize, + f: impl FnMut(usize) -> u8, + ) -> Result { + SizedBitString::new_with(status_size, len, f).map(SizedBitString::into_unsized) + } + + /// Creates a new bit-string of the given length, setting every status + /// to the same value. + pub fn new_with_value( + status_size: StatusSize, + len: usize, + value: u8, + ) -> Result { + Self::new_with(status_size, len, |_| value) + } + + /// Creates a new bit-string of the given length, setting every status + /// to 0. + pub fn new_zeroed(status_size: StatusSize, len: usize) -> Self { + Self::new_with_value(status_size, len, 0).unwrap() // 0 cannot overflow. + } + + /// Creates a new bit-string with the given status size and capacity + /// (in number of statuses). + pub fn with_capacity(status_size: StatusSize, capacity: usize) -> Self { + Self(Vec::with_capacity( + (capacity * status_size.0 as usize).div_ceil(8), + )) + } + + /// Creates a bit-string from a byte array and status size. + pub fn from_bytes(bytes: Vec) -> Self { + Self(bytes) + } + + /// Returns the value stored in the list at the given index. + pub fn get(&self, status_size: StatusSize, index: usize) -> Option { + if status_size.last_of(index).byte >= self.0.len() { + return None; + } + + let offset = status_size.offset_of(index); + let (high_shift, low_shift) = offset.left_shift(status_size); + + Some(self.get_at(status_size, offset.byte, high_shift, low_shift)) + } + + fn get_at( + &self, + status_size: StatusSize, + byte_offset: usize, + high_shift: i32, + low_shift: Option, + ) -> u8 { let high = self - .bytes + .0 .get(byte_offset) .unwrap() .overflowing_signed_shr(high_shift) @@ -350,7 +497,7 @@ impl BitString { let low = match low_shift { Some(low_shift) => { - self.bytes + self.0 .get(byte_offset + 1) .unwrap() .overflowing_shr(low_shift) @@ -359,81 +506,57 @@ impl BitString { None => 0, }; - (high | low) & self.status_size.mask() + (high | low) & status_size.mask() } /// Sets the value at the given index. /// /// Returns the previous value, or an `Overflow` error if either the index /// is out of bounds or the value is too large. - pub fn set(&mut self, index: usize, value: u8) -> Result { - if index >= self.len { + pub fn set( + &mut self, + status_size: StatusSize, + index: usize, + value: u8, + ) -> Result { + if status_size.last_of(index).byte >= self.0.len() { return Err(Overflow::Index(index)); } - let mask = self.status_size.mask(); + let mask = status_size.mask(); let masked_value = value & mask; if masked_value != value { return Err(Overflow::Value(value)); } - let offset = self.status_size.offset_of(index); - let (high_shift, low_shift) = offset.left_shift(self.status_size); + let offset = status_size.offset_of(index); + let (high_shift, low_shift) = offset.left_shift(status_size); - let old_value = self.get_at(offset.byte, high_shift, low_shift); + let old_value = self.get_at(status_size, offset.byte, high_shift, low_shift); - self.bytes[offset.byte] &= !mask.overflowing_signed_shl(high_shift).0; // clear high - self.bytes[offset.byte] |= masked_value.overflowing_signed_shl(high_shift).0; // set high + self.0[offset.byte] &= !mask.overflowing_signed_shl(high_shift).0; // clear high + self.0[offset.byte] |= masked_value.overflowing_signed_shl(high_shift).0; // set high if let Some(low_shift) = low_shift { - self.bytes[offset.byte + 1] &= !mask.overflowing_shl(low_shift).0; // clear low - self.bytes[offset.byte + 1] |= masked_value.overflowing_shl(low_shift).0; + self.0[offset.byte + 1] &= !mask.overflowing_shl(low_shift).0; // clear low + self.0[offset.byte + 1] |= masked_value.overflowing_shl(low_shift).0; // set low } Ok(old_value) } - /// Push a new value into the bit-string. - /// - /// Returns the index of the newly inserted value in the list, - /// or an error if the value is too large w.r.t. `status_size`. - pub fn push(&mut self, value: u8) -> Result { - let masked_value = value & self.status_size.mask(); - if masked_value != value { - return Err(Overflow::Value(value)); - } - - let index = self.len; - let offset = self.status_size.offset_of(index); - - let (high_shift, low_shift) = offset.left_shift(self.status_size); - - if offset.byte == self.bytes.len() { - self.bytes - .push(masked_value.overflowing_signed_shl(high_shift).0); - } else { - self.bytes[offset.byte] |= masked_value.overflowing_signed_shl(high_shift).0 - } - - if let Some(low_shift) = low_shift { - self.bytes.push(masked_value.overflowing_shl(low_shift).0); - } - - self.len += 1; - Ok(index) - } - /// Returns an iterator over all the statuses stored in this bit-string. - pub fn iter(&self) -> BitStringIter { + pub fn iter(&self, status_size: StatusSize) -> BitStringIter { BitStringIter { bit_string: self, + status_size, index: 0, } } /// Encodes the bit-string. pub fn encode(&self) -> EncodedList { - EncodedList::encode(&self.bytes) + EncodedList::encode(&self.0) } } @@ -468,26 +591,65 @@ pub struct StatusList { } impl StatusList { - pub fn new(status_size: StatusSize, ttl: TimeToLive) -> Self { + pub fn new(ttl: TimeToLive) -> Self { Self { - bit_string: BitString::new(status_size), + bit_string: BitString::new(), ttl, } } - pub fn from_bytes(status_size: StatusSize, bytes: Vec, ttl: TimeToLive) -> Self { + pub fn from_bytes(bytes: Vec, ttl: TimeToLive) -> Self { Self { - bit_string: BitString::from_bytes(status_size, bytes), + bit_string: BitString::from_bytes(bytes), ttl, } } - pub fn is_empty(&self) -> bool { - self.bit_string.is_empty() + pub fn get(&self, status_size: StatusSize, index: usize) -> Option { + self.bit_string.get(status_size, index) } - pub fn len(&self) -> usize { - self.bit_string.len() + pub fn set( + &mut self, + status_size: StatusSize, + index: usize, + value: u8, + ) -> Result { + self.bit_string.set(status_size, index, value) + } + + pub fn iter(&self, status_size: StatusSize) -> BitStringIter { + self.bit_string.iter(status_size) + } + + pub fn to_credential_subject( + &self, + id: Option, + status_purpose: StatusPurpose, + ) -> BitstringStatusList { + BitstringStatusList::new(id, status_purpose, self.bit_string.encode(), self.ttl) + } +} + +#[derive(Debug, Clone)] +pub struct SizedStatusList { + bit_string: SizedBitString, + ttl: TimeToLive, +} + +impl SizedStatusList { + pub fn new(status_size: StatusSize, ttl: TimeToLive) -> Self { + Self { + bit_string: SizedBitString::new(status_size), + ttl, + } + } + + pub fn from_bytes(status_size: StatusSize, bytes: Vec, ttl: TimeToLive) -> Self { + Self { + bit_string: SizedBitString::from_bytes(status_size, bytes), + ttl, + } } pub fn get(&self, index: usize) -> Option { @@ -510,21 +672,21 @@ impl StatusList { &self, id: Option, status_purpose: StatusPurpose, - status_message: Vec, ) -> BitstringStatusList { - BitstringStatusList::new( - id, - status_purpose, - self.bit_string.status_size, - self.bit_string.encode(), - self.ttl, - status_message, - ) + BitstringStatusList::new(id, status_purpose, self.bit_string.encode(), self.ttl) + } + + pub fn into_unsized(self) -> StatusList { + StatusList { + bit_string: self.bit_string.into_unsized(), + ttl: self.ttl, + } } } pub struct BitStringIter<'a> { bit_string: &'a BitString, + status_size: StatusSize, index: usize, } @@ -532,22 +694,32 @@ impl<'a> Iterator for BitStringIter<'a> { type Item = u8; fn next(&mut self) -> Option { - self.bit_string.get(self.index).inspect(|_| { - self.index += 1; - }) + self.bit_string + .get(self.status_size, self.index) + .inspect(|_| { + self.index += 1; + }) } } impl StatusMap for StatusList { type Key = usize; + type StatusSize = StatusSize; type Status = u8; fn time_to_live(&self) -> Option { Some(self.ttl.into()) } - fn get_by_key(&self, key: Self::Key) -> Option { - self.bit_string.get(key).map(Into::into) + fn get_by_key( + &self, + status_size: Option, + key: Self::Key, + ) -> Result, StatusSizeError> { + Ok(self + .bit_string + .get(status_size.ok_or(StatusSizeError::Missing)?, key) + .map(Into::into)) } } @@ -579,20 +751,20 @@ mod tests { use crate::Overflow; - use super::{BitString, StatusSize}; + use super::{SizedBitString, StatusSize}; fn random_bit_string( rng: &mut StdRng, status_size: StatusSize, len: usize, - ) -> (Vec, BitString) { + ) -> (Vec, SizedBitString) { let mut values = Vec::with_capacity(len); for _ in 0..len { values.push((rng.next_u32() & 0xff) as u8 & status_size.mask()) } - let mut bit_string = BitString::new(status_size); + let mut bit_string = SizedBitString::new(status_size); for &s in &values { bit_string.push(s).unwrap(); } @@ -605,7 +777,7 @@ mod tests { let (values, bit_string) = random_bit_string(&mut rng, status_size, len); let encoded = bit_string.encode(); - let decoded = BitString::from_bytes(status_size, encoded.decode(None).unwrap()); + let decoded = SizedBitString::from_bytes(status_size, encoded.decode(None).unwrap()); assert!(decoded.len() >= len); diff --git a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs index 59a12906f..9a31da4b2 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs @@ -4,7 +4,10 @@ use serde::{Deserialize, Serialize}; mod credential; pub use credential::*; -use crate::{bitstring_status_list::StatusPurpose, StatusMapEntry}; +use crate::{ + bitstring_status_list::{StatusMessage, StatusPurpose, StatusSize}, + StatusMapEntry, +}; pub const BITSTRING_STATUS_LIST_ENTRY_TYPE: &str = "BitstringStatusListEntry"; @@ -24,9 +27,24 @@ pub struct BitstringStatusListEntry { /// credential. Must *not* be the URL of the status list. pub id: Option, + /// Size of the status entry in bits. + #[serde(default, skip_serializing_if = "StatusSize::is_default")] + pub status_size: StatusSize, + /// Purpose of the status entry. pub status_purpose: StatusPurpose, + #[serde( + rename = "statusMessage", + default, + skip_serializing_if = "Vec::is_empty" + )] + pub status_messages: Vec, + + /// URL to material related to the status. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_reference: Option, + /// URL to a `BitstringStatusListCredential` verifiable credential. pub status_list_credential: UriBuf, @@ -40,13 +58,18 @@ impl BitstringStatusListEntry { /// Creates a new bit-string status list entry. pub fn new( id: Option, + status_size: StatusSize, status_purpose: StatusPurpose, + status_message: Vec, status_list_credential: UriBuf, status_list_index: usize, ) -> Self { Self { id, + status_size, status_purpose, + status_messages: status_message, + status_reference: None, status_list_credential, status_list_index, } @@ -55,6 +78,7 @@ impl BitstringStatusListEntry { impl StatusMapEntry for BitstringStatusListEntry { type Key = usize; + type StatusSize = StatusSize; fn status_list_url(&self) -> &Uri { &self.status_list_credential @@ -63,6 +87,10 @@ impl StatusMapEntry for BitstringStatusListEntry { fn key(&self) -> Self::Key { self.status_list_index } + + fn status_size(&self) -> Option { + Some(self.status_size) + } } mod base10_nat_string { @@ -77,3 +105,55 @@ mod base10_nat_string { string.parse().map_err(serde::de::Error::custom) } } + +#[cfg(test)] +mod tests { + use super::BitstringStatusListEntry; + use crate::bitstring_status_list::{StatusMessage, StatusPurpose}; + + const ENTRY: &str = r#"{ + "id": "https://example.com/credentials/status/8#492847", + "type": "BitstringStatusListEntry", + "statusPurpose": "message", + "statusListIndex": "492847", + "statusSize": 2, + "statusListCredential": "https://example.com/credentials/status/8", + "statusMessage": [ + {"status":"0x0", "message":"pending_review"}, + {"status":"0x1", "message":"accepted"}, + {"status":"0x2", "message":"rejected"} + ], + "statusReference": "https://example.org/status-dictionary/" + }"#; + + #[test] + fn deserialize() { + serde_json::from_str::(ENTRY).unwrap(); + } + + #[test] + fn serialize() { + let expected: serde_json::Value = serde_json::from_str(ENTRY).unwrap(); + + let status_list = BitstringStatusListEntry { + id: Some( + "https://example.com/credentials/status/8#492847" + .parse() + .unwrap(), + ), + status_size: 2.try_into().unwrap(), + status_purpose: StatusPurpose::Message, + status_list_index: 492847, + status_messages: vec![ + StatusMessage::new(0, "pending_review".to_owned()), + StatusMessage::new(1, "accepted".to_owned()), + StatusMessage::new(2, "rejected".to_owned()), + ], + status_list_credential: "https://example.com/credentials/status/8".parse().unwrap(), + status_reference: Some("https://example.org/status-dictionary/".parse().unwrap()), + }; + + let value = serde_json::to_value(status_list).unwrap(); + assert_eq!(value, expected); + } +} diff --git a/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs index 36c99d7d7..0737a7e08 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs @@ -4,9 +4,7 @@ use serde::{Deserialize, Serialize}; mod credential; pub use credential::*; -use crate::bitstring_status_list::{ - EncodedList, StatusList, StatusMessage, StatusPurpose, StatusSize, TimeToLive, -}; +use crate::bitstring_status_list::{EncodedList, StatusList, StatusPurpose, TimeToLive}; pub const BITSTRING_STATUS_LIST_TYPE: &str = "BitstringStatusList"; @@ -19,68 +17,46 @@ pub struct BitstringStatusList { /// Status purpose. pub status_purpose: StatusPurpose, - #[serde(default, skip_serializing_if = "StatusSize::is_default")] - pub status_size: StatusSize, - /// Encoded status list. pub encoded_list: EncodedList, /// Time to live. - #[serde(default, skip_serializing_if = "TimeToLive::is_default")] - pub ttl: TimeToLive, - - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub status_message: Vec, - - /// URL to material related to the status. #[serde(default, skip_serializing_if = "Option::is_none")] - pub status_reference: Option, + pub ttl: Option, } impl BitstringStatusList { pub fn new( id: Option, status_purpose: StatusPurpose, - status_size: StatusSize, encoded_list: EncodedList, ttl: TimeToLive, - status_message: Vec, ) -> Self { Self { id, status_purpose, - status_size, encoded_list, - ttl, - status_message, - status_reference: None, + ttl: if ttl.is_default() { None } else { Some(ttl) }, } } pub fn decode(&self) -> Result { let bytes = self.encoded_list.decode(None)?; - Ok(StatusList::from_bytes(self.status_size, bytes, self.ttl)) + Ok(StatusList::from_bytes(bytes, self.ttl.unwrap_or_default())) } } #[cfg(test)] mod tests { use super::BitstringStatusList; - use crate::bitstring_status_list::{EncodedList, StatusMessage, StatusPurpose, TimeToLive}; + use crate::bitstring_status_list::{EncodedList, StatusPurpose, TimeToLive}; const STATUS_LIST: &str = r#"{ "id": "https://example.com/status/3#list", "type": "BitstringStatusList", "ttl": 500, - "statusPurpose": "message", - "statusReference": "https://example.org/status-dictionary/", - "statusSize": 2, - "statusMessage": [ - {"status":"0x0", "message":"valid"}, - {"status":"0x1", "message":"invalid"}, - {"status":"0x2", "message":"pending_review"} - ], - "encodedList": "uH4sIAAAAAAAAA-3BMQEAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA" + "statusPurpose": "revocation", + "encodedList": "uH4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA" }"#; #[test] @@ -94,17 +70,10 @@ mod tests { let status_list = BitstringStatusList { id: Some("https://example.com/status/3#list".parse().unwrap()), - ttl: TimeToLive(500), - status_purpose: StatusPurpose::Message, - status_reference: Some("https://example.org/status-dictionary/".parse().unwrap()), - status_size: 2.try_into().unwrap(), - status_message: vec![ - StatusMessage::new(0, "valid".to_owned()), - StatusMessage::new(1, "invalid".to_owned()), - StatusMessage::new(2, "pending_review".to_owned()), - ], + ttl: Some(TimeToLive(500)), + status_purpose: StatusPurpose::Revocation, encoded_list: EncodedList::new( - "uH4sIAAAAAAAAA-3BMQEAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA".to_owned(), + "uH4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA".to_owned(), ), }; diff --git a/crates/status/src/impl/token_status_list/json.rs b/crates/status/src/impl/token_status_list/json.rs index 4ecb6c25d..7749d0d18 100644 --- a/crates/status/src/impl/token_status_list/json.rs +++ b/crates/status/src/impl/token_status_list/json.rs @@ -232,6 +232,7 @@ pub struct StatusListReference { impl StatusMapEntry for StatusListReference { type Key = usize; + type StatusSize = StatusSize; fn key(&self) -> Self::Key { self.idx @@ -240,6 +241,10 @@ impl StatusMapEntry for StatusListReference { fn status_list_url(&self) -> &iref::Uri { &self.uri } + + fn status_size(&self) -> Option { + None + } } #[cfg(test)] diff --git a/crates/status/src/impl/token_status_list/mod.rs b/crates/status/src/impl/token_status_list/mod.rs index b66367990..4a0a97fc4 100644 --- a/crates/status/src/impl/token_status_list/mod.rs +++ b/crates/status/src/impl/token_status_list/mod.rs @@ -27,7 +27,7 @@ use ssi_jwt::{ClaimSet, InvalidClaimValue, JWTClaims, ToDecodedJwt}; use crate::{ EncodedStatusMap, FromBytes, FromBytesOptions, Overflow, StatusMap, StatusMapEntry, - StatusMapEntrySet, + StatusMapEntrySet, StatusSizeError, }; /// Status value describing a Token that is valid, correct or legal. @@ -175,6 +175,12 @@ pub const JWT_TYPE: &str = "statuslist+jwt"; #[error("invalid status size {0}")] pub struct InvalidStatusSize(u8); +impl From for StatusSizeError { + fn from(_value: InvalidStatusSize) -> Self { + Self::Invalid + } +} + /// Number of bits per Referenced Token in a Status List. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] #[serde(transparent)] @@ -216,6 +222,12 @@ impl TryFrom for StatusSize { } } +impl From for u8 { + fn from(value: StatusSize) -> Self { + value.0 + } +} + impl<'de> serde::Deserialize<'de> for StatusSize { fn deserialize(deserializer: D) -> Result where @@ -245,14 +257,19 @@ impl StatusList { impl StatusMap for StatusList { type Key = usize; + type StatusSize = StatusSize; type Status = u8; fn time_to_live(&self) -> Option { self.ttl.map(Duration::from_secs) } - fn get_by_key(&self, key: Self::Key) -> Option { - self.bit_string.get(key) + fn get_by_key( + &self, + _status_size: Option, + key: Self::Key, + ) -> Result, StatusSizeError> { + Ok(self.bit_string.get(key)) } } @@ -591,6 +608,7 @@ pub enum AnyStatusListReference<'a> { impl<'a> StatusMapEntry for AnyStatusListReference<'a> { type Key = usize; + type StatusSize = StatusSize; fn key(&self) -> Self::Key { match self { @@ -603,6 +621,12 @@ impl<'a> StatusMapEntry for AnyStatusListReference<'a> { Self::Json(e) => e.status_list_url(), } } + + fn status_size(&self) -> Option { + match self { + Self::Json(e) => e.status_size(), + } + } } #[cfg(test)] diff --git a/crates/status/src/lib.rs b/crates/status/src/lib.rs index 9cea9b47b..1530e5868 100644 --- a/crates/status/src/lib.rs +++ b/crates/status/src/lib.rs @@ -41,8 +41,18 @@ pub trait FromBytes: Sized { } } +#[derive(Debug, thiserror::Error)] +pub enum StatusSizeError { + #[error("missing status size")] + Missing, + + #[error("invalid status size")] + Invalid, +} + pub trait StatusMap: Clone { type Key; + type StatusSize; type Status; /// Maximum duration an implementer is allowed to cache a @@ -51,10 +61,17 @@ pub trait StatusMap: Clone { None } - fn get_by_key(&self, key: Self::Key) -> Option; - - fn get_entry>(&self, entry: &E) -> Option { - self.get_by_key(entry.key()) + fn get_by_key( + &self, + status_size: Option, + key: Self::Key, + ) -> Result, StatusSizeError>; + + fn get_entry>( + &self, + entry: &E, + ) -> Result, StatusSizeError> { + self.get_by_key(entry.status_size(), entry.key()) } } @@ -68,19 +85,27 @@ pub trait StatusMapEntrySet { pub trait StatusMapEntry { type Key; + type StatusSize; fn status_list_url(&self) -> &Uri; + fn status_size(&self) -> Option; + fn key(&self) -> Self::Key; } impl<'a, E: StatusMapEntry> StatusMapEntry for &'a E { type Key = E::Key; + type StatusSize = E::StatusSize; fn status_list_url(&self) -> &Uri { E::status_list_url(*self) } + fn status_size(&self) -> Option { + E::status_size(*self) + } + fn key(&self) -> Self::Key { E::key(*self) } From 4c45cc2c9e02171a19a07dfe3aa8691f5c9e253f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 5 Sep 2024 16:03:12 +0200 Subject: [PATCH 2/5] Fix doc link. --- crates/status/src/impl/bitstring_status_list/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index c51263778..180e9d9db 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -171,8 +171,8 @@ pub enum StatusPurpose { /// Convey an arbitrary message related to the status of the verifiable /// credential. /// - /// The actual message is stored in the status list credential, in - /// [`BitstringStatusList::status_message`]. + /// The actual message is stored in the status list entry, in + /// [`BitstringStatusListEntry::status_messages`]. Message, } From e5abf36ef5212c8a30a2d9bb928604623eac63ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Fri, 20 Sep 2024 12:31:50 +0200 Subject: [PATCH 3/5] More documentation & other fixes. --- .../src/impl/bitstring_status_list/mod.rs | 17 +++++++++++++++++ .../syntax/entry_set/mod.rs | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index 180e9d9db..7707fba11 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -258,6 +258,13 @@ impl FromStr for StatusPurpose { } } +/// Bit-string with status size. +/// +/// +/// This type is similar to [`BitString`] but also stores the bit size of +/// each item (status size) and the number of items in the list. +/// This provides a safer access to the underlying bit-string, ensuring that +/// status and list boundaries are respected. #[derive(Debug, Clone)] pub struct SizedBitString { inner: BitString, @@ -419,6 +426,10 @@ impl SizedBitString { /// ``` /// /// See: +/// +/// This type does not store the actual status size (the size of each item) +/// nor the total number of items in the list. Use the [`SizedBitString`] type +/// to access the list safely with regard to the items boundaries. #[derive(Debug, Default, Clone)] pub struct BitString(Vec); @@ -631,6 +642,12 @@ impl StatusList { } } +/// Status list with status size. +/// +/// This type is similar to [`StatusList`] but also stores the bit size of +/// each item (status size) and the number of items in the list. +/// This provides a safer access to the underlying bit-string, ensuring that +/// status and list boundaries are respected. #[derive(Debug, Clone)] pub struct SizedStatusList { bit_string: SizedBitString, diff --git a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs index 9a31da4b2..0fc7f767f 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/mod.rs @@ -60,7 +60,7 @@ impl BitstringStatusListEntry { id: Option, status_size: StatusSize, status_purpose: StatusPurpose, - status_message: Vec, + status_messages: Vec, status_list_credential: UriBuf, status_list_index: usize, ) -> Self { @@ -68,7 +68,7 @@ impl BitstringStatusListEntry { id, status_size, status_purpose, - status_messages: status_message, + status_messages, status_reference: None, status_list_credential, status_list_index, From 4378b7f4735cc8635edef8cae2e123fbceb40be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Fri, 20 Sep 2024 13:39:08 +0200 Subject: [PATCH 4/5] Add doc comment on `StatusList` pointing to `SizedStatusList`. --- crates/status/src/impl/bitstring_status_list/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index 7707fba11..46f25e71d 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -595,6 +595,11 @@ impl OverflowingSignedShift for u8 { } } +/// Status list. +/// +/// This type does not store the actual status size (the size of each item) +/// nor the total number of items in the list. Use the [`SizedStatusList`] type +/// to access the list safely with regard to the items boundaries. #[derive(Debug, Clone)] pub struct StatusList { bit_string: BitString, From 7a7e7805fb5c0ae0756810fd9c367a02bea1269d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Fri, 20 Sep 2024 13:59:23 +0200 Subject: [PATCH 5/5] Added some documentation for `StatusMap` and `StatusMapEntry`. --- crates/status/src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/status/src/lib.rs b/crates/status/src/lib.rs index 1530e5868..6de1910f7 100644 --- a/crates/status/src/lib.rs +++ b/crates/status/src/lib.rs @@ -5,6 +5,7 @@ use iref::Uri; pub use r#impl::*; pub mod client; +/// Encoded [`StatusMap`]. pub trait EncodedStatusMap { type Decoded: StatusMap; type DecodeError: std::error::Error; @@ -50,9 +51,19 @@ pub enum StatusSizeError { Invalid, } +/// Status map. +/// +/// A status map is a map from [`StatusMapEntry`] to [`StatusMap::Status`]. +/// The [`StatusMapEntry`] is generally found in the credential or claims you +/// need to verify. pub trait StatusMap: Clone { + /// Key indexing each status in the map. type Key; + + /// Status bit size type. type StatusSize; + + /// Status type. type Status; /// Maximum duration an implementer is allowed to cache a @@ -61,12 +72,18 @@ pub trait StatusMap: Clone { None } + /// Returns a status using the given status size and key. + /// + /// If `status_size` is `None`, it is assumed that the map itself knows the + /// status size. If it does not, a [`StatusSizeError::Missing`] error is + /// returned. fn get_by_key( &self, status_size: Option, key: Self::Key, ) -> Result, StatusSizeError>; + /// Returns the status associated to the given entry. fn get_entry>( &self, entry: &E, @@ -83,14 +100,34 @@ pub trait StatusMapEntrySet { fn get_entry(&self, purpose: StatusPurpose<&str>) -> Option>; } +/// Status map entry. +/// +/// A status map entry is a reference to a particular status in a status map. +/// It links to a status map, providing a key in this map. pub trait StatusMapEntry { + /// Key indexing each status in the referenced status list. type Key; + + /// Status map status size type. type StatusSize; + /// URL to the status map. fn status_list_url(&self) -> &Uri; + /// Size of each status in the status map, if it is known by the entry. + /// + /// For some [`StatusMap`] implementations such as + /// [`crate::token_status_list::StatusList`] the status size is stored in + /// the map, while for some other implementations such as + /// [`crate::bitstring_status_list::StatusList`] the status size is stored + /// in the entry + /// ([`crate::bitstring_status_list::BitstringStatusListEntry`]). + /// + /// If this function returns `None`, it is assumed that the status size + /// will be provided by the status map. fn status_size(&self) -> Option; + /// Entry key. fn key(&self) -> Self::Key; }