use crate::coding::{checked_count, checked_decode_count};
use crate::error::{EncodingError, Result};
#[derive(Debug, Clone, Copy)]
pub struct BitmapEncoder;
#[derive(Debug, Clone, Copy)]
pub struct BitmapDecoder;
impl BitmapEncoder {
pub fn encode(values: &[bool]) -> Result<Vec<u8>> {
if values.is_empty() {
return Err(EncodingError::EmptyInput {
context: "bitmap encoder",
});
}
let count = checked_count(values.len())?;
let bitmap_bytes = values.len().div_ceil(8);
let mut buf = Vec::with_capacity(4 + 1 + bitmap_bytes);
buf.extend_from_slice(&count.to_le_bytes());
buf.push(0);
buf.resize(4 + 1 + bitmap_bytes, 0);
for (i, &val) in values.iter().enumerate() {
if val {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8); buf[5 + byte_idx] |= 1 << bit_idx;
}
}
Ok(buf)
}
pub fn encode_nullable(values: &[Option<bool>]) -> Result<Vec<u8>> {
if values.is_empty() {
return Err(EncodingError::EmptyInput {
context: "bitmap encoder (nullable)",
});
}
let has_nulls = values.iter().any(Option::is_none);
let bitmap_bytes = values.len().div_ceil(8);
let total_size = 4 + 1 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
let count = checked_count(values.len())?;
let mut buf = vec![0u8; total_size];
buf[..4].copy_from_slice(&count.to_le_bytes());
buf[4] = u8::from(has_nulls);
for (i, val) in values.iter().enumerate() {
if val.unwrap_or(false) {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
buf[5 + byte_idx] |= 1 << bit_idx;
}
}
if has_nulls {
let null_offset = 5 + bitmap_bytes;
for (i, val) in values.iter().enumerate() {
if val.is_some() {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
buf[null_offset + byte_idx] |= 1 << bit_idx;
}
}
}
Ok(buf)
}
}
impl BitmapDecoder {
pub fn decode(data: &[u8]) -> Result<Vec<bool>> {
if data.len() < 5 {
return Err(EncodingError::CorruptData {
detail: "bitmap data too short for header".to_string(),
});
}
let count = checked_decode_count(
u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
"bitmap",
)?;
let has_nulls = data[4];
if has_nulls != 0 {
return Err(EncodingError::CorruptData {
detail: "non-nullable decode called on nullable bitmap data".to_string(),
});
}
let bitmap_bytes = count.div_ceil(8);
if data.len() < 5 + bitmap_bytes {
return Err(EncodingError::CorruptData {
detail: "bitmap data truncated".to_string(),
});
}
let mut result = Vec::with_capacity(count);
for i in 0..count {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
result.push(val);
}
Ok(result)
}
pub fn decode_nullable(data: &[u8]) -> Result<Vec<Option<bool>>> {
if data.len() < 5 {
return Err(EncodingError::CorruptData {
detail: "bitmap data too short for header".to_string(),
});
}
let count = checked_decode_count(
u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
"bitmap",
)?;
let has_nulls = data[4] != 0;
let bitmap_bytes = count.div_ceil(8);
let expected_len = 5 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
if data.len() < expected_len {
return Err(EncodingError::CorruptData {
detail: "nullable bitmap data truncated".to_string(),
});
}
let mut result = Vec::with_capacity(count);
let null_offset = 5 + bitmap_bytes;
for i in 0..count {
let byte_idx = i / 8;
let bit_idx = 7 - (i % 8);
let is_valid = if has_nulls {
(data[null_offset + byte_idx] >> bit_idx) & 1 == 1
} else {
true
};
if is_valid {
let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
result.push(Some(val));
} else {
result.push(None);
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_all_true() {
let values = vec![true; 100];
let encoded = BitmapEncoder::encode(&values).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_all_false() {
let values = vec![false; 100];
let encoded = BitmapEncoder::encode(&values).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_alternating() {
let values: Vec<bool> = (0..100).map(|i| i % 2 == 0).collect();
let encoded = BitmapEncoder::encode(&values).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_non_aligned() {
let values = vec![
true, false, true, true, false, false, true, false, true, false, true, true, false,
];
let encoded = BitmapEncoder::encode(&values).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_nullable() {
let values = vec![Some(true), Some(false), None, Some(true), None, Some(false)];
let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_nullable_no_nulls() {
let values = vec![Some(true), Some(false), Some(true)];
let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn roundtrip_nullable_all_null() {
let values = vec![None, None, None];
let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
assert_eq!(values, decoded);
}
#[test]
fn empty_error() {
assert!(BitmapEncoder::encode(&[]).is_err());
let vals: Vec<Option<bool>> = vec![];
assert!(BitmapEncoder::encode_nullable(&vals).is_err());
}
#[test]
fn compression_ratio() {
let values = vec![true; 10_000];
let raw_size = values.len(); let encoded = BitmapEncoder::encode(&values).unwrap();
#[allow(clippy::cast_precision_loss)]
let ratio = raw_size as f64 / encoded.len() as f64;
assert!(ratio > 6.0, "Expected ratio > 6x, got {ratio:.1}x");
}
#[test]
fn single_value() {
let encoded = BitmapEncoder::encode(&[true]).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(vec![true], decoded);
let encoded = BitmapEncoder::encode(&[false]).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
assert_eq!(vec![false], decoded);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..500)) {
let encoded = BitmapEncoder::encode(&values).unwrap();
let decoded = BitmapDecoder::decode(&encoded).unwrap();
prop_assert_eq!(&values, &decoded);
}
#[test]
fn roundtrip_nullable(values in proptest::collection::vec(any::<Option<bool>>(), 1..500)) {
let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
prop_assert_eq!(&values, &decoded);
}
}
}