Skip to main content

chronix_encoding/
plain.rs

1//! Plain (uncompressed) encoding for fallback use.
2//!
3//! Stores values directly in their natural binary representations when
4//! specialised encoders cannot be applied or when the data is incompressible.
5//!
6//! ## Format
7//!
8//! ```text
9//! [count: u32]
10//! [raw values…]
11//! ```
12//!
13//! - `f64`/`i64`/`u64`: 8 bytes little-endian each.
14//! - `bool`: 1 byte each (`0x01` = true, `0x00` = false).
15//! - `String`: `[len: u32][UTF-8 bytes…]` per entry.
16
17use crate::coding::{checked_count, checked_decode_count};
18use crate::error::{EncodingError, Result};
19
20/// Plain (uncompressed) encoder.
21#[derive(Debug, Clone, Copy)]
22pub struct PlainEncoder;
23
24/// Plain (uncompressed) decoder.
25#[derive(Debug, Clone, Copy)]
26pub struct PlainDecoder;
27
28impl PlainEncoder {
29    /// Encode `f64` values uncompressed.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
34    pub fn encode_f64(values: &[f64]) -> Result<Vec<u8>> {
35        if values.is_empty() {
36            return Err(EncodingError::EmptyInput {
37                context: "plain f64",
38            });
39        }
40        let count = checked_count(values.len())?;
41        let mut buf = Vec::with_capacity(4 + values.len() * 8);
42        buf.extend_from_slice(&count.to_le_bytes());
43        for &v in values {
44            buf.extend_from_slice(&v.to_le_bytes());
45        }
46        Ok(buf)
47    }
48
49    /// Encode `i64` values uncompressed.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
54    pub fn encode_i64(values: &[i64]) -> Result<Vec<u8>> {
55        if values.is_empty() {
56            return Err(EncodingError::EmptyInput {
57                context: "plain i64",
58            });
59        }
60        let count = checked_count(values.len())?;
61        let mut buf = Vec::with_capacity(4 + values.len() * 8);
62        buf.extend_from_slice(&count.to_le_bytes());
63        for &v in values {
64            buf.extend_from_slice(&v.to_le_bytes());
65        }
66        Ok(buf)
67    }
68
69    /// Encode `u64` values uncompressed.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
74    pub fn encode_u64(values: &[u64]) -> Result<Vec<u8>> {
75        if values.is_empty() {
76            return Err(EncodingError::EmptyInput {
77                context: "plain u64",
78            });
79        }
80        let count = checked_count(values.len())?;
81        let mut buf = Vec::with_capacity(4 + values.len() * 8);
82        buf.extend_from_slice(&count.to_le_bytes());
83        for &v in values {
84            buf.extend_from_slice(&v.to_le_bytes());
85        }
86        Ok(buf)
87    }
88
89    /// Encode boolean values uncompressed (1 byte per bool).
90    ///
91    /// # Errors
92    ///
93    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
94    pub fn encode_bool(values: &[bool]) -> Result<Vec<u8>> {
95        if values.is_empty() {
96            return Err(EncodingError::EmptyInput {
97                context: "plain bool",
98            });
99        }
100        let count = checked_count(values.len())?;
101        let mut buf = Vec::with_capacity(4 + values.len());
102        buf.extend_from_slice(&count.to_le_bytes());
103        for &v in values {
104            buf.push(u8::from(v));
105        }
106        Ok(buf)
107    }
108
109    /// Encode string values uncompressed (length-prefixed).
110    ///
111    /// # Errors
112    ///
113    /// Returns [`EncodingError::EmptyInput`] if `values` is empty.
114    pub fn encode_string(values: &[&str]) -> Result<Vec<u8>> {
115        if values.is_empty() {
116            return Err(EncodingError::EmptyInput {
117                context: "plain string",
118            });
119        }
120        let count = checked_count(values.len())?;
121        let total_bytes: usize = values.iter().map(|s| 4 + s.len()).sum();
122        let mut buf = Vec::with_capacity(4 + total_bytes);
123        buf.extend_from_slice(&count.to_le_bytes());
124        for &s in values {
125            let len = u32::try_from(s.len()).map_err(|_| EncodingError::CorruptData {
126                detail: format!("string length {} exceeds u32::MAX", s.len()),
127            })?;
128            buf.extend_from_slice(&len.to_le_bytes());
129            buf.extend_from_slice(s.as_bytes());
130        }
131        Ok(buf)
132    }
133}
134
135impl PlainDecoder {
136    /// Decode `f64` values.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
141    pub fn decode_f64(data: &[u8]) -> Result<Vec<f64>> {
142        let count = Self::read_count(data, "f64")?;
143        let expected = 4 + count * 8;
144        Self::check_len(data, expected, "f64")?;
145
146        let mut values = Vec::with_capacity(count);
147        for i in 0..count {
148            let offset = 4 + i * 8;
149            let bytes: [u8; 8] =
150                data[offset..offset + 8]
151                    .try_into()
152                    .map_err(|_| EncodingError::CorruptData {
153                        detail: "plain f64 slice conversion failed".to_string(),
154                    })?;
155            values.push(f64::from_le_bytes(bytes));
156        }
157        Ok(values)
158    }
159
160    /// Decode `i64` values.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
165    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
166        let count = Self::read_count(data, "i64")?;
167        let expected = 4 + count * 8;
168        Self::check_len(data, expected, "i64")?;
169
170        let mut values = Vec::with_capacity(count);
171        for i in 0..count {
172            let offset = 4 + i * 8;
173            let bytes: [u8; 8] =
174                data[offset..offset + 8]
175                    .try_into()
176                    .map_err(|_| EncodingError::CorruptData {
177                        detail: "plain i64 slice conversion failed".to_string(),
178                    })?;
179            values.push(i64::from_le_bytes(bytes));
180        }
181        Ok(values)
182    }
183
184    /// Decode `u64` values.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
189    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
190        let count = Self::read_count(data, "u64")?;
191        let expected = 4 + count * 8;
192        Self::check_len(data, expected, "u64")?;
193
194        let mut values = Vec::with_capacity(count);
195        for i in 0..count {
196            let offset = 4 + i * 8;
197            let bytes: [u8; 8] =
198                data[offset..offset + 8]
199                    .try_into()
200                    .map_err(|_| EncodingError::CorruptData {
201                        detail: "plain u64 slice conversion failed".to_string(),
202                    })?;
203            values.push(u64::from_le_bytes(bytes));
204        }
205        Ok(values)
206    }
207
208    /// Decode boolean values.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
213    pub fn decode_bool(data: &[u8]) -> Result<Vec<bool>> {
214        let count = Self::read_count(data, "bool")?;
215        let expected = 4 + count;
216        Self::check_len(data, expected, "bool")?;
217
218        let mut values = Vec::with_capacity(count);
219        for i in 0..count {
220            values.push(data[4 + i] != 0);
221        }
222        Ok(values)
223    }
224
225    /// Decode string values.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
230    pub fn decode_string(data: &[u8]) -> Result<Vec<String>> {
231        let count = Self::read_count(data, "string")?;
232        let mut offset = 4;
233        let mut values = Vec::with_capacity(count);
234
235        for _ in 0..count {
236            if offset + 4 > data.len() {
237                return Err(EncodingError::CorruptData {
238                    detail: "plain string data truncated at length prefix".to_string(),
239                });
240            }
241            let len = u32::from_le_bytes([
242                data[offset],
243                data[offset + 1],
244                data[offset + 2],
245                data[offset + 3],
246            ]) as usize;
247            offset += 4;
248
249            if offset + len > data.len() {
250                return Err(EncodingError::CorruptData {
251                    detail: "plain string data truncated".to_string(),
252                });
253            }
254            let s = std::str::from_utf8(&data[offset..offset + len]).map_err(|e| {
255                EncodingError::CorruptData {
256                    detail: format!("invalid UTF-8 in plain string: {e}"),
257                }
258            })?;
259            values.push(s.to_owned());
260            offset += len;
261        }
262
263        Ok(values)
264    }
265
266    fn read_count(data: &[u8], label: &str) -> Result<usize> {
267        if data.len() < 4 {
268            return Err(EncodingError::CorruptData {
269                detail: format!("plain {label} data too short for header"),
270            });
271        }
272        checked_decode_count(
273            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
274            "plain",
275        )
276    }
277
278    fn check_len(data: &[u8], expected: usize, label: &str) -> Result<()> {
279        if data.len() < expected {
280            return Err(EncodingError::CorruptData {
281                detail: format!(
282                    "plain {label} data truncated: expected {expected}, got {}",
283                    data.len()
284                ),
285            });
286        }
287        Ok(())
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn f64_roundtrip() {
297        let values = vec![1.0, -2.5, 0.0, f64::INFINITY, f64::NEG_INFINITY];
298        let encoded = PlainEncoder::encode_f64(&values).unwrap();
299        let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
300        assert_eq!(values, decoded);
301    }
302
303    #[test]
304    fn f64_nan_roundtrip() {
305        let values = vec![f64::NAN];
306        let encoded = PlainEncoder::encode_f64(&values).unwrap();
307        let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
308        assert!(decoded[0].is_nan());
309    }
310
311    #[test]
312    fn i64_roundtrip() {
313        let values = vec![i64::MIN, -1, 0, 1, i64::MAX];
314        let encoded = PlainEncoder::encode_i64(&values).unwrap();
315        let decoded = PlainDecoder::decode_i64(&encoded).unwrap();
316        assert_eq!(values, decoded);
317    }
318
319    #[test]
320    fn u64_roundtrip() {
321        let values = vec![0, 1, u64::MAX, 42, 1_000_000];
322        let encoded = PlainEncoder::encode_u64(&values).unwrap();
323        let decoded = PlainDecoder::decode_u64(&encoded).unwrap();
324        assert_eq!(values, decoded);
325    }
326
327    #[test]
328    fn bool_roundtrip() {
329        let values = vec![true, false, true, true, false];
330        let encoded = PlainEncoder::encode_bool(&values).unwrap();
331        let decoded = PlainDecoder::decode_bool(&encoded).unwrap();
332        assert_eq!(values, decoded);
333    }
334
335    #[test]
336    fn string_roundtrip() {
337        let values = vec!["hello", "world", "", "café", "日本語"];
338        let encoded = PlainEncoder::encode_string(&values).unwrap();
339        let decoded = PlainDecoder::decode_string(&encoded).unwrap();
340        let expected: Vec<String> = values.iter().map(|s| (*s).to_owned()).collect();
341        assert_eq!(expected, decoded);
342    }
343
344    #[test]
345    fn empty_errors() {
346        assert!(PlainEncoder::encode_f64(&[]).is_err());
347        assert!(PlainEncoder::encode_i64(&[]).is_err());
348        assert!(PlainEncoder::encode_u64(&[]).is_err());
349        assert!(PlainEncoder::encode_bool(&[]).is_err());
350        let empty: &[&str] = &[];
351        assert!(PlainEncoder::encode_string(empty).is_err());
352    }
353
354    #[test]
355    fn single_values() {
356        let e = PlainEncoder::encode_f64(&[std::f64::consts::PI]).unwrap();
357        assert_eq!(
358            vec![std::f64::consts::PI],
359            PlainDecoder::decode_f64(&e).unwrap()
360        );
361
362        let e = PlainEncoder::encode_i64(&[42]).unwrap();
363        assert_eq!(vec![42i64], PlainDecoder::decode_i64(&e).unwrap());
364
365        let e = PlainEncoder::encode_u64(&[99]).unwrap();
366        assert_eq!(vec![99u64], PlainDecoder::decode_u64(&e).unwrap());
367
368        let e = PlainEncoder::encode_bool(&[true]).unwrap();
369        assert_eq!(vec![true], PlainDecoder::decode_bool(&e).unwrap());
370
371        let e = PlainEncoder::encode_string(&["x"]).unwrap();
372        assert_eq!(
373            vec!["x".to_owned()],
374            PlainDecoder::decode_string(&e).unwrap()
375        );
376    }
377
378    #[test]
379    fn corrupt_data_detected() {
380        // Too short
381        assert!(PlainDecoder::decode_f64(&[0, 0]).is_err());
382        // Count says 2 but only 1 value present
383        let mut bad = PlainEncoder::encode_f64(&[1.0, 2.0]).unwrap();
384        bad.truncate(4 + 8); // Remove second value
385        assert!(PlainDecoder::decode_f64(&bad).is_err());
386    }
387
388    #[test]
389    fn no_compression_overhead() {
390        // Plain encoding should have exactly 4-byte header + raw data
391        let values: Vec<f64> = (0..1000).map(|i| i as f64).collect();
392        let encoded = PlainEncoder::encode_f64(&values).unwrap();
393        assert_eq!(encoded.len(), 4 + 1000 * 8);
394    }
395}
396
397#[cfg(test)]
398mod proptests {
399    use super::*;
400    use proptest::prelude::*;
401
402    proptest! {
403        #[test]
404        fn roundtrip_f64(values in proptest::collection::vec(any::<f64>(), 1..500)) {
405            let encoded = PlainEncoder::encode_f64(&values).unwrap();
406            let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
407            prop_assert_eq!(values.len(), decoded.len());
408            for (a, b) in values.iter().zip(decoded.iter()) {
409                prop_assert_eq!(a.to_bits(), b.to_bits());
410            }
411        }
412
413        #[test]
414        fn roundtrip_i64(values in proptest::collection::vec(any::<i64>(), 1..500)) {
415            let encoded = PlainEncoder::encode_i64(&values).unwrap();
416            let decoded = PlainDecoder::decode_i64(&encoded).unwrap();
417            prop_assert_eq!(&values, &decoded);
418        }
419
420        #[test]
421        fn roundtrip_u64(values in proptest::collection::vec(any::<u64>(), 1..500)) {
422            let encoded = PlainEncoder::encode_u64(&values).unwrap();
423            let decoded = PlainDecoder::decode_u64(&encoded).unwrap();
424            prop_assert_eq!(&values, &decoded);
425        }
426
427        #[test]
428        fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..500)) {
429            let encoded = PlainEncoder::encode_bool(&values).unwrap();
430            let decoded = PlainDecoder::decode_bool(&encoded).unwrap();
431            prop_assert_eq!(&values, &decoded);
432        }
433
434        #[test]
435        fn roundtrip_string(values in proptest::collection::vec("[a-z]{0,8}", 1..200)) {
436            let refs: Vec<&str> = values.iter().map(String::as_str).collect();
437            let encoded = PlainEncoder::encode_string(&refs).unwrap();
438            let decoded = PlainDecoder::decode_string(&encoded).unwrap();
439            prop_assert_eq!(&values, &decoded);
440        }
441    }
442}