chronix_encoding/
bitmap.rs1use crate::coding::{checked_count, checked_decode_count};
19use crate::error::{EncodingError, Result};
20
21#[derive(Debug, Clone, Copy)]
23pub struct BitmapEncoder;
24
25#[derive(Debug, Clone, Copy)]
27pub struct BitmapDecoder;
28
29impl BitmapEncoder {
30 pub fn encode(values: &[bool]) -> Result<Vec<u8>> {
36 if values.is_empty() {
37 return Err(EncodingError::EmptyInput {
38 context: "bitmap encoder",
39 });
40 }
41
42 let count = checked_count(values.len())?;
43 let bitmap_bytes = values.len().div_ceil(8);
44 let mut buf = Vec::with_capacity(4 + 1 + bitmap_bytes);
45
46 buf.extend_from_slice(&count.to_le_bytes());
48 buf.push(0);
50 buf.resize(4 + 1 + bitmap_bytes, 0);
52
53 for (i, &val) in values.iter().enumerate() {
54 if val {
55 let byte_idx = i / 8;
56 let bit_idx = 7 - (i % 8); buf[5 + byte_idx] |= 1 << bit_idx;
58 }
59 }
60
61 Ok(buf)
62 }
63
64 pub fn encode_nullable(values: &[Option<bool>]) -> Result<Vec<u8>> {
72 if values.is_empty() {
73 return Err(EncodingError::EmptyInput {
74 context: "bitmap encoder (nullable)",
75 });
76 }
77
78 let has_nulls = values.iter().any(Option::is_none);
79 let bitmap_bytes = values.len().div_ceil(8);
80
81 let total_size = 4 + 1 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
82 let count = checked_count(values.len())?;
83 let mut buf = vec![0u8; total_size];
84
85 buf[..4].copy_from_slice(&count.to_le_bytes());
87 buf[4] = u8::from(has_nulls);
89
90 for (i, val) in values.iter().enumerate() {
92 if val.unwrap_or(false) {
93 let byte_idx = i / 8;
94 let bit_idx = 7 - (i % 8);
95 buf[5 + byte_idx] |= 1 << bit_idx;
96 }
97 }
98
99 if has_nulls {
101 let null_offset = 5 + bitmap_bytes;
102 for (i, val) in values.iter().enumerate() {
103 if val.is_some() {
104 let byte_idx = i / 8;
105 let bit_idx = 7 - (i % 8);
106 buf[null_offset + byte_idx] |= 1 << bit_idx;
107 }
108 }
109 }
110
111 Ok(buf)
112 }
113}
114
115impl BitmapDecoder {
116 pub fn decode(data: &[u8]) -> Result<Vec<bool>> {
122 if data.len() < 5 {
123 return Err(EncodingError::CorruptData {
124 detail: "bitmap data too short for header".to_string(),
125 });
126 }
127
128 let count = checked_decode_count(
129 u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
130 "bitmap",
131 )?;
132 let has_nulls = data[4];
133 if has_nulls != 0 {
134 return Err(EncodingError::CorruptData {
135 detail: "non-nullable decode called on nullable bitmap data".to_string(),
136 });
137 }
138
139 let bitmap_bytes = count.div_ceil(8);
140 if data.len() < 5 + bitmap_bytes {
141 return Err(EncodingError::CorruptData {
142 detail: "bitmap data truncated".to_string(),
143 });
144 }
145
146 let mut result = Vec::with_capacity(count);
147 for i in 0..count {
148 let byte_idx = i / 8;
149 let bit_idx = 7 - (i % 8);
150 let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
151 result.push(val);
152 }
153
154 Ok(result)
155 }
156
157 pub fn decode_nullable(data: &[u8]) -> Result<Vec<Option<bool>>> {
163 if data.len() < 5 {
164 return Err(EncodingError::CorruptData {
165 detail: "bitmap data too short for header".to_string(),
166 });
167 }
168
169 let count = checked_decode_count(
170 u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
171 "bitmap",
172 )?;
173 let has_nulls = data[4] != 0;
174
175 let bitmap_bytes = count.div_ceil(8);
176 let expected_len = 5 + bitmap_bytes + if has_nulls { bitmap_bytes } else { 0 };
177 if data.len() < expected_len {
178 return Err(EncodingError::CorruptData {
179 detail: "nullable bitmap data truncated".to_string(),
180 });
181 }
182
183 let mut result = Vec::with_capacity(count);
184 let null_offset = 5 + bitmap_bytes;
185
186 for i in 0..count {
187 let byte_idx = i / 8;
188 let bit_idx = 7 - (i % 8);
189
190 let is_valid = if has_nulls {
191 (data[null_offset + byte_idx] >> bit_idx) & 1 == 1
192 } else {
193 true
194 };
195
196 if is_valid {
197 let val = (data[5 + byte_idx] >> bit_idx) & 1 == 1;
198 result.push(Some(val));
199 } else {
200 result.push(None);
201 }
202 }
203
204 Ok(result)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn roundtrip_all_true() {
214 let values = vec![true; 100];
215 let encoded = BitmapEncoder::encode(&values).unwrap();
216 let decoded = BitmapDecoder::decode(&encoded).unwrap();
217 assert_eq!(values, decoded);
218 }
219
220 #[test]
221 fn roundtrip_all_false() {
222 let values = vec![false; 100];
223 let encoded = BitmapEncoder::encode(&values).unwrap();
224 let decoded = BitmapDecoder::decode(&encoded).unwrap();
225 assert_eq!(values, decoded);
226 }
227
228 #[test]
229 fn roundtrip_alternating() {
230 let values: Vec<bool> = (0..100).map(|i| i % 2 == 0).collect();
231 let encoded = BitmapEncoder::encode(&values).unwrap();
232 let decoded = BitmapDecoder::decode(&encoded).unwrap();
233 assert_eq!(values, decoded);
234 }
235
236 #[test]
237 fn roundtrip_non_aligned() {
238 let values = vec![
240 true, false, true, true, false, false, true, false, true, false, true, true, false,
241 ];
242 let encoded = BitmapEncoder::encode(&values).unwrap();
243 let decoded = BitmapDecoder::decode(&encoded).unwrap();
244 assert_eq!(values, decoded);
245 }
246
247 #[test]
248 fn roundtrip_nullable() {
249 let values = vec![Some(true), Some(false), None, Some(true), None, Some(false)];
250 let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
251 let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
252 assert_eq!(values, decoded);
253 }
254
255 #[test]
256 fn roundtrip_nullable_no_nulls() {
257 let values = vec![Some(true), Some(false), Some(true)];
258 let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
259 let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
260 assert_eq!(values, decoded);
261 }
262
263 #[test]
264 fn roundtrip_nullable_all_null() {
265 let values = vec![None, None, None];
266 let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
267 let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
268 assert_eq!(values, decoded);
269 }
270
271 #[test]
272 fn empty_error() {
273 assert!(BitmapEncoder::encode(&[]).is_err());
274 let vals: Vec<Option<bool>> = vec![];
275 assert!(BitmapEncoder::encode_nullable(&vals).is_err());
276 }
277
278 #[test]
279 fn compression_ratio() {
280 let values = vec![true; 10_000];
281 let raw_size = values.len(); let encoded = BitmapEncoder::encode(&values).unwrap();
283 #[allow(clippy::cast_precision_loss)]
284 let ratio = raw_size as f64 / encoded.len() as f64;
285 assert!(ratio > 6.0, "Expected ratio > 6x, got {ratio:.1}x");
287 }
288
289 #[test]
290 fn single_value() {
291 let encoded = BitmapEncoder::encode(&[true]).unwrap();
292 let decoded = BitmapDecoder::decode(&encoded).unwrap();
293 assert_eq!(vec![true], decoded);
294
295 let encoded = BitmapEncoder::encode(&[false]).unwrap();
296 let decoded = BitmapDecoder::decode(&encoded).unwrap();
297 assert_eq!(vec![false], decoded);
298 }
299}
300
301#[cfg(test)]
302mod proptests {
303 use super::*;
304 use proptest::prelude::*;
305
306 proptest! {
307 #[test]
308 fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..500)) {
309 let encoded = BitmapEncoder::encode(&values).unwrap();
310 let decoded = BitmapDecoder::decode(&encoded).unwrap();
311 prop_assert_eq!(&values, &decoded);
312 }
313
314 #[test]
315 fn roundtrip_nullable(values in proptest::collection::vec(any::<Option<bool>>(), 1..500)) {
316 let encoded = BitmapEncoder::encode_nullable(&values).unwrap();
317 let decoded = BitmapDecoder::decode_nullable(&encoded).unwrap();
318 prop_assert_eq!(&values, &decoded);
319 }
320 }
321}