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}