Skip to main content

argus/
codec.rs

1//! Chunk payload codecs shared by Tetration and Tessera.
2//!
3//! Wire tags: **`0` = raw** (`stored_byte_len == raw_byte_len`), **`1` = zstd**
4//! (frame that decompresses to `raw_byte_len` bytes).
5//!
6//! Decoding validates the logical length recorded by the caller. It does not
7//! validate file offsets or stored spans; use [`crate::le`] before passing a
8//! slice into [`decode`].
9
10use std::borrow::Cow;
11
12/// Payload storage codec represented as a `u32` on disk.
13///
14/// The numeric discriminants are part of the shared wire contract and must
15/// not be renumbered.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(u32)]
18pub enum PayloadCodec {
19    /// Stored bytes are the raw payload (`stored_byte_len == raw_byte_len`).
20    Raw = 0,
21    /// Stored bytes are one zstd frame decoding to `raw_byte_len` bytes.
22    Zstd = 1,
23}
24
25impl PayloadCodec {
26    /// Convert a wire discriminant into a supported codec.
27    ///
28    /// # Errors
29    ///
30    /// Returns [`CodecError::Unsupported`] for unknown tags.
31    pub fn from_u32(value: u32) -> Result<Self, CodecError> {
32        match value {
33            0 => Ok(Self::Raw),
34            1 => Ok(Self::Zstd),
35            other => Err(CodecError::Unsupported { codec: other }),
36        }
37    }
38
39    /// Return the stable `u32` discriminant stored on disk.
40    #[must_use]
41    pub const fn as_u32(self) -> u32 {
42        self as u32
43    }
44}
45
46/// Failures while selecting, encoding, or decoding a payload codec.
47#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
48pub enum CodecError {
49    /// Unknown codec tag.
50    #[error("unsupported payload codec {codec}")]
51    Unsupported {
52        /// Wire `u32` tag.
53        codec: u32,
54    },
55    /// Raw codec requires equal raw and stored lengths.
56    #[error("raw codec: raw_byte_len {raw} != stored_byte_len {stored}")]
57    RawStoredMismatch {
58        /// Declared uncompressed length.
59        raw: u64,
60        /// Declared on-disk length.
61        stored: u64,
62    },
63    /// Decompressed length did not match the index.
64    #[error("decoded {decoded} bytes, index says raw_byte_len {raw}")]
65    DecodedLengthMismatch {
66        /// Actual decompressed length.
67        decoded: usize,
68        /// Expected `raw_byte_len`.
69        raw: u64,
70    },
71    /// zstd compress or decompress failed.
72    #[error("zstd: {0}")]
73    Zstd(String),
74}
75
76/// Encode uncompressed bytes using the selected wire codec.
77///
78/// Raw encoding copies `raw` unchanged. Zstd uses the library's default
79/// compression level (`0`) so the format does not prescribe a tuning level.
80///
81/// # Errors
82///
83/// Returns [`CodecError::Zstd`] when compression fails.
84pub fn encode(codec: PayloadCodec, raw: &[u8]) -> Result<Vec<u8>, CodecError> {
85    match codec {
86        PayloadCodec::Raw => Ok(raw.to_vec()),
87        PayloadCodec::Zstd => zstd::encode_all(raw, 0).map_err(|e| CodecError::Zstd(e.to_string())),
88    }
89}
90
91/// Decode stored chunk bytes to uncompressed payload (`raw_byte_len` bytes).
92///
93/// Raw → [`Cow::Borrowed`]; zstd → owned. For raw, `stored.len()` must equal
94/// `raw_byte_len`.
95///
96/// The `Cow` return avoids allocating for memory-mapped raw chunks while
97/// preserving one API for compressed and uncompressed payloads.
98///
99/// # Errors
100///
101/// Returns [`CodecError`] on length mismatch or zstd failure.
102pub fn decode<'a>(
103    codec: PayloadCodec,
104    stored: &'a [u8],
105    raw_byte_len: u64,
106) -> Result<Cow<'a, [u8]>, CodecError> {
107    match codec {
108        PayloadCodec::Raw => {
109            let stored_len = stored.len() as u64;
110            if stored_len != raw_byte_len {
111                return Err(CodecError::RawStoredMismatch {
112                    raw: raw_byte_len,
113                    stored: stored_len,
114                });
115            }
116            // Keep raw mmap-backed payloads zero-copy.
117            Ok(Cow::Borrowed(stored))
118        }
119        PayloadCodec::Zstd => {
120            let dec = zstd::decode_all(stored).map_err(|e| CodecError::Zstd(e.to_string()))?;
121            // A valid zstd frame is not enough: its logical size must agree
122            // with the format-specific index row supplied by the caller.
123            if dec.len() as u64 != raw_byte_len {
124                return Err(CodecError::DecodedLengthMismatch {
125                    decoded: dec.len(),
126                    raw: raw_byte_len,
127                });
128            }
129            Ok(Cow::Owned(dec))
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn raw_round_trip_borrows() {
140        let raw = b"hello chunk";
141        let stored = encode(PayloadCodec::Raw, raw).unwrap();
142        assert_eq!(stored.as_slice(), raw);
143        let out = decode(PayloadCodec::Raw, &stored, raw.len() as u64).unwrap();
144        assert!(matches!(out, Cow::Borrowed(_)));
145        assert_eq!(&*out, raw);
146    }
147
148    #[test]
149    fn zstd_round_trip() {
150        let raw = b"compress me please........";
151        let stored = encode(PayloadCodec::Zstd, raw).unwrap();
152        assert_ne!(stored.as_slice(), raw);
153        let out = decode(PayloadCodec::Zstd, &stored, raw.len() as u64).unwrap();
154        assert_eq!(&*out, raw);
155    }
156
157    #[test]
158    fn raw_rejects_length_mismatch() {
159        let err = decode(PayloadCodec::Raw, b"ab", 3).unwrap_err();
160        assert_eq!(err, CodecError::RawStoredMismatch { raw: 3, stored: 2 });
161    }
162
163    #[test]
164    fn from_u32_rejects_unknown() {
165        assert!(matches!(
166            PayloadCodec::from_u32(99),
167            Err(CodecError::Unsupported { codec: 99 })
168        ));
169    }
170}