Skip to main content

nodedb_codec/fastlanes/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Whole-stream encode / decode for the FastLanes FOR + bit-packing codec.
4
5use std::mem::size_of;
6
7use crate::bounds::{checked_add, checked_capacity, checked_mul, decoded_len, encode_input_len};
8use crate::error::CodecError;
9
10use super::block::{decode_block, encode_block};
11use super::header::{GLOBAL_HEADER_SIZE, expected_block_count, parse_header};
12
13/// Encode a slice of i64 values using FOR + bit-packing.
14pub fn encode(values: &[i64]) -> Result<Vec<u8>, CodecError> {
15    let total_bytes = checked_mul(values.len(), size_of::<i64>(), "FastLanes input bytes")?;
16    decoded_len(total_bytes, "FastLanes input")?;
17    let total_count = encode_input_len(values.len(), "FastLanes value count")?;
18    let block_count = values.len().div_ceil(super::header::BLOCK_SIZE);
19    let block_count = u16::try_from(block_count).map_err(|_| CodecError::ResourceLimit {
20        resource: "FastLanes block count".into(),
21        requested: block_count,
22        limit: u16::MAX as usize,
23    })?;
24    let estimated = checked_add(
25        GLOBAL_HEADER_SIZE,
26        checked_mul(values.len(), 5, "FastLanes output estimate")?,
27        "FastLanes output estimate",
28    )?;
29    let mut out = Vec::with_capacity(estimated);
30    out.extend_from_slice(&total_count.to_le_bytes());
31    out.extend_from_slice(&block_count.to_le_bytes());
32    for chunk in values.chunks(super::header::BLOCK_SIZE) {
33        encode_block(chunk, &mut out)?;
34    }
35    Ok(out)
36}
37
38/// Decode FOR + bit-packed bytes back to i64 values.
39pub fn decode(data: &[u8]) -> Result<Vec<i64>, CodecError> {
40    if data.len() < GLOBAL_HEADER_SIZE {
41        return Err(CodecError::Truncated {
42            expected: GLOBAL_HEADER_SIZE,
43            actual: data.len(),
44        });
45    }
46
47    let (total_count, block_count) = parse_header(data)?;
48    if total_count == 0 {
49        if data.len() != GLOBAL_HEADER_SIZE {
50            return Err(CodecError::Corrupt {
51                detail: "trailing bytes after empty FastLanes frame".into(),
52            });
53        }
54        return Ok(Vec::new());
55    }
56
57    let value_capacity = checked_capacity(total_count, size_of::<i64>(), "FastLanes values")?;
58    let mut values = Vec::with_capacity(value_capacity);
59    let mut offset = GLOBAL_HEADER_SIZE;
60    for block_idx in 0..block_count {
61        offset = decode_block(
62            data,
63            offset,
64            &mut values,
65            block_idx,
66            expected_block_count(total_count, block_idx)?,
67        )?;
68    }
69    if offset != data.len() {
70        return Err(CodecError::Corrupt {
71            detail: "trailing bytes after FastLanes frame".into(),
72        });
73    }
74    if values.len() != total_count {
75        return Err(CodecError::Corrupt {
76            detail: format!(
77                "value count mismatch: header says {total_count}, decoded {}",
78                values.len()
79            ),
80        });
81    }
82
83    Ok(values)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::super::bits;
89    use super::super::block;
90    use super::super::iterator::BlockIterator;
91    use super::super::range::{
92        block_byte_offsets, block_count, decode_block_range, decode_single_block,
93    };
94    use super::*;
95    use crate::fastlanes::bit_width_for_range;
96
97    fn encode(values: &[i64]) -> Vec<u8> {
98        super::encode(values).expect("test FastLanes encode")
99    }
100
101    #[test]
102    fn empty_roundtrip() {
103        let encoded = encode(&[]);
104        let decoded = decode(&encoded).unwrap();
105        assert!(decoded.is_empty());
106    }
107
108    #[test]
109    fn single_value() {
110        let encoded = encode(&[42i64]);
111        let decoded = decode(&encoded).unwrap();
112        assert_eq!(decoded, vec![42i64]);
113    }
114
115    #[test]
116    fn identical_values_zero_bits() {
117        let values = vec![999i64; 1024];
118        let encoded = encode(&values);
119        let decoded = decode(&encoded).unwrap();
120        assert_eq!(decoded, values);
121
122        // All identical → bit_width=0 → only headers, no packed data.
123        // Global header(6) + block header(11) = 17 bytes for 1024 values.
124        assert_eq!(encoded.len(), 17);
125    }
126
127    #[test]
128    fn small_range_values() {
129        // Values in range [100, 107] → 3 bits per value.
130        let values: Vec<i64> = (0..1024).map(|i| 100 + (i % 8)).collect();
131        let encoded = encode(&values);
132        let decoded = decode(&encoded).unwrap();
133        assert_eq!(decoded, values);
134
135        // 1024 values × 3 bits = 384 bytes packed + headers.
136        let expected_packed = (1024usize * 3).div_ceil(8); // 384 bytes
137        let expected_total = GLOBAL_HEADER_SIZE + block::BLOCK_HEADER_SIZE + expected_packed;
138        assert_eq!(encoded.len(), expected_total);
139    }
140
141    #[test]
142    fn constant_rate_timestamps() {
143        let values: Vec<i64> = (0..10_000)
144            .map(|i| 1_700_000_000_000 + i * 10_000)
145            .collect();
146        let encoded = encode(&values);
147        let decoded = decode(&encoded).unwrap();
148        assert_eq!(decoded, values);
149
150        let bytes_per_sample = encoded.len() as f64 / values.len() as f64;
151        assert!(
152            bytes_per_sample < 4.0,
153            "timestamps should pack to <4 bytes/sample, got {bytes_per_sample:.2}"
154        );
155    }
156
157    #[test]
158    fn pre_delta_timestamps() {
159        let deltas: Vec<i64> = vec![10_000i64; 10_000];
160        let encoded = encode(&deltas);
161        let decoded = decode(&encoded).unwrap();
162        assert_eq!(decoded, deltas);
163
164        let bytes_per_sample = encoded.len() as f64 / deltas.len() as f64;
165        assert!(
166            bytes_per_sample < 0.2,
167            "constant deltas should pack to near-zero, got {bytes_per_sample:.2}"
168        );
169    }
170
171    #[test]
172    fn pre_delta_timestamps_with_jitter() {
173        let mut deltas = Vec::with_capacity(10_000);
174        let mut rng: u64 = 42;
175        for _ in 0..10_000 {
176            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
177            let jitter = ((rng >> 33) as i64 % 101) - 50;
178            deltas.push(10_000 + jitter);
179        }
180        let encoded = encode(&deltas);
181        let decoded = decode(&encoded).unwrap();
182        assert_eq!(decoded, deltas);
183
184        let bytes_per_sample = encoded.len() as f64 / deltas.len() as f64;
185        assert!(
186            bytes_per_sample < 1.5,
187            "jittered deltas should pack to <1.5 bytes/sample, got {bytes_per_sample:.2}"
188        );
189    }
190
191    #[test]
192    fn negative_values() {
193        let values: Vec<i64> = (-500..500).collect();
194        let encoded = encode(&values);
195        let decoded = decode(&encoded).unwrap();
196        assert_eq!(decoded, values);
197    }
198
199    #[test]
200    fn boundary_values() {
201        let values = vec![i64::MIN, 0, i64::MAX];
202        let encoded = encode(&values);
203        let decoded = decode(&encoded).unwrap();
204        assert_eq!(decoded, values);
205    }
206
207    #[test]
208    fn multiple_blocks() {
209        let values: Vec<i64> = (0..3000).map(|i| i * 7 + 100).collect();
210        let encoded = encode(&values);
211        let decoded = decode(&encoded).unwrap();
212        assert_eq!(decoded, values);
213    }
214
215    #[test]
216    fn partial_last_block() {
217        let values: Vec<i64> = (0..1025).collect();
218        let encoded = encode(&values);
219        let decoded = decode(&encoded).unwrap();
220        assert_eq!(decoded, values);
221    }
222
223    #[test]
224    fn compression_vs_raw() {
225        let values: Vec<i64> = (0..10_000)
226            .map(|i| 1_700_000_000_000 + i * 10_000)
227            .collect();
228        let encoded = encode(&values);
229        let raw_size = values.len() * 8;
230        let ratio = raw_size as f64 / encoded.len() as f64;
231        assert!(ratio > 2.0, "expected >2x compression, got {ratio:.1}x");
232    }
233
234    #[test]
235    fn bit_width_calculation() {
236        assert_eq!(bit_width_for_range(0, 0), 0);
237        assert_eq!(bit_width_for_range(100, 100), 0);
238        assert_eq!(bit_width_for_range(0, 1), 1);
239        assert_eq!(bit_width_for_range(0, 7), 3);
240        assert_eq!(bit_width_for_range(0, 8), 4);
241        assert_eq!(bit_width_for_range(0, 255), 8);
242        assert_eq!(bit_width_for_range(0, 256), 9);
243        assert_eq!(bit_width_for_range(i64::MIN, i64::MAX), 64);
244    }
245
246    #[test]
247    fn pack_unpack_roundtrip() {
248        for bw in 1..=64u8 {
249            let max_val: u64 = if bw == 64 { u64::MAX } else { (1u64 << bw) - 1 };
250            let test_vals = [0u64, 1, max_val / 2, max_val];
251            for &val in &test_vals {
252                let mut packed = vec![0u8; 16];
253                bits::pack_bits(&mut packed, 0, val, bw);
254                let unpacked = bits::unpack_bits(&packed, 0, bw);
255                let mask = if bw == 64 { u64::MAX } else { (1u64 << bw) - 1 };
256                assert_eq!(
257                    unpacked & mask,
258                    val & mask,
259                    "pack/unpack failed for bw={bw}, val={val}"
260                );
261            }
262        }
263    }
264
265    #[test]
266    fn pack_unpack_at_offsets() {
267        let mut packed = vec![0u8; 32];
268        bits::pack_bits(&mut packed, 0, 0b101, 3);
269        bits::pack_bits(&mut packed, 3, 0b110, 3);
270        bits::pack_bits(&mut packed, 6, 0b011, 3);
271
272        assert_eq!(bits::unpack_bits(&packed, 0, 3), 0b101);
273        assert_eq!(bits::unpack_bits(&packed, 3, 3), 0b110);
274        assert_eq!(bits::unpack_bits(&packed, 6, 3), 0b011);
275    }
276
277    #[test]
278    fn truncated_input_errors() {
279        assert!(decode(&[]).is_err());
280        assert!(decode(&[1, 0, 0, 0, 1, 0]).is_err()); // count=1, blocks=1, no block data
281    }
282
283    #[test]
284    fn large_dataset_roundtrip() {
285        let mut values = Vec::with_capacity(100_000);
286        let mut rng: u64 = 12345;
287        for _ in 0..100_000 {
288            rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
289            values.push((rng >> 1) as i64);
290        }
291        let encoded = encode(&values);
292        let decoded = decode(&encoded).unwrap();
293        assert_eq!(decoded, values);
294    }
295
296    #[test]
297    fn decode_single_block_correctness() {
298        let values: Vec<i64> = (0..3000).collect();
299        let encoded = encode(&values);
300        assert_eq!(block_count(&encoded).unwrap(), 3);
301
302        let b0 = decode_single_block(&encoded, 0).unwrap();
303        assert_eq!(b0.len(), 1024);
304        assert_eq!(b0, &values[..1024]);
305
306        let b1 = decode_single_block(&encoded, 1).unwrap();
307        assert_eq!(b1.len(), 1024);
308        assert_eq!(b1, &values[1024..2048]);
309
310        let b2 = decode_single_block(&encoded, 2).unwrap();
311        assert_eq!(b2.len(), 952);
312        assert_eq!(b2, &values[2048..]);
313    }
314
315    #[test]
316    fn block_iterator_matches_full_decode() {
317        let values: Vec<i64> = (0..5000).map(|i| i * 7 - 2000).collect();
318        let encoded = encode(&values);
319
320        let mut all = Vec::new();
321        let iter = BlockIterator::new(&encoded).unwrap();
322        for blk in iter {
323            all.extend(blk.unwrap());
324        }
325        assert_eq!(all, values);
326    }
327
328    #[test]
329    fn block_iterator_skip() {
330        let values: Vec<i64> = (0..3000).collect();
331        let encoded = encode(&values);
332
333        let mut iter = BlockIterator::new(&encoded).unwrap();
334        iter.skip_block().unwrap(); // skip block 0
335        let b1 = iter.next().unwrap().unwrap();
336        assert_eq!(b1, &values[1024..2048]);
337    }
338
339    #[test]
340    fn hostile_counts_and_block_shapes_fail_before_allocation_or_looping() {
341        let mut huge = vec![0; GLOBAL_HEADER_SIZE];
342        huge[..4].copy_from_slice(&u32::MAX.to_le_bytes());
343        huge[4..].copy_from_slice(&u16::MAX.to_le_bytes());
344        assert!(matches!(
345            decode(&huge),
346            Err(CodecError::ResourceLimit { .. })
347        ));
348
349        let mut mismatched = vec![0; GLOBAL_HEADER_SIZE];
350        mismatched[..4].copy_from_slice(&1u32.to_le_bytes());
351        assert!(matches!(
352            decode(&mismatched),
353            Err(CodecError::Corrupt { .. })
354        ));
355
356        let mut bad_block = encode(&[1, 2]);
357        bad_block[GLOBAL_HEADER_SIZE..GLOBAL_HEADER_SIZE + 2].copy_from_slice(&1u16.to_le_bytes());
358        assert!(matches!(
359            decode(&bad_block),
360            Err(CodecError::Corrupt { .. })
361        ));
362    }
363
364    #[test]
365    fn nonzero_final_padding_is_rejected_by_all_offset_scans() {
366        let mut encoded = encode(&[1, 2]);
367        *encoded.last_mut().expect("packed byte") |= 0x80;
368        assert!(matches!(decode(&encoded), Err(CodecError::Corrupt { .. })));
369        assert!(matches!(
370            block_byte_offsets(&encoded),
371            Err(CodecError::Corrupt { .. })
372        ));
373        assert!(matches!(
374            decode_block_range(&encoded, 0, 1),
375            Err(CodecError::Corrupt { .. })
376        ));
377        assert!(matches!(
378            decode_single_block(&encoded, 0),
379            Err(CodecError::Corrupt { .. })
380        ));
381        assert!(matches!(
382            BlockIterator::new(&encoded),
383            Err(CodecError::Corrupt { .. })
384        ));
385    }
386
387    #[test]
388    fn truncated_packed_block_is_rejected_by_all_offset_scans() {
389        let mut encoded = encode(&[1, 2]);
390        encoded.pop();
391        assert!(matches!(
392            decode(&encoded),
393            Err(CodecError::Truncated { .. })
394        ));
395        assert!(matches!(
396            block_byte_offsets(&encoded),
397            Err(CodecError::Truncated { .. })
398        ));
399        assert!(matches!(
400            decode_single_block(&encoded, 0),
401            Err(CodecError::Truncated { .. })
402        ));
403    }
404}