Skip to main content

chronix_encoding/
rle.rs

1//! Run-Length Encoding (RLE) for columns with repeated consecutive values.
2//!
3//! Time-series data frequently contains runs of identical values (e.g.
4//! constant status codes, boolean flags, repeated tags).  RLE encodes
5//! each run as `(value, run_length)`, which can dramatically reduce size
6//! when long runs are present.
7//!
8//! ## Wire format
9//!
10//! ```text
11//! [value_type: u8][num_values: u32 LE][num_runs: u32 LE][run_data…]
12//! ```
13//!
14//! `value_type`: 0=i64, 1=u64, 2=f64, 3=bool, 4=string
15//!
16//! ### Numeric types (i64 / u64 / f64)
17//!
18//! Each run: `[value: 8 bytes LE][run_length: u32 LE]`
19//!
20//! ### Bool
21//!
22//! Each run: `[value: 1 byte (0 or 1)][run_length: u32 LE]`
23//!
24//! ### String
25//!
26//! Each run: `[string_len: u32 LE][string_bytes…][run_length: u32 LE]`
27
28use crate::coding::{checked_count, checked_decode_count};
29use crate::error::{EncodingError, Result};
30
31/// Value type discriminants stored at the start of an RLE payload.
32const VALUE_TYPE_I64: u8 = 0;
33const VALUE_TYPE_U64: u8 = 1;
34const VALUE_TYPE_F64: u8 = 2;
35const VALUE_TYPE_BOOL: u8 = 3;
36const VALUE_TYPE_STRING: u8 = 4;
37
38// ---------------------------------------------------------------------------
39// Encoder
40// ---------------------------------------------------------------------------
41
42/// Run-Length Encoder for data with repeated consecutive values.
43pub struct RleEncoder;
44
45/// Run-Length Decoder.
46pub struct RleDecoder;
47
48// ---------------------------------------------------------------------------
49// i64
50// ---------------------------------------------------------------------------
51
52impl RleEncoder {
53    /// Encode a slice of `i64` values using run-length encoding.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`EncodingError::EmptyInput`] if `values` is empty, or
58    /// [`EncodingError::CorruptData`] if the count exceeds `u32::MAX`.
59    pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
60        if values.is_empty() {
61            return Err(EncodingError::EmptyInput {
62                context: "RLE i64 encoder",
63            });
64        }
65        let num_values = checked_count(values.len())?;
66        let runs = build_runs(values, |a, b| a == b);
67        let num_runs = checked_count(runs.len())?;
68
69        let mut buf = Vec::with_capacity(9 + runs.len() * 12);
70        buf.push(VALUE_TYPE_I64);
71        buf.extend_from_slice(&num_values.to_le_bytes());
72        buf.extend_from_slice(&num_runs.to_le_bytes());
73        for (val, run_len) in &runs {
74            buf.extend_from_slice(&val.to_le_bytes());
75            buf.extend_from_slice(&run_len.to_le_bytes());
76        }
77        Ok(buf)
78    }
79
80    /// Encode a slice of `u64` values using run-length encoding.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
85    pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
86        if values.is_empty() {
87            return Err(EncodingError::EmptyInput {
88                context: "RLE u64 encoder",
89            });
90        }
91        let num_values = checked_count(values.len())?;
92        let runs = build_runs(values, |a, b| a == b);
93        let num_runs = checked_count(runs.len())?;
94
95        let mut buf = Vec::with_capacity(9 + runs.len() * 12);
96        buf.push(VALUE_TYPE_U64);
97        buf.extend_from_slice(&num_values.to_le_bytes());
98        buf.extend_from_slice(&num_runs.to_le_bytes());
99        for (val, run_len) in &runs {
100            buf.extend_from_slice(&val.to_le_bytes());
101            buf.extend_from_slice(&run_len.to_le_bytes());
102        }
103        Ok(buf)
104    }
105
106    /// Encode a slice of `f64` values using run-length encoding.
107    ///
108    /// Uses `to_bits()` for equality comparison so that `NaN == NaN`.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
113    pub fn encode_f64(values: &[f64]) -> Result<Vec<u8>> {
114        if values.is_empty() {
115            return Err(EncodingError::EmptyInput {
116                context: "RLE f64 encoder",
117            });
118        }
119        let num_values = checked_count(values.len())?;
120        let runs = build_runs(values, |a, b| a.to_bits() == b.to_bits());
121        let num_runs = checked_count(runs.len())?;
122
123        let mut buf = Vec::with_capacity(9 + runs.len() * 12);
124        buf.push(VALUE_TYPE_F64);
125        buf.extend_from_slice(&num_values.to_le_bytes());
126        buf.extend_from_slice(&num_runs.to_le_bytes());
127        for (val, run_len) in &runs {
128            buf.extend_from_slice(&val.to_bits().to_le_bytes());
129            buf.extend_from_slice(&run_len.to_le_bytes());
130        }
131        Ok(buf)
132    }
133
134    /// Encode a slice of `bool` values using run-length encoding.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
139    pub fn encode_bool(values: &[bool]) -> Result<Vec<u8>> {
140        if values.is_empty() {
141            return Err(EncodingError::EmptyInput {
142                context: "RLE bool encoder",
143            });
144        }
145        let num_values = checked_count(values.len())?;
146        let runs = build_runs(values, |a, b| a == b);
147        let num_runs = checked_count(runs.len())?;
148
149        let mut buf = Vec::with_capacity(9 + runs.len() * 5);
150        buf.push(VALUE_TYPE_BOOL);
151        buf.extend_from_slice(&num_values.to_le_bytes());
152        buf.extend_from_slice(&num_runs.to_le_bytes());
153        for (val, run_len) in &runs {
154            buf.push(u8::from(*val));
155            buf.extend_from_slice(&run_len.to_le_bytes());
156        }
157        Ok(buf)
158    }
159
160    /// Encode a slice of string references using run-length encoding.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
165    pub fn encode_string(values: &[&str]) -> Result<Vec<u8>> {
166        if values.is_empty() {
167            return Err(EncodingError::EmptyInput {
168                context: "RLE string encoder",
169            });
170        }
171        let num_values = checked_count(values.len())?;
172        let runs = build_runs(values, |a, b| a == b);
173        let num_runs = checked_count(runs.len())?;
174
175        let mut buf = Vec::with_capacity(9 + runs.len() * 16);
176        buf.push(VALUE_TYPE_STRING);
177        buf.extend_from_slice(&num_values.to_le_bytes());
178        buf.extend_from_slice(&num_runs.to_le_bytes());
179        for (val, run_len) in &runs {
180            let s_bytes = val.as_bytes();
181            let s_len = checked_count(s_bytes.len())?;
182            buf.extend_from_slice(&s_len.to_le_bytes());
183            buf.extend_from_slice(s_bytes);
184            buf.extend_from_slice(&run_len.to_le_bytes());
185        }
186        Ok(buf)
187    }
188}
189
190// ---------------------------------------------------------------------------
191// Decoder
192// ---------------------------------------------------------------------------
193
194impl RleDecoder {
195    /// Peek at the value-type discriminant in an RLE payload and dispatch
196    /// to the appropriate typed decoder.
197    ///
198    /// This is used by [`crate::unified::ColumnDecoder`] to decode an
199    /// `EncodingType::Rle` block without knowing the column type in advance.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the payload is too short or the type byte is
204    /// unknown.
205    pub fn value_type(data: &[u8]) -> Result<u8> {
206        if data.is_empty() {
207            return Err(corrupt("RLE payload empty — no value type byte"));
208        }
209        Ok(data[0])
210    }
211
212    /// Decode an RLE-encoded `i64` payload.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`EncodingError::CorruptData`] if the data is truncated or
217    /// the run lengths don't sum to the declared value count.
218    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
219        let (num_values, num_runs, mut pos) = read_header(data)?;
220        let mut result = Vec::with_capacity(num_values);
221        for _ in 0..num_runs {
222            if pos + 12 > data.len() {
223                return Err(corrupt("i64 run data truncated"));
224            }
225            let val = i64::from_le_bytes(
226                data[pos..pos + 8]
227                    .try_into()
228                    .map_err(|_| corrupt("i64 value bytes"))?,
229            );
230            pos += 8;
231            let run_len = u32::from_le_bytes(
232                data[pos..pos + 4]
233                    .try_into()
234                    .map_err(|_| corrupt("i64 run length bytes"))?,
235            ) as usize;
236            pos += 4;
237            checked_run(result.len(), run_len, num_values)?;
238            result.extend(std::iter::repeat_n(val, run_len));
239        }
240        verify_count(num_values, result.len())?;
241        Ok(result)
242    }
243
244    /// Decode an RLE-encoded `u64` payload.
245    ///
246    /// # Errors
247    ///
248    /// Returns [`EncodingError::CorruptData`] on truncation or count mismatch.
249    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
250        let (num_values, num_runs, mut pos) = read_header(data)?;
251        let mut result = Vec::with_capacity(num_values);
252        for _ in 0..num_runs {
253            if pos + 12 > data.len() {
254                return Err(corrupt("u64 run data truncated"));
255            }
256            let val = u64::from_le_bytes(
257                data[pos..pos + 8]
258                    .try_into()
259                    .map_err(|_| corrupt("u64 value bytes"))?,
260            );
261            pos += 8;
262            let run_len = u32::from_le_bytes(
263                data[pos..pos + 4]
264                    .try_into()
265                    .map_err(|_| corrupt("u64 run length bytes"))?,
266            ) as usize;
267            pos += 4;
268            checked_run(result.len(), run_len, num_values)?;
269            result.extend(std::iter::repeat_n(val, run_len));
270        }
271        verify_count(num_values, result.len())?;
272        Ok(result)
273    }
274
275    /// Decode an RLE-encoded `f64` payload.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`EncodingError::CorruptData`] on truncation or count mismatch.
280    pub fn decode_f64(data: &[u8]) -> Result<Vec<f64>> {
281        let (num_values, num_runs, mut pos) = read_header(data)?;
282        let mut result = Vec::with_capacity(num_values);
283        for _ in 0..num_runs {
284            if pos + 12 > data.len() {
285                return Err(corrupt("f64 run data truncated"));
286            }
287            let bits = u64::from_le_bytes(
288                data[pos..pos + 8]
289                    .try_into()
290                    .map_err(|_| corrupt("f64 value bytes"))?,
291            );
292            let val = f64::from_bits(bits);
293            pos += 8;
294            let run_len = u32::from_le_bytes(
295                data[pos..pos + 4]
296                    .try_into()
297                    .map_err(|_| corrupt("f64 run length bytes"))?,
298            ) as usize;
299            pos += 4;
300            checked_run(result.len(), run_len, num_values)?;
301            result.extend(std::iter::repeat_n(val, run_len));
302        }
303        verify_count(num_values, result.len())?;
304        Ok(result)
305    }
306
307    /// Decode an RLE-encoded `bool` payload.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`EncodingError::CorruptData`] on truncation or count mismatch.
312    pub fn decode_bool(data: &[u8]) -> Result<Vec<bool>> {
313        let (num_values, num_runs, mut pos) = read_header(data)?;
314        let mut result = Vec::with_capacity(num_values);
315        for _ in 0..num_runs {
316            if pos + 5 > data.len() {
317                return Err(corrupt("bool run data truncated"));
318            }
319            let val = data[pos] != 0;
320            pos += 1;
321            let run_len = u32::from_le_bytes(
322                data[pos..pos + 4]
323                    .try_into()
324                    .map_err(|_| corrupt("bool run length bytes"))?,
325            ) as usize;
326            pos += 4;
327            checked_run(result.len(), run_len, num_values)?;
328            result.extend(std::iter::repeat_n(val, run_len));
329        }
330        verify_count(num_values, result.len())?;
331        Ok(result)
332    }
333
334    /// Decode an RLE-encoded string payload.
335    ///
336    /// # Errors
337    ///
338    /// Returns [`EncodingError::CorruptData`] on truncation, invalid UTF-8,
339    /// or count mismatch.
340    pub fn decode_string(data: &[u8]) -> Result<Vec<String>> {
341        let (num_values, num_runs, mut pos) = read_header(data)?;
342        let mut result = Vec::with_capacity(num_values);
343        for _ in 0..num_runs {
344            if pos + 4 > data.len() {
345                return Err(corrupt("string run: string_len truncated"));
346            }
347            let s_len = u32::from_le_bytes(
348                data[pos..pos + 4]
349                    .try_into()
350                    .map_err(|_| corrupt("string length bytes"))?,
351            ) as usize;
352            pos += 4;
353            if pos + s_len > data.len() {
354                return Err(corrupt("string run: string bytes truncated"));
355            }
356            let s = std::str::from_utf8(&data[pos..pos + s_len])
357                .map_err(|e| EncodingError::CorruptData {
358                    detail: format!("RLE string: invalid UTF-8: {e}"),
359                })?
360                .to_owned();
361            pos += s_len;
362            if pos + 4 > data.len() {
363                return Err(corrupt("string run: run_length truncated"));
364            }
365            let run_len = u32::from_le_bytes(
366                data[pos..pos + 4]
367                    .try_into()
368                    .map_err(|_| corrupt("string run length bytes"))?,
369            ) as usize;
370            pos += 4;
371            checked_run(result.len(), run_len, num_values)?;
372            result.extend(std::iter::repeat_n(s, run_len));
373        }
374        verify_count(num_values, result.len())?;
375        Ok(result)
376    }
377}
378
379// ---------------------------------------------------------------------------
380// Internal helpers
381// ---------------------------------------------------------------------------
382
383/// Build runs from a slice using a custom equality function.
384fn build_runs<T: Copy, F: Fn(&T, &T) -> bool>(values: &[T], eq: F) -> Vec<(T, u32)> {
385    let mut runs = Vec::new();
386    if values.is_empty() {
387        return runs;
388    }
389    let mut current = values[0];
390    let mut count: u32 = 1;
391    for &v in &values[1..] {
392        if eq(&v, &current) {
393            // Split the run at u32::MAX to prevent silent data loss
394            // on decode (saturating_add would cap the count, producing
395            // fewer values than were encoded).
396            if count == u32::MAX {
397                runs.push((current, count));
398                count = 1;
399            } else {
400                count += 1;
401            }
402        } else {
403            runs.push((current, count));
404            current = v;
405            count = 1;
406        }
407    }
408    runs.push((current, count));
409    runs
410}
411
412/// Smallest number of payload bytes one run can occupy.
413///
414/// The boolean form is the cheapest: a one-byte value plus a `u32` run
415/// length. It is what bounds `num_runs` against the payload actually present.
416const MIN_RUN_BYTES: usize = 5;
417
418/// Read the common `[value_type: u8][num_values: u32][num_runs: u32]` header.
419///
420/// Returns `(num_values, num_runs, position_after_header)`.
421///
422/// Both counts are validated here rather than by the caller, because RLE was
423/// the one decoder that reached neither guard. `num_values` sized a
424/// `Vec::with_capacity` directly from the header — a 9-byte payload declaring
425/// `u32::MAX` reserved 34 GiB — the same unbounded-header defect the other
426/// codecs carry a ceiling for. `num_runs` was trusted the same way and drove the
427/// decode loop.
428fn read_header(data: &[u8]) -> Result<(usize, usize, usize)> {
429    // 1 byte type + 4 bytes num_values + 4 bytes num_runs = 9
430    if data.len() < 9 {
431        return Err(corrupt("RLE header too short"));
432    }
433    // data[0] is value_type — callers check it separately
434    let num_values = checked_decode_count(
435        u32::from_le_bytes(
436            data[1..5]
437                .try_into()
438                .map_err(|_| corrupt("header num_values bytes"))?,
439        ) as usize,
440        "RLE",
441    )?;
442    let num_runs = u32::from_le_bytes(
443        data[5..9]
444            .try_into()
445            .map_err(|_| corrupt("header num_runs bytes"))?,
446    ) as usize;
447    let max_runs = (data.len() - 9) / MIN_RUN_BYTES;
448    if num_runs > max_runs {
449        return Err(corrupt(
450            "RLE block declares more runs than the payload can hold",
451        ));
452    }
453    Ok((num_values, num_runs, 9))
454}
455
456/// Check that a run fits inside the block's declared value count *before*
457/// materialising it.
458///
459/// Bounding `num_values` alone is not enough: run lengths are independent
460/// `u32`s, so a block declaring three values and one run of `u32::MAX`
461/// expands to 34 GiB and only then fails `verify_count`. The count check has
462/// to happen before the `extend`, not after it — a post-hoc check on an
463/// allocation that already happened is not a bound.
464#[inline]
465fn checked_run(decoded: usize, run_len: usize, num_values: usize) -> Result<()> {
466    if decoded + run_len > num_values {
467        return Err(corrupt(
468            "RLE run lengths exceed the declared value count — header is corrupt",
469        ));
470    }
471    Ok(())
472}
473
474/// Verify that the decoded count matches the declared count.
475fn verify_count(expected: usize, actual: usize) -> Result<()> {
476    if expected != actual {
477        return Err(EncodingError::CountMismatch { expected, actual });
478    }
479    Ok(())
480}
481
482/// Shorthand for a corrupt-data error.
483fn corrupt(detail: &str) -> EncodingError {
484    EncodingError::CorruptData {
485        detail: detail.to_string(),
486    }
487}
488
489// ---------------------------------------------------------------------------
490// Tests
491// ---------------------------------------------------------------------------
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    // ── i64 ──────────────────────────────────────────────────────────
498
499    #[test]
500    fn i64_constant_roundtrip() {
501        let values = vec![42_i64; 1000];
502        let encoded = RleEncoder::encode_i64(&values).unwrap();
503        // 1 run -> header(8) + 1*(8+4) = 20 bytes, much smaller than 8000
504        assert!(encoded.len() < values.len() * 8);
505        let decoded = RleDecoder::decode_i64(&encoded).unwrap();
506        assert_eq!(values, decoded);
507    }
508
509    #[test]
510    fn i64_varied_roundtrip() {
511        let values: Vec<i64> = (0..500).collect();
512        let encoded = RleEncoder::encode_i64(&values).unwrap();
513        let decoded = RleDecoder::decode_i64(&encoded).unwrap();
514        assert_eq!(values, decoded);
515    }
516
517    #[test]
518    fn i64_runs_roundtrip() {
519        // 3 runs: 100×1, 200×2, 100×3
520        let mut values = Vec::new();
521        values.extend(std::iter::repeat_n(1_i64, 100));
522        values.extend(std::iter::repeat_n(2_i64, 200));
523        values.extend(std::iter::repeat_n(3_i64, 100));
524        let encoded = RleEncoder::encode_i64(&values).unwrap();
525        let decoded = RleDecoder::decode_i64(&encoded).unwrap();
526        assert_eq!(values, decoded);
527    }
528
529    #[test]
530    fn i64_single_value() {
531        let values = vec![99_i64];
532        let encoded = RleEncoder::encode_i64(&values).unwrap();
533        let decoded = RleDecoder::decode_i64(&encoded).unwrap();
534        assert_eq!(values, decoded);
535    }
536
537    #[test]
538    fn i64_empty_error() {
539        assert!(RleEncoder::encode_i64(&[]).is_err());
540    }
541
542    #[test]
543    fn i64_negative_values() {
544        let values = vec![-1_i64; 50];
545        let encoded = RleEncoder::encode_i64(&values).unwrap();
546        let decoded = RleDecoder::decode_i64(&encoded).unwrap();
547        assert_eq!(values, decoded);
548    }
549
550    // ── u64 ──────────────────────────────────────────────────────────
551
552    #[test]
553    fn u64_constant_roundtrip() {
554        let values = vec![100_u64; 500];
555        let encoded = RleEncoder::encode_u64(&values).unwrap();
556        let decoded = RleDecoder::decode_u64(&encoded).unwrap();
557        assert_eq!(values, decoded);
558    }
559
560    #[test]
561    fn u64_varied_roundtrip() {
562        let values: Vec<u64> = (0..200).collect();
563        let encoded = RleEncoder::encode_u64(&values).unwrap();
564        let decoded = RleDecoder::decode_u64(&encoded).unwrap();
565        assert_eq!(values, decoded);
566    }
567
568    #[test]
569    fn u64_empty_error() {
570        assert!(RleEncoder::encode_u64(&[]).is_err());
571    }
572
573    // ── f64 ──────────────────────────────────────────────────────────
574
575    #[test]
576    fn f64_constant_roundtrip() {
577        let values = vec![2.75_f64; 1000];
578        let encoded = RleEncoder::encode_f64(&values).unwrap();
579        assert!(encoded.len() < values.len() * 8);
580        let decoded = RleDecoder::decode_f64(&encoded).unwrap();
581        assert_eq!(values.len(), decoded.len());
582        for (a, b) in values.iter().zip(decoded.iter()) {
583            assert_eq!(a.to_bits(), b.to_bits());
584        }
585    }
586
587    #[test]
588    fn f64_nan_roundtrip() {
589        let values = vec![f64::NAN; 100];
590        let encoded = RleEncoder::encode_f64(&values).unwrap();
591        let decoded = RleDecoder::decode_f64(&encoded).unwrap();
592        assert_eq!(values.len(), decoded.len());
593        for v in &decoded {
594            assert!(v.is_nan());
595        }
596    }
597
598    #[test]
599    fn f64_mixed_special_values() {
600        let values = vec![
601            f64::INFINITY,
602            f64::INFINITY,
603            f64::NEG_INFINITY,
604            f64::NEG_INFINITY,
605            f64::NAN,
606            0.0,
607            -0.0, // -0.0 has different bits than 0.0
608        ];
609        let encoded = RleEncoder::encode_f64(&values).unwrap();
610        let decoded = RleDecoder::decode_f64(&encoded).unwrap();
611        assert_eq!(values.len(), decoded.len());
612        for (a, b) in values.iter().zip(decoded.iter()) {
613            assert_eq!(a.to_bits(), b.to_bits());
614        }
615    }
616
617    #[test]
618    fn f64_empty_error() {
619        assert!(RleEncoder::encode_f64(&[]).is_err());
620    }
621
622    // ── bool ─────────────────────────────────────────────────────────
623
624    #[test]
625    fn bool_constant_roundtrip() {
626        let values = vec![true; 500];
627        let encoded = RleEncoder::encode_bool(&values).unwrap();
628        assert!(encoded.len() < values.len());
629        let decoded = RleDecoder::decode_bool(&encoded).unwrap();
630        assert_eq!(values, decoded);
631    }
632
633    #[test]
634    fn bool_alternating_roundtrip() {
635        let values: Vec<bool> = (0..100).map(|i| i % 2 == 0).collect();
636        let encoded = RleEncoder::encode_bool(&values).unwrap();
637        let decoded = RleDecoder::decode_bool(&encoded).unwrap();
638        assert_eq!(values, decoded);
639    }
640
641    #[test]
642    fn bool_runs_roundtrip() {
643        let mut values = Vec::new();
644        values.extend(std::iter::repeat_n(true, 50));
645        values.extend(std::iter::repeat_n(false, 100));
646        values.extend(std::iter::repeat_n(true, 30));
647        let encoded = RleEncoder::encode_bool(&values).unwrap();
648        let decoded = RleDecoder::decode_bool(&encoded).unwrap();
649        assert_eq!(values, decoded);
650    }
651
652    #[test]
653    fn bool_empty_error() {
654        assert!(RleEncoder::encode_bool(&[]).is_err());
655    }
656
657    // ── string ───────────────────────────────────────────────────────
658
659    #[test]
660    fn string_constant_roundtrip() {
661        let values = vec!["us-east-1"; 200];
662        let encoded = RleEncoder::encode_string(&values).unwrap();
663        let decoded = RleDecoder::decode_string(&encoded).unwrap();
664        let expected: Vec<String> = values.iter().map(|s| (*s).to_owned()).collect();
665        assert_eq!(expected, decoded);
666    }
667
668    #[test]
669    fn string_runs_roundtrip() {
670        let values = vec!["a", "a", "a", "b", "b", "c", "c", "c", "c"];
671        let encoded = RleEncoder::encode_string(&values).unwrap();
672        let decoded = RleDecoder::decode_string(&encoded).unwrap();
673        let expected: Vec<String> = values.iter().map(|s| (*s).to_owned()).collect();
674        assert_eq!(expected, decoded);
675    }
676
677    #[test]
678    fn string_varied_roundtrip() {
679        let owned: Vec<String> = (0..50).map(|i| format!("val-{i}")).collect();
680        let refs: Vec<&str> = owned.iter().map(String::as_str).collect();
681        let encoded = RleEncoder::encode_string(&refs).unwrap();
682        let decoded = RleDecoder::decode_string(&encoded).unwrap();
683        assert_eq!(owned, decoded);
684    }
685
686    #[test]
687    fn string_empty_strings() {
688        let values = vec![""; 100];
689        let encoded = RleEncoder::encode_string(&values).unwrap();
690        let decoded = RleDecoder::decode_string(&encoded).unwrap();
691        let expected: Vec<String> = values.iter().map(|s| (*s).to_owned()).collect();
692        assert_eq!(expected, decoded);
693    }
694
695    #[test]
696    fn string_empty_error() {
697        let empty: &[&str] = &[];
698        assert!(RleEncoder::encode_string(empty).is_err());
699    }
700
701    // ── corruption / edge cases ──────────────────────────────────────
702
703    #[test]
704    fn decode_truncated_header() {
705        assert!(RleDecoder::decode_i64(&[0, 1, 2, 3]).is_err());
706    }
707
708    #[test]
709    fn decode_truncated_run() {
710        // Valid header: type=i64, 1 value, 1 run, but missing run data
711        let mut data = vec![VALUE_TYPE_I64];
712        data.extend_from_slice(&1_u32.to_le_bytes());
713        data.extend_from_slice(&1_u32.to_le_bytes());
714        assert!(RleDecoder::decode_i64(&data).is_err());
715    }
716
717    #[test]
718    fn decode_count_mismatch() {
719        // Header says 999 values but data has 1 run of length 1
720        let mut data = vec![VALUE_TYPE_I64];
721        data.extend_from_slice(&999_u32.to_le_bytes()); // num_values = 999
722        data.extend_from_slice(&1_u32.to_le_bytes()); // num_runs = 1
723        data.extend_from_slice(&42_i64.to_le_bytes()); // value
724        data.extend_from_slice(&1_u32.to_le_bytes()); // run_length = 1
725        assert!(RleDecoder::decode_i64(&data).is_err());
726    }
727
728    // ── compression ratio ────────────────────────────────────────────
729
730    #[test]
731    fn constant_column_achieves_high_compression() {
732        let values = vec![42_i64; 10_000];
733        let encoded = RleEncoder::encode_i64(&values).unwrap();
734        // 10k × 8 = 80_000 bytes raw; RLE should be 20 bytes
735        let ratio = (values.len() * 8) as f64 / encoded.len() as f64;
736        assert!(ratio > 100.0, "Expected >100× compression, got {ratio:.1}×");
737    }
738}
739
740#[cfg(test)]
741mod proptests {
742    use super::*;
743    use proptest::prelude::*;
744
745    proptest! {
746        #[test]
747        fn roundtrip_i64(values in proptest::collection::vec(any::<i64>(), 1..300)) {
748            let encoded = RleEncoder::encode_i64(&values).unwrap();
749            let decoded = RleDecoder::decode_i64(&encoded).unwrap();
750            prop_assert_eq!(&values, &decoded);
751        }
752
753        #[test]
754        fn roundtrip_u64(values in proptest::collection::vec(any::<u64>(), 1..300)) {
755            let encoded = RleEncoder::encode_u64(&values).unwrap();
756            let decoded = RleDecoder::decode_u64(&encoded).unwrap();
757            prop_assert_eq!(&values, &decoded);
758        }
759
760        #[test]
761        fn roundtrip_f64(values in proptest::collection::vec(any::<f64>(), 1..300)) {
762            let encoded = RleEncoder::encode_f64(&values).unwrap();
763            let decoded = RleDecoder::decode_f64(&encoded).unwrap();
764            // Bitwise comparison for f64 (NaN == NaN)
765            prop_assert_eq!(values.len(), decoded.len());
766            for (a, b) in values.iter().zip(decoded.iter()) {
767                prop_assert_eq!(a.to_bits(), b.to_bits());
768            }
769        }
770
771        #[test]
772        fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..300)) {
773            let encoded = RleEncoder::encode_bool(&values).unwrap();
774            let decoded = RleDecoder::decode_bool(&encoded).unwrap();
775            prop_assert_eq!(&values, &decoded);
776        }
777
778        #[test]
779        fn roundtrip_string(values in proptest::collection::vec("[a-z]{0,8}", 1..100)) {
780            let refs: Vec<&str> = values.iter().map(String::as_str).collect();
781            let encoded = RleEncoder::encode_string(&refs).unwrap();
782            let decoded = RleDecoder::decode_string(&encoded).unwrap();
783            prop_assert_eq!(&values, &decoded);
784        }
785    }
786}
787
788#[cfg(test)]
789mod bomb_tests {
790    use super::*;
791
792    /// A nine-byte header must not size an allocation.
793    ///
794    /// RLE was the last decoder to gain a ceiling: `read_header` fed
795    /// `num_values` straight into `Vec::with_capacity`, so a nine-byte
796    /// payload declaring `u32::MAX` reserved 34 GiB. This is the ALP finding
797    /// in the codec the ALP fix did not cover, and it was reachable from a
798    /// single flipped bit on the flash it decodes from.
799    #[test]
800    fn a_declared_value_count_cannot_size_an_allocation() {
801        let mut payload = vec![VALUE_TYPE_I64];
802        payload.extend_from_slice(&u32::MAX.to_le_bytes()); // num_values
803        payload.extend_from_slice(&0u32.to_le_bytes()); // num_runs
804        let err = RleDecoder::decode_i64(&payload).expect_err("must refuse the count");
805        assert!(
806            err.to_string().contains("ceiling"),
807            "expected the decode ceiling to reject it, got: {err}"
808        );
809    }
810
811    /// A single run must not expand past the declared count.
812    ///
813    /// Bounding `num_values` alone leaves the run lengths unbounded: they are
814    /// independent `u32`s, and `verify_count` only ran *after* the expansion
815    /// had already been materialised. Three declared values and one run of
816    /// `u32::MAX` is 34 GiB of `repeat_n` before the mismatch is noticed — a
817    /// check after the allocation is not a bound.
818    #[test]
819    fn a_run_length_cannot_expand_past_the_declared_count() {
820        let mut payload = vec![VALUE_TYPE_I64];
821        payload.extend_from_slice(&3u32.to_le_bytes()); // num_values
822        payload.extend_from_slice(&1u32.to_le_bytes()); // num_runs
823        payload.extend_from_slice(&7i64.to_le_bytes()); // value
824        payload.extend_from_slice(&u32::MAX.to_le_bytes()); // run length
825        let err = RleDecoder::decode_i64(&payload).expect_err("must refuse the run");
826        assert!(
827            err.to_string().contains("exceed the declared value count"),
828            "expected the run guard to reject it, got: {err}"
829        );
830    }
831
832    /// The same guard on the string form, where each element is 24 bytes.
833    #[test]
834    fn a_string_run_cannot_expand_past_the_declared_count() {
835        let mut payload = vec![VALUE_TYPE_STRING];
836        payload.extend_from_slice(&1u32.to_le_bytes()); // num_values
837        payload.extend_from_slice(&1u32.to_le_bytes()); // num_runs
838        payload.extend_from_slice(&1u32.to_le_bytes()); // string length
839        payload.push(b'x');
840        payload.extend_from_slice(&u32::MAX.to_le_bytes()); // run length
841        let err = RleDecoder::decode_string(&payload).expect_err("must refuse the run");
842        assert!(
843            err.to_string().contains("exceed the declared value count"),
844            "expected the run guard to reject it, got: {err}"
845        );
846    }
847
848    /// A run count the payload cannot possibly hold is a corrupt header, and
849    /// saying so beats iterating `num_runs` times to discover it.
850    #[test]
851    fn a_run_count_beyond_the_payload_is_rejected() {
852        let mut payload = vec![VALUE_TYPE_BOOL];
853        payload.extend_from_slice(&10u32.to_le_bytes()); // num_values
854        payload.extend_from_slice(&1_000_000u32.to_le_bytes()); // num_runs
855        let err = RleDecoder::decode_bool(&payload).expect_err("must refuse the run count");
856        assert!(
857            err.to_string().contains("more runs than the payload"),
858            "expected the run-count guard to reject it, got: {err}"
859        );
860    }
861
862    /// And a legitimate block still round-trips: the guards bound corruption,
863    /// they do not narrow the format.
864    #[test]
865    fn legitimate_blocks_still_round_trip() {
866        let values: Vec<i64> = std::iter::repeat_n(4, 1000)
867            .chain(std::iter::repeat_n(9, 500))
868            .collect();
869        let encoded = RleEncoder::encode_i64(&values).expect("encode");
870        assert_eq!(RleDecoder::decode_i64(&encoded).expect("decode"), values);
871    }
872}