Skip to main content

loonfs_api/
envelope.rs

1//! Shared encoding and validation for durable envelopes.
2//!
3//! Durable objects declare `kind`, `format_version`, and `payload_checksum`
4//! before their payload. Checksums cover the stored payload bytes rather than
5//! a re-encoding. JSON control objects and CBOR WAL segments use the same
6//! validation rules and errors.
7
8use crate::digest::sha256_digest;
9use serde::de::DeserializeOwned;
10use serde::{Deserialize, Serialize};
11use serde_json::value::RawValue;
12use thiserror::Error;
13
14/// Identifying prefix of a durable envelope document.
15///
16/// Readers decode this probe before the full document so that objects written
17/// with an unknown kind or an unsupported format version fail with a precise
18/// error instead of a generic decode error. `kind` is deliberately a string
19/// (not an enum) so future kinds remain reportable.
20#[derive(Debug, Deserialize)]
21pub struct EnvelopeProbe {
22    /// Durable family declared by the stored object.
23    pub kind: String,
24    /// Family-independent version gate declared by the stored object.
25    pub format_version: u32,
26}
27
28/// Failure vocabulary shared by every envelope codec. Messages are
29/// envelope-generic; the wrapping error names the object (and its key) the
30/// bytes came from.
31#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum EnvelopeCodecError {
34    /// Reports a payload that cannot be serialized into its family's durable encoding.
35    #[error("failed to encode envelope payload: {0}")]
36    PayloadEncode(String),
37    /// Reports an envelope document that cannot be serialized around an encoded payload.
38    #[error("failed to encode envelope document: {0}")]
39    EnvelopeEncode(String),
40    /// Reports bytes that do not decode as the shared durable envelope layout.
41    #[error("failed to decode envelope document: {0}")]
42    EnvelopeDecode(String),
43    /// Reports a verified envelope whose opaque payload does not decode as its declared family.
44    #[error("failed to decode envelope payload: {0}")]
45    PayloadDecode(String),
46    /// Reports an envelope that the configured transport codec could not compress.
47    #[error("failed to compress envelope: {0}")]
48    Compress(String),
49    /// Reports stored bytes that the configured transport codec could not decompress.
50    #[error("failed to decompress envelope: {0}")]
51    Decompress(String),
52    /// Reports a WAL document that exceeds the decompressed format limit.
53    #[error("decompressed WAL document exceeds the {max_bytes}-byte limit")]
54    WalSegmentTooLarge {
55        /// Largest accepted decompressed document.
56        max_bytes: usize,
57    },
58    /// Reports inline content that violates the WAL format rules.
59    #[error(
60        "invalid wal inline content in commit `{seq}` for `content_id` `{content_id}`: {reason}"
61    )]
62    InvalidWalInlineContent {
63        /// Commit containing the rejected entry.
64        seq: crate::ChangeSeq,
65        /// Content whose entry violates a rule or exceeds the segment total.
66        content_id: crate::ContentId,
67        /// Format rule violated by the entry.
68        reason: &'static str,
69    },
70    /// Reports an unrecognized durable-family discriminator found during the envelope probe.
71    #[error("unknown envelope kind `{found}`")]
72    UnknownKind {
73        /// Untrusted `kind` spelling decoded from the stored object.
74        found: String,
75    },
76    /// Reports a valid discriminator that does not belong to the decoder the caller selected.
77    #[error("envelope kind mismatch: expected `{expected}`, found `{found}`")]
78    KindMismatch {
79        /// Durable family the selected decoder accepts.
80        expected: String,
81        /// Durable family declared by the stored object.
82        found: String,
83    },
84    /// Reports a known durable family whose format version this build cannot read.
85    #[error(
86        "unsupported `{kind}` envelope format version `{found}`: \
87         this build supports `{supported}`"
88    )]
89    UnsupportedFormatVersion {
90        /// Durable family whose independent version gate rejected the object.
91        kind: String,
92        /// Version declared by the stored object.
93        found: u32,
94        /// Sole version this build reads and writes for `kind`.
95        supported: u32,
96    },
97    /// Reports stored payload bytes that do not match the checksum recorded beside them.
98    #[error("envelope payload checksum mismatch: expected `{expected}`, actual `{actual}`")]
99    ChecksumMismatch {
100        /// Digest recorded in the durable envelope.
101        expected: String,
102        /// Digest recomputed over the exact stored payload bytes.
103        actual: String,
104    },
105}
106
107/// Requires the probed kind to be exactly `expected`.
108pub fn verify_kind(expected: &str, found: &str) -> Result<(), EnvelopeCodecError> {
109    if found != expected {
110        return Err(EnvelopeCodecError::KindMismatch {
111            expected: expected.to_owned(),
112            found: found.to_owned(),
113        });
114    }
115    Ok(())
116}
117
118/// Requires the probed format version to be exactly what this build writes
119/// for `kind` — no envelope family tolerates version skew.
120pub fn verify_version(kind: &str, found: u32, supported: u32) -> Result<(), EnvelopeCodecError> {
121    if found != supported {
122        return Err(EnvelopeCodecError::UnsupportedFormatVersion {
123            kind: kind.to_owned(),
124            found,
125            supported,
126        });
127    }
128    Ok(())
129}
130
131/// Requires the stored checksum to match the payload bytes as stored.
132pub fn verify_payload_checksum(
133    expected: &str,
134    payload_bytes: &[u8],
135) -> Result<(), EnvelopeCodecError> {
136    let actual = sha256_digest(payload_bytes);
137    if actual != expected {
138        return Err(EnvelopeCodecError::ChecksumMismatch {
139            expected: expected.to_owned(),
140            actual,
141        });
142    }
143    Ok(())
144}
145
146/// Durable layout of a JSON-bodied envelope: the shared fields plus the
147/// payload as a raw JSON fragment, kept inline so the object remains
148/// directly readable JSON while `payload_checksum` covers the exact
149/// fragment bytes as stored.
150#[derive(Serialize, Deserialize)]
151#[serde(deny_unknown_fields)]
152struct JsonEnvelopeDocument {
153    kind: String,
154    format_version: u32,
155    payload_checksum: String,
156    payload: Box<RawValue>,
157}
158
159/// An envelope whose framing was verified on read or derived on write.
160///
161/// Payload access is read-only: changing a payload requires a new encoding.
162/// Family codecs apply their own kind, version, and payload validation rules.
163/// Kind and version are checked at the codec boundary, not retained as caller state.
164/// This type has no serde decoder; durable reads must use a checked codec.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct VerifiedEnvelope<T> {
167    pub(crate) payload_checksum: String,
168    pub(crate) payload: T,
169}
170
171impl<T> VerifiedEnvelope<T> {
172    /// Checksum of the exact stored payload bytes.
173    pub fn payload_checksum(&self) -> &str {
174        &self.payload_checksum
175    }
176    /// Payload protected by this framing. Clone it to prepare a changed successor.
177    pub fn payload(&self) -> &T {
178        &self.payload
179    }
180    /// Takes the payload out of its verified framing for reuse or modification.
181    pub fn into_payload(self) -> T {
182        self.payload
183    }
184}
185
186/// One encoding and the envelope derived from those exact bytes.
187///
188/// Only codecs construct this pair. Neither half can be changed in place.
189/// Readers retain just [`VerifiedEnvelope`], without a second copy of the bytes.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct EncodedEnvelope<T> {
192    pub(crate) envelope: VerifiedEnvelope<T>,
193    pub(crate) bytes: Vec<u8>,
194    pub(crate) document_len: usize,
195}
196
197impl<T> EncodedEnvelope<T> {
198    /// Returns the document length before any compression.
199    pub fn document_len(&self) -> usize {
200        self.document_len
201    }
202
203    /// Envelope derived while encoding, without decoding or serializing again.
204    pub fn envelope(&self) -> &VerifiedEnvelope<T> {
205        &self.envelope
206    }
207    /// Complete durable document, ready to write.
208    pub fn as_bytes(&self) -> &[u8] {
209        &self.bytes
210    }
211    /// Takes the complete durable document.
212    pub fn into_bytes(self) -> Vec<u8> {
213        self.bytes
214    }
215    /// Discards the encoding and retains its verified envelope.
216    pub fn into_envelope(self) -> VerifiedEnvelope<T> {
217        self.envelope
218    }
219    /// Separates the immutable envelope and its bytes for storage publication.
220    pub fn into_parts(self) -> (VerifiedEnvelope<T>, Vec<u8>) {
221        (self.envelope, self.bytes)
222    }
223}
224
225/// Serializes the payload once and derives framing from those same bytes.
226pub fn encode_json_envelope<T: Serialize>(
227    kind: &str,
228    format_version: u32,
229    payload: T,
230) -> Result<EncodedEnvelope<T>, EnvelopeCodecError> {
231    let payload_json = serde_json::to_string(&payload)
232        .map_err(|err| EnvelopeCodecError::PayloadEncode(err.to_string()))?;
233    let payload_checksum = sha256_digest(payload_json.as_bytes());
234    let document = JsonEnvelopeDocument {
235        kind: kind.to_owned(),
236        format_version,
237        payload_checksum: payload_checksum.clone(),
238        payload: RawValue::from_string(payload_json)
239            .map_err(|err| EnvelopeCodecError::PayloadEncode(err.to_string()))?,
240    };
241    let bytes = serde_json::to_vec(&document)
242        .map_err(|err| EnvelopeCodecError::EnvelopeEncode(err.to_string()))?;
243    Ok(EncodedEnvelope {
244        envelope: VerifiedEnvelope {
245            payload_checksum,
246            payload,
247        },
248        document_len: bytes.len(),
249        bytes,
250    })
251}
252
253/// Decodes one JSON-bodied envelope, then checks its kind, version, checksum,
254/// and payload. Unknown envelope fields are rejected for every durable family.
255///
256/// `classify_kind` lets a family with a kind registry report unknown kinds
257/// distinctly from mismatched ones; families with one kind pass
258/// [`verify_kind`] directly.
259pub fn decode_json_envelope<T: DeserializeOwned>(
260    bytes: &[u8],
261    supported_version: u32,
262    classify_kind: impl FnOnce(&str) -> Result<(), EnvelopeCodecError>,
263) -> Result<VerifiedEnvelope<T>, EnvelopeCodecError> {
264    let probe: EnvelopeProbe = serde_json::from_slice(bytes)
265        .map_err(|err| EnvelopeCodecError::EnvelopeDecode(err.to_string()))?;
266    classify_kind(&probe.kind)?;
267    verify_version(&probe.kind, probe.format_version, supported_version)?;
268    let document: JsonEnvelopeDocument = serde_json::from_slice(bytes)
269        .map_err(|err| EnvelopeCodecError::EnvelopeDecode(err.to_string()))?;
270    decode_json_envelope_payload(document)
271}
272
273fn decode_json_envelope_payload<T: DeserializeOwned>(
274    document: JsonEnvelopeDocument,
275) -> Result<VerifiedEnvelope<T>, EnvelopeCodecError> {
276    verify_payload_checksum(
277        &document.payload_checksum,
278        document.payload.get().as_bytes(),
279    )?;
280    let payload: T = serde_json::from_str(document.payload.get())
281        .map_err(|err| EnvelopeCodecError::PayloadDecode(err.to_string()))?;
282
283    Ok(VerifiedEnvelope {
284        payload_checksum: document.payload_checksum,
285        payload,
286    })
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use std::cell::Cell;
293
294    #[test]
295    fn encoding_serializes_the_payload_once_and_checksums_those_bytes() {
296        struct CountedPayload<'a>(&'a Cell<usize>);
297        impl Serialize for CountedPayload<'_> {
298            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
299                self.0.set(self.0.get() + 1);
300                serializer.serialize_u64(42)
301            }
302        }
303        let calls = Cell::new(0);
304        let encoded = encode_json_envelope("test", 1, CountedPayload(&calls)).expect("encode");
305        let document: JsonEnvelopeDocument =
306            serde_json::from_slice(encoded.as_bytes()).expect("document");
307        assert_eq!(calls.get(), 1);
308        assert_eq!(document.payload.get(), "42");
309        assert_eq!(encoded.envelope().payload_checksum(), sha256_digest(b"42"));
310        assert_eq!(
311            document.payload_checksum,
312            encoded.envelope().payload_checksum()
313        );
314    }
315
316    #[test]
317    fn decoding_checks_the_stored_payload_including_noncanonical_whitespace() {
318        let payload = r#"{ "value" : 42 }"#;
319        let checksum = sha256_digest(payload.as_bytes());
320        let bytes = format!(
321            r#"{{"kind":"test","format_version":1,"payload_checksum":"{checksum}","payload":{payload}}}"#
322        );
323        let decoded: VerifiedEnvelope<serde_json::Value> =
324            decode_json_envelope(bytes.as_bytes(), 1, |kind| verify_kind("test", kind))
325                .expect("decode exact stored bytes");
326        assert_eq!(decoded.payload_checksum(), checksum);
327        let successor =
328            encode_json_envelope("test", 1, decoded.into_payload()).expect("canonical encoding");
329        assert_ne!(successor.envelope().payload_checksum(), checksum);
330        let reread: VerifiedEnvelope<serde_json::Value> =
331            decode_json_envelope(successor.as_bytes(), 1, |kind| verify_kind("test", kind))
332                .expect("decode canonical bytes");
333        assert_eq!(&reread, successor.envelope());
334    }
335}