use std::io::Read;
use base64::Engine;
use flate2::read::GzDecoder;
#[derive(Debug, Clone)]
pub struct StatusList {
bits: Vec<u8>,
}
impl StatusList {
pub fn from_encoded_list(encoded: &str) -> anyhow::Result<Self> {
let b64 = encoded.strip_prefix('u').unwrap_or(encoded);
let compressed = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(b64)
.map_err(|e| anyhow::anyhow!("status list base64url: {e}"))?;
let mut bits = Vec::new();
GzDecoder::new(&compressed[..])
.read_to_end(&mut bits)
.map_err(|e| anyhow::anyhow!("status list gunzip: {e}"))?;
Ok(Self { bits })
}
pub fn from_bitstring(bits: Vec<u8>) -> Self {
Self { bits }
}
pub fn len_bits(&self) -> usize {
self.bits.len() * 8
}
pub fn get(&self, index: usize) -> Option<bool> {
let byte = self.bits.get(index / 8)?;
Some(byte & (0x80 >> (index % 8)) != 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::{Compression, write::GzEncoder};
use std::io::Write;
fn encode_list(bits: &[u8]) -> String {
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
enc.write_all(bits).unwrap();
let gz = enc.finish().unwrap();
format!(
"u{}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(gz)
)
}
#[test]
fn bit_indexing_is_big_endian_within_byte() {
let list = StatusList::from_bitstring(vec![0b1000_0001]);
assert_eq!(list.get(0), Some(true));
assert_eq!(list.get(1), Some(false));
assert_eq!(list.get(7), Some(true));
assert_eq!(list.get(8), None, "out-of-range index is None");
}
#[test]
fn encoded_list_round_trips() {
let bits = vec![0b0000_0100, 0b0000_0000];
let encoded = encode_list(&bits);
let list = StatusList::from_encoded_list(&encoded).expect("decode");
assert_eq!(list.get(5), Some(true));
assert_eq!(list.get(4), Some(false));
assert_eq!(list.get(6), Some(false));
assert_eq!(list.len_bits(), 16);
}
#[test]
fn decodes_without_multibase_prefix() {
let bits = vec![0b1000_0000];
let encoded = encode_list(&bits);
let without_u = encoded.strip_prefix('u').unwrap();
let list = StatusList::from_encoded_list(without_u).expect("decode");
assert_eq!(list.get(0), Some(true));
}
#[test]
fn garbage_is_an_error_not_a_panic() {
assert!(StatusList::from_encoded_list("u!!!not-base64!!!").is_err());
assert!(StatusList::from_encoded_list("udGhpcyBpcyBub3QgZ3ppcA").is_err());
}
}