identity_credential/revocation/status_list_2021/
status_list.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use flate2::read::GzDecoder;
5use flate2::write::GzEncoder;
6use flate2::Compression;
7use identity_core::convert::Base;
8use identity_core::convert::BaseEncoding;
9use std::io::Write;
10use thiserror::Error;
11
12const MINIMUM_LIST_SIZE: usize = 16 * 1024 * 8;
13
14/// [`std::error::Error`] type for [`StatusList2021`]'s operations.
15#[derive(Debug, Error, PartialEq, Eq, Clone, strum::IntoStaticStr)]
16pub enum StatusListError {
17  /// Requested entry is not in the list.
18  #[error("The requested entry is not in the list.")]
19  IndexOutOfBounds,
20  /// Improperly encoded status list.
21  #[error("\"{0}\" is not a valid encoded status list.")]
22  InvalidEncoding(String),
23  /// Invalid list size
24  #[error("A StatusList2021 must have at least {MINIMUM_LIST_SIZE} entries.")]
25  InvalidListSize,
26}
27
28/// StatusList2021 data structure as described in [W3C's VC status list 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/).
29#[derive(Debug, Clone, Eq, PartialEq, Hash)]
30pub struct StatusList2021(Box<[u8]>);
31
32impl Default for StatusList2021 {
33  fn default() -> Self {
34    StatusList2021::new(MINIMUM_LIST_SIZE).unwrap()
35  }
36}
37
38impl StatusList2021 {
39  /// Returns a new zero-filled [`StatusList2021`] that can hold `num_entries` credential statuses.
40  ///
41  /// ## Notes:
42  /// - The actual length of the list will be rounded up to the closest multiple of 8 to accommodate for byte sizes.
43  /// - `num_entries` must be at least 131,072 which corresponds to a size of 16KB.
44  pub fn new(num_entries: usize) -> Result<Self, StatusListError> {
45    if num_entries < MINIMUM_LIST_SIZE {
46      return Err(StatusListError::InvalidListSize);
47    }
48
49    let size = num_entries / 8 + (num_entries % 8 != 0) as usize;
50    let store = vec![0; size];
51
52    Ok(StatusList2021(store.into_boxed_slice()))
53  }
54
55  /// Returns the number of entries.
56  #[allow(clippy::len_without_is_empty)]
57  pub const fn len(&self) -> usize {
58    self.0.len() * 8
59  }
60
61  /// Returns the status of the entry at `index` without bound checking.
62  /// ## Panic:
63  /// * if `index` is greater than or equal to `self.len()`.
64  const fn get_unchecked(&self, index: usize) -> bool {
65    let (i, offset) = Self::entry_index_to_store_index(index);
66    self.0[i] & (0b1000_0000 >> offset) != 0
67  }
68
69  /// Sets the status of the `index`-th entry to `value`.
70  ///
71  /// ## Panic:
72  /// * if `index` is greater than or equal to `self.len()`.
73  fn set_unchecked(&mut self, index: usize, value: bool) {
74    let (i, offset) = Self::entry_index_to_store_index(index);
75    if value {
76      self.0[i] |= 0b1000_0000 >> offset
77    } else {
78      self.0[i] &= 0b0111_1111 >> offset
79    }
80  }
81
82  /// Returns the status of the `index`-th entry, if it exists.
83  pub fn get(&self, index: usize) -> Result<bool, StatusListError> {
84    (index < self.len())
85      .then_some(self.get_unchecked(index))
86      .ok_or(StatusListError::IndexOutOfBounds)
87  }
88
89  /// Sets the status of the `index`-th entry to `value`.
90  pub fn set(&mut self, index: usize, value: bool) -> Result<(), StatusListError> {
91    if index < self.len() {
92      self.set_unchecked(index, value);
93      Ok(())
94    } else {
95      Err(StatusListError::IndexOutOfBounds)
96    }
97  }
98
99  /// Attempts to parse a [`StatusList2021`] from a string, following the
100  /// [StatusList2021 expansion algorithm](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#bitstring-expansion-algorithm).
101  pub fn try_from_encoded_str(s: &str) -> Result<Self, StatusListError> {
102    let compressed_status_list =
103      BaseEncoding::decode(s, Base::Base64).or(Err(StatusListError::InvalidEncoding(s.to_owned())))?;
104    let status_list = {
105      use std::io::Read;
106
107      let mut decompressor = GzDecoder::new(&compressed_status_list[..]);
108      let mut status_list = vec![];
109      decompressor
110        .read_to_end(&mut status_list)
111        .or(Err(StatusListError::InvalidEncoding(s.to_owned())))?;
112
113      StatusList2021(status_list.into_boxed_slice())
114    };
115
116    Ok(status_list)
117  }
118
119  /// Encode this [`StatusList2021`] into its string representation following
120  /// [StatusList2021 generation algorithm](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#bitstring-generation-algorithm).
121  pub fn into_encoded_str(self) -> String {
122    let compressed_status_list = {
123      let mut compressor = GzEncoder::new(vec![], Compression::best());
124      compressor.write_all(&self.0).unwrap();
125      compressor.finish().unwrap()
126    };
127
128    BaseEncoding::encode(&compressed_status_list[..], Base::Base64)
129  }
130
131  /// Returns the byte location and the bit location within it.
132  const fn entry_index_to_store_index(index: usize) -> (usize, usize) {
133    (index / 8, index % 8)
134  }
135}
136
137#[cfg(test)]
138mod tests {
139  use super::*;
140
141  #[test]
142  fn default_status_list() {
143    let mut status_list = StatusList2021::default();
144    status_list.set(131071, true).unwrap();
145    assert!(status_list.get(131071).unwrap());
146    assert_eq!(status_list.set(131072, true), Err(StatusListError::IndexOutOfBounds));
147  }
148
149  #[test]
150  fn status_list_too_short_fails() {
151    assert_eq!(StatusList2021::new(100), Err(StatusListError::InvalidListSize));
152  }
153
154  #[test]
155  fn status_list_entry_access() {
156    let mut status_list = StatusList2021::default();
157    status_list.set(42, true).unwrap();
158    assert!(status_list.get(42).unwrap());
159
160    status_list.set(42, false).unwrap();
161    assert_eq!(status_list, StatusList2021::default());
162  }
163
164  #[test]
165  fn status_list_encode_decode() {
166    let mut status_list = StatusList2021::default();
167    status_list.set(42, true).unwrap();
168    status_list.set(420, true).unwrap();
169    status_list.set(4200, true).unwrap();
170    let encoded = status_list.clone().into_encoded_str();
171    let decoded = StatusList2021::try_from_encoded_str(&encoded).unwrap();
172    assert_eq!(decoded, status_list);
173  }
174}