1use std::io::Read;
15
16use base64::Engine;
17use flate2::read::GzDecoder;
18
19#[derive(Debug, Clone)]
21pub struct StatusList {
22 bits: Vec<u8>,
23}
24
25impl StatusList {
26 pub fn from_encoded_list(encoded: &str) -> anyhow::Result<Self> {
29 let b64 = encoded.strip_prefix('u').unwrap_or(encoded);
30 let compressed = base64::engine::general_purpose::URL_SAFE_NO_PAD
31 .decode(b64)
32 .map_err(|e| anyhow::anyhow!("status list base64url: {e}"))?;
33 let mut bits = Vec::new();
34 GzDecoder::new(&compressed[..])
35 .read_to_end(&mut bits)
36 .map_err(|e| anyhow::anyhow!("status list gunzip: {e}"))?;
37 Ok(Self { bits })
38 }
39
40 pub fn from_bitstring(bits: Vec<u8>) -> Self {
42 Self { bits }
43 }
44
45 pub fn len_bits(&self) -> usize {
47 self.bits.len() * 8
48 }
49
50 pub fn get(&self, index: usize) -> Option<bool> {
54 let byte = self.bits.get(index / 8)?;
55 Some(byte & (0x80 >> (index % 8)) != 0)
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use flate2::{Compression, write::GzEncoder};
63 use std::io::Write;
64
65 fn encode_list(bits: &[u8]) -> String {
66 let mut enc = GzEncoder::new(Vec::new(), Compression::default());
67 enc.write_all(bits).unwrap();
68 let gz = enc.finish().unwrap();
69 format!(
70 "u{}",
71 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(gz)
72 )
73 }
74
75 #[test]
76 fn bit_indexing_is_big_endian_within_byte() {
77 let list = StatusList::from_bitstring(vec![0b1000_0001]);
78 assert_eq!(list.get(0), Some(true));
79 assert_eq!(list.get(1), Some(false));
80 assert_eq!(list.get(7), Some(true));
81 assert_eq!(list.get(8), None, "out-of-range index is None");
82 }
83
84 #[test]
85 fn encoded_list_round_trips() {
86 let bits = vec![0b0000_0100, 0b0000_0000];
87 let encoded = encode_list(&bits);
88 let list = StatusList::from_encoded_list(&encoded).expect("decode");
89 assert_eq!(list.get(5), Some(true));
90 assert_eq!(list.get(4), Some(false));
91 assert_eq!(list.get(6), Some(false));
92 assert_eq!(list.len_bits(), 16);
93 }
94
95 #[test]
96 fn decodes_without_multibase_prefix() {
97 let bits = vec![0b1000_0000];
98 let encoded = encode_list(&bits);
99 let without_u = encoded.strip_prefix('u').unwrap();
100 let list = StatusList::from_encoded_list(without_u).expect("decode");
101 assert_eq!(list.get(0), Some(true));
102 }
103
104 #[test]
105 fn garbage_is_an_error_not_a_panic() {
106 assert!(StatusList::from_encoded_list("u!!!not-base64!!!").is_err());
107 assert!(StatusList::from_encoded_list("udGhpcyBpcyBub3QgZ3ppcA").is_err());
108 }
109}