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    /// Encode a byte-string column.
135    ///
136    /// The same wire shape as [`encode_string`](Self::encode_string) — a
137    /// count, then a `u32` length and that many bytes per value — without the
138    /// UTF-8 constraint. It exists for native histograms, whose values are
139    /// composite objects with no numeric representation to compress.
140    ///
141    /// # Errors
142    ///
143    /// [`EncodingError::EmptyInput`] for no values, or
144    /// [`EncodingError::CorruptData`] for a value longer than `u32::MAX`.
145    pub fn encode_bytes(values: &[&[u8]]) -> Result<Vec<u8>> {
146        if values.is_empty() {
147            return Err(EncodingError::EmptyInput {
148                context: "plain bytes",
149            });
150        }
151        let count = checked_count(values.len())?;
152        let total_bytes: usize = values.iter().map(|b| 4 + b.len()).sum();
153        let mut buf = Vec::with_capacity(4 + total_bytes);
154        buf.extend_from_slice(&count.to_le_bytes());
155        for &b in values {
156            let len = u32::try_from(b.len()).map_err(|_| EncodingError::CorruptData {
157                detail: format!("byte string length {} exceeds u32::MAX", b.len()),
158            })?;
159            buf.extend_from_slice(&len.to_le_bytes());
160            buf.extend_from_slice(b);
161        }
162        Ok(buf)
163    }
164}
165
166impl PlainDecoder {
167    /// Decode `f64` values.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
172    pub fn decode_f64(data: &[u8]) -> Result<Vec<f64>> {
173        let count = Self::read_count(data, "f64")?;
174        let expected = 4 + count * 8;
175        Self::check_len(data, expected, "f64")?;
176
177        let mut values = Vec::with_capacity(count);
178        for i in 0..count {
179            let offset = 4 + i * 8;
180            let bytes: [u8; 8] =
181                data[offset..offset + 8]
182                    .try_into()
183                    .map_err(|_| EncodingError::CorruptData {
184                        detail: "plain f64 slice conversion failed".to_string(),
185                    })?;
186            values.push(f64::from_le_bytes(bytes));
187        }
188        Ok(values)
189    }
190
191    /// Decode a byte-string column.
192    ///
193    /// # Errors
194    ///
195    /// [`EncodingError::CorruptData`] if a length prefix runs past the end of
196    /// the payload, or if the declared total would exceed the decode ceiling.
197    /// Both are reachable from one flipped bit in a stored block, which is why
198    /// the bound is checked before the allocation rather than after it.
199    pub fn decode_bytes(data: &[u8]) -> Result<Vec<Vec<u8>>> {
200        let count = Self::read_count(data, "bytes")?;
201        let mut offset = 4;
202        let mut values = Vec::with_capacity(count.min(1024));
203        let mut decoded_total = 0usize;
204
205        for _ in 0..count {
206            if offset + 4 > data.len() {
207                return Err(EncodingError::CorruptData {
208                    detail: "plain bytes data truncated at length prefix".to_string(),
209                });
210            }
211            let len = u32::from_le_bytes([
212                data[offset],
213                data[offset + 1],
214                data[offset + 2],
215                data[offset + 3],
216            ]) as usize;
217            offset += 4;
218
219            if offset + len > data.len() {
220                return Err(EncodingError::CorruptData {
221                    detail: "plain bytes data truncated".to_string(),
222                });
223            }
224            // Bound the running total, not just the per-value length: a
225            // payload can declare many short values whose sum is large.
226            decoded_total = decoded_total.saturating_add(len);
227            if decoded_total > crate::coding::MAX_DECODED_BYTES {
228                return Err(EncodingError::CorruptData {
229                    detail: format!(
230                        "plain bytes would decode to {decoded_total} bytes, above the                          {} byte ceiling",
231                        crate::coding::MAX_DECODED_BYTES
232                    ),
233                });
234            }
235            values.push(data[offset..offset + len].to_vec());
236            offset += len;
237        }
238
239        Ok(values)
240    }
241
242    /// Decode `i64` values.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
247    pub fn decode_i64(data: &[u8]) -> Result<Vec<i64>> {
248        let count = Self::read_count(data, "i64")?;
249        let expected = 4 + count * 8;
250        Self::check_len(data, expected, "i64")?;
251
252        let mut values = Vec::with_capacity(count);
253        for i in 0..count {
254            let offset = 4 + i * 8;
255            let bytes: [u8; 8] =
256                data[offset..offset + 8]
257                    .try_into()
258                    .map_err(|_| EncodingError::CorruptData {
259                        detail: "plain i64 slice conversion failed".to_string(),
260                    })?;
261            values.push(i64::from_le_bytes(bytes));
262        }
263        Ok(values)
264    }
265
266    /// Decode `u64` values.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
271    pub fn decode_u64(data: &[u8]) -> Result<Vec<u64>> {
272        let count = Self::read_count(data, "u64")?;
273        let expected = 4 + count * 8;
274        Self::check_len(data, expected, "u64")?;
275
276        let mut values = Vec::with_capacity(count);
277        for i in 0..count {
278            let offset = 4 + i * 8;
279            let bytes: [u8; 8] =
280                data[offset..offset + 8]
281                    .try_into()
282                    .map_err(|_| EncodingError::CorruptData {
283                        detail: "plain u64 slice conversion failed".to_string(),
284                    })?;
285            values.push(u64::from_le_bytes(bytes));
286        }
287        Ok(values)
288    }
289
290    /// Decode boolean values.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
295    pub fn decode_bool(data: &[u8]) -> Result<Vec<bool>> {
296        let count = Self::read_count(data, "bool")?;
297        let expected = 4 + count;
298        Self::check_len(data, expected, "bool")?;
299
300        let mut values = Vec::with_capacity(count);
301        for i in 0..count {
302            values.push(data[4 + i] != 0);
303        }
304        Ok(values)
305    }
306
307    /// Decode string values.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`EncodingError::CorruptData`] if the data is malformed.
312    pub fn decode_string(data: &[u8]) -> Result<Vec<String>> {
313        let count = Self::read_count(data, "string")?;
314        let mut offset = 4;
315        let mut values = Vec::with_capacity(count);
316
317        for _ in 0..count {
318            if offset + 4 > data.len() {
319                return Err(EncodingError::CorruptData {
320                    detail: "plain string data truncated at length prefix".to_string(),
321                });
322            }
323            let len = u32::from_le_bytes([
324                data[offset],
325                data[offset + 1],
326                data[offset + 2],
327                data[offset + 3],
328            ]) as usize;
329            offset += 4;
330
331            if offset + len > data.len() {
332                return Err(EncodingError::CorruptData {
333                    detail: "plain string data truncated".to_string(),
334                });
335            }
336            let s = std::str::from_utf8(&data[offset..offset + len]).map_err(|e| {
337                EncodingError::CorruptData {
338                    detail: format!("invalid UTF-8 in plain string: {e}"),
339                }
340            })?;
341            values.push(s.to_owned());
342            offset += len;
343        }
344
345        Ok(values)
346    }
347
348    fn read_count(data: &[u8], label: &str) -> Result<usize> {
349        if data.len() < 4 {
350            return Err(EncodingError::CorruptData {
351                detail: format!("plain {label} data too short for header"),
352            });
353        }
354        checked_decode_count(
355            u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize,
356            "plain",
357        )
358    }
359
360    fn check_len(data: &[u8], expected: usize, label: &str) -> Result<()> {
361        if data.len() < expected {
362            return Err(EncodingError::CorruptData {
363                detail: format!(
364                    "plain {label} data truncated: expected {expected}, got {}",
365                    data.len()
366                ),
367            });
368        }
369        Ok(())
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn f64_roundtrip() {
379        let values = vec![1.0, -2.5, 0.0, f64::INFINITY, f64::NEG_INFINITY];
380        let encoded = PlainEncoder::encode_f64(&values).unwrap();
381        let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
382        assert_eq!(values, decoded);
383    }
384
385    #[test]
386    fn f64_nan_roundtrip() {
387        let values = vec![f64::NAN];
388        let encoded = PlainEncoder::encode_f64(&values).unwrap();
389        let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
390        assert!(decoded[0].is_nan());
391    }
392
393    #[test]
394    fn i64_roundtrip() {
395        let values = vec![i64::MIN, -1, 0, 1, i64::MAX];
396        let encoded = PlainEncoder::encode_i64(&values).unwrap();
397        let decoded = PlainDecoder::decode_i64(&encoded).unwrap();
398        assert_eq!(values, decoded);
399    }
400
401    #[test]
402    fn u64_roundtrip() {
403        let values = vec![0, 1, u64::MAX, 42, 1_000_000];
404        let encoded = PlainEncoder::encode_u64(&values).unwrap();
405        let decoded = PlainDecoder::decode_u64(&encoded).unwrap();
406        assert_eq!(values, decoded);
407    }
408
409    #[test]
410    fn bool_roundtrip() {
411        let values = vec![true, false, true, true, false];
412        let encoded = PlainEncoder::encode_bool(&values).unwrap();
413        let decoded = PlainDecoder::decode_bool(&encoded).unwrap();
414        assert_eq!(values, decoded);
415    }
416
417    #[test]
418    fn string_roundtrip() {
419        let values = vec!["hello", "world", "", "café", "日本語"];
420        let encoded = PlainEncoder::encode_string(&values).unwrap();
421        let decoded = PlainDecoder::decode_string(&encoded).unwrap();
422        let expected: Vec<String> = values.iter().map(|s| (*s).to_owned()).collect();
423        assert_eq!(expected, decoded);
424    }
425
426    #[test]
427    fn empty_errors() {
428        assert!(PlainEncoder::encode_f64(&[]).is_err());
429        assert!(PlainEncoder::encode_i64(&[]).is_err());
430        assert!(PlainEncoder::encode_u64(&[]).is_err());
431        assert!(PlainEncoder::encode_bool(&[]).is_err());
432        let empty: &[&str] = &[];
433        assert!(PlainEncoder::encode_string(empty).is_err());
434    }
435
436    #[test]
437    fn single_values() {
438        let e = PlainEncoder::encode_f64(&[std::f64::consts::PI]).unwrap();
439        assert_eq!(
440            vec![std::f64::consts::PI],
441            PlainDecoder::decode_f64(&e).unwrap()
442        );
443
444        let e = PlainEncoder::encode_i64(&[42]).unwrap();
445        assert_eq!(vec![42i64], PlainDecoder::decode_i64(&e).unwrap());
446
447        let e = PlainEncoder::encode_u64(&[99]).unwrap();
448        assert_eq!(vec![99u64], PlainDecoder::decode_u64(&e).unwrap());
449
450        let e = PlainEncoder::encode_bool(&[true]).unwrap();
451        assert_eq!(vec![true], PlainDecoder::decode_bool(&e).unwrap());
452
453        let e = PlainEncoder::encode_string(&["x"]).unwrap();
454        assert_eq!(
455            vec!["x".to_owned()],
456            PlainDecoder::decode_string(&e).unwrap()
457        );
458    }
459
460    #[test]
461    fn corrupt_data_detected() {
462        // Too short
463        assert!(PlainDecoder::decode_f64(&[0, 0]).is_err());
464        // Count says 2 but only 1 value present
465        let mut bad = PlainEncoder::encode_f64(&[1.0, 2.0]).unwrap();
466        bad.truncate(4 + 8); // Remove second value
467        assert!(PlainDecoder::decode_f64(&bad).is_err());
468    }
469
470    #[test]
471    fn no_compression_overhead() {
472        // Plain encoding should have exactly 4-byte header + raw data
473        let values: Vec<f64> = (0..1000).map(|i| i as f64).collect();
474        let encoded = PlainEncoder::encode_f64(&values).unwrap();
475        assert_eq!(encoded.len(), 4 + 1000 * 8);
476    }
477}
478
479#[cfg(test)]
480mod proptests {
481    use super::*;
482    use proptest::prelude::*;
483
484    proptest! {
485        #[test]
486        fn roundtrip_f64(values in proptest::collection::vec(any::<f64>(), 1..500)) {
487            let encoded = PlainEncoder::encode_f64(&values).unwrap();
488            let decoded = PlainDecoder::decode_f64(&encoded).unwrap();
489            prop_assert_eq!(values.len(), decoded.len());
490            for (a, b) in values.iter().zip(decoded.iter()) {
491                prop_assert_eq!(a.to_bits(), b.to_bits());
492            }
493        }
494
495        #[test]
496        fn roundtrip_i64(values in proptest::collection::vec(any::<i64>(), 1..500)) {
497            let encoded = PlainEncoder::encode_i64(&values).unwrap();
498            let decoded = PlainDecoder::decode_i64(&encoded).unwrap();
499            prop_assert_eq!(&values, &decoded);
500        }
501
502        #[test]
503        fn roundtrip_u64(values in proptest::collection::vec(any::<u64>(), 1..500)) {
504            let encoded = PlainEncoder::encode_u64(&values).unwrap();
505            let decoded = PlainDecoder::decode_u64(&encoded).unwrap();
506            prop_assert_eq!(&values, &decoded);
507        }
508
509        #[test]
510        fn roundtrip_bool(values in proptest::collection::vec(any::<bool>(), 1..500)) {
511            let encoded = PlainEncoder::encode_bool(&values).unwrap();
512            let decoded = PlainDecoder::decode_bool(&encoded).unwrap();
513            prop_assert_eq!(&values, &decoded);
514        }
515
516        #[test]
517        fn roundtrip_string(values in proptest::collection::vec("[a-z]{0,8}", 1..200)) {
518            let refs: Vec<&str> = values.iter().map(String::as_str).collect();
519            let encoded = PlainEncoder::encode_string(&refs).unwrap();
520            let decoded = PlainDecoder::decode_string(&encoded).unwrap();
521            prop_assert_eq!(&values, &decoded);
522        }
523    }
524}