Skip to main content

brain_db_sdk/wire/
cbor.rs

1//! CBOR payload codec + trailing raw-vector section.
2//!
3//! A wire payload is a self-describing CBOR map carrying the structured
4//! fields, optionally followed by a raw little-endian `f32` block for
5//! embedding vectors. Vectors stay out of the CBOR so the floats keep
6//! full precision (CBOR would tag each one and risk half-precision
7//! rounding) and stay contiguous for a bulk copy.
8//!
9//! Encoding is reproducible: a given value always produces the same
10//! bytes (fixed struct field order, shortest-form integers from
11//! `ciborium`). That stability is what lets the conformance corpus pin
12//! the format.
13
14use serde::de::DeserializeOwned;
15use serde::Serialize;
16
17/// Failures decoding a CBOR payload.
18#[derive(Debug, thiserror::Error)]
19pub enum CborError {
20    /// The CBOR item failed to decode (truncated, type mismatch, …).
21    #[error("CBOR decode failed: {0}")]
22    Decode(String),
23    /// A complete CBOR item decoded but extra bytes followed it where
24    /// none were expected (non-vector payloads have no trailing section).
25    #[error("trailing bytes after CBOR payload")]
26    TrailingBytes,
27    /// A trailing raw-vector section was not a whole number of 4-byte
28    /// floats.
29    #[error("vector section is {0} bytes, not a multiple of 4")]
30    RaggedVector(usize),
31}
32
33/// Serialize a value to a fresh CBOR byte vector.
34///
35/// # Panics
36///
37/// Never, for any value this crate serializes. `ciborium`'s only error path
38/// when writing to an in-memory `Vec` is the writer failing, which a `Vec`
39/// does not do short of allocation failure — so the `expect` documents an
40/// invariant rather than guarding a reachable case.
41pub fn to_cbor_bytes<T: Serialize>(value: &T) -> Vec<u8> {
42    let mut buf = Vec::new();
43    ciborium::into_writer(value, &mut buf)
44        .expect("invariant: CBOR encode of an owned value is infallible");
45    buf
46}
47
48/// Decode a CBOR `T`, requiring the whole buffer to be consumed.
49/// Trailing bytes after a complete item mean a malformed frame.
50pub fn from_cbor_bytes<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CborError> {
51    let mut cursor = std::io::Cursor::new(bytes);
52    let value: T =
53        ciborium::from_reader(&mut cursor).map_err(|e| CborError::Decode(e.to_string()))?;
54    // `position()` is a u64, but it indexes a `&[u8]` whose length is a usize,
55    // so it cannot exceed `usize::MAX`. `try_from` states that rather than
56    // leaving a silent truncation on 32-bit targets to inspection.
57    let consumed = usize::try_from(cursor.position()).map_err(|_| CborError::TrailingBytes)?;
58    if consumed != bytes.len() {
59        return Err(CborError::TrailingBytes);
60    }
61    Ok(value)
62}
63
64/// Decode a CBOR `T` from the front of `bytes` and report how many bytes
65/// the CBOR section consumed, so the caller can read a trailing raw
66/// section from `bytes[consumed..]`.
67pub fn from_cbor_prefix<T: DeserializeOwned>(bytes: &[u8]) -> Result<(T, usize), CborError> {
68    let mut cursor = std::io::Cursor::new(bytes);
69    let value: T =
70        ciborium::from_reader(&mut cursor).map_err(|e| CborError::Decode(e.to_string()))?;
71    // Same bound as `from_cbor_bytes`: the cursor indexes `bytes`, so its
72    // position fits a usize by construction.
73    let consumed = usize::try_from(cursor.position()).map_err(|_| CborError::TrailingBytes)?;
74    Ok((value, consumed))
75}
76
77/// Pack an `f32` slice into a contiguous little-endian byte vector for
78/// the trailing raw-vector section.
79#[must_use]
80pub fn f32_slice_to_le_bytes(v: &[f32]) -> Vec<u8> {
81    let mut out = Vec::with_capacity(v.len() * 4);
82    for f in v {
83        out.extend_from_slice(&f.to_le_bytes());
84    }
85    out
86}
87
88/// Read a little-endian `f32` vector from the trailing raw section. The
89/// byte length must be a whole number of 4-byte floats. Reads in 4-byte
90/// chunks rather than casting, so it is alignment-safe over a socket
91/// buffer.
92pub fn le_bytes_to_f32_vec(bytes: &[u8]) -> Result<Vec<f32>, CborError> {
93    if bytes.len() % 4 != 0 {
94        return Err(CborError::RaggedVector(bytes.len()));
95    }
96    Ok(bytes
97        .chunks_exact(4)
98        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
99        .collect())
100}
101
102/// Serde adapter for `Vec<[u8; 16]>` of UUID-shaped wire ids: each id
103/// encodes as its own CBOR byte string (major type 2) inside an array.
104///
105/// `serde_bytes` flattens a single contiguous buffer, so a vector of
106/// distinct 16-byte ids must keep each element framed separately. Use
107/// via `#[serde(with = "vec_byte_array16")]`.
108pub mod vec_byte_array16 {
109    use serde::de::{Deserializer, SeqAccess, Visitor};
110    use serde::ser::SerializeSeq;
111    use std::fmt;
112
113    /// Write each id as its own CBOR byte string, framed separately.
114    pub fn serialize<S>(v: &[[u8; 16]], serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: serde::ser::Serializer,
117    {
118        let mut seq = serializer.serialize_seq(Some(v.len()))?;
119        for id in v {
120            seq.serialize_element(serde_bytes::Bytes::new(id))?;
121        }
122        seq.end()
123    }
124
125    /// Read a CBOR sequence of 16-byte strings back into fixed-size arrays.
126    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<[u8; 16]>, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        struct V;
131        impl<'de> Visitor<'de> for V {
132            type Value = Vec<[u8; 16]>;
133            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134                f.write_str("a sequence of 16-byte byte strings")
135            }
136            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
137            where
138                A: SeqAccess<'de>,
139            {
140                // The size hint echoes the array length the *peer*
141                // declared, so a small frame could claim a huge element
142                // count and force an oversized up-front allocation.
143                // Reserve a bounded floor and let `push` grow to the real
144                // size as elements actually decode.
145                const PREALLOC_FLOOR: usize = 64;
146                let hint = seq.size_hint().unwrap_or(0).min(PREALLOC_FLOOR);
147                let mut out = Vec::with_capacity(hint);
148                while let Some(b) = seq.next_element::<serde_bytes::ByteBuf>()? {
149                    let arr: [u8; 16] = b.as_ref().try_into().map_err(|_| {
150                        serde::de::Error::invalid_length(b.len(), &"exactly 16 bytes")
151                    })?;
152                    out.push(arr);
153                }
154                Ok(out)
155            }
156        }
157        deserializer.deserialize_seq(V)
158    }
159}
160
161/// Serde adapter for `Option<Vec<[u8; 16]>>`, deferring to
162/// [`vec_byte_array16`] for the `Some` case. Use via
163/// `#[serde(with = "opt_vec_byte_array16")]`.
164pub mod opt_vec_byte_array16 {
165    use serde::de::Deserializer;
166
167    /// As [`vec_byte_array16::serialize`], but omits the field when `None`.
168    pub fn serialize<S>(v: &Option<Vec<[u8; 16]>>, serializer: S) -> Result<S::Ok, S::Error>
169    where
170        S: serde::ser::Serializer,
171    {
172        match v {
173            None => serializer.serialize_none(),
174            Some(inner) => {
175                #[derive(serde::Serialize)]
176                struct W<'a>(#[serde(with = "super::vec_byte_array16")] &'a [[u8; 16]]);
177                serializer.serialize_some(&W(inner))
178            }
179        }
180    }
181
182    /// As [`vec_byte_array16::deserialize`], yielding `None` for a missing
183    /// or null field.
184    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<[u8; 16]>>, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        use serde::Deserialize as _;
189        #[derive(serde::Deserialize)]
190        struct W(#[serde(with = "super::vec_byte_array16")] Vec<[u8; 16]>);
191        let opt: Option<W> = Option::deserialize(deserializer)?;
192        Ok(opt.map(|w| w.0))
193    }
194}
195
196/// Serde adapter for `Option<[u8; 16]>`, encoding the `Some` payload as a
197/// CBOR byte string (major type 2) rather than an array of `u8`.
198///
199/// `serde_bytes` cannot reach through `Option`: applied to an
200/// `Option<[u8; 16]>` field it serializes the inner array element-wise,
201/// producing a CBOR array. Routing the inner array through
202/// `serde_bytes::Bytes` keeps a present id a single 16-byte byte string,
203/// so every wire id has one encoding regardless of how it is wrapped. Use
204/// via `#[serde(with = "opt_byte_array16")]`.
205pub(crate) mod opt_byte_array16 {
206    use serde::de::Deserializer;
207
208    pub(crate) fn serialize<S>(v: &Option<[u8; 16]>, serializer: S) -> Result<S::Ok, S::Error>
209    where
210        S: serde::ser::Serializer,
211    {
212        match v {
213            None => serializer.serialize_none(),
214            Some(id) => serializer.serialize_some(serde_bytes::Bytes::new(id)),
215        }
216    }
217
218    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 16]>, D::Error>
219    where
220        D: Deserializer<'de>,
221    {
222        use serde::Deserialize as _;
223        let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
224        match opt {
225            None => Ok(None),
226            Some(b) => {
227                let arr: [u8; 16] = b
228                    .as_ref()
229                    .try_into()
230                    .map_err(|_| serde::de::Error::invalid_length(b.len(), &"exactly 16 bytes"))?;
231                Ok(Some(arr))
232            }
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
242    struct Sample {
243        a: u32,
244        b: String,
245    }
246
247    #[test]
248    fn round_trip_whole_buffer() {
249        let s = Sample {
250            a: 7,
251            b: "hi".into(),
252        };
253        let bytes = to_cbor_bytes(&s);
254        let back: Sample = from_cbor_bytes(&bytes).expect("decode");
255        assert_eq!(s, back);
256    }
257
258    #[test]
259    fn trailing_bytes_rejected() {
260        let s = Sample {
261            a: 1,
262            b: "x".into(),
263        };
264        let mut bytes = to_cbor_bytes(&s);
265        bytes.push(0xFF);
266        assert!(matches!(
267            from_cbor_bytes::<Sample>(&bytes),
268            Err(CborError::TrailingBytes)
269        ));
270    }
271
272    #[test]
273    fn prefix_reports_consumed_and_vector_round_trips() {
274        let s = Sample {
275            a: 3,
276            b: "v".into(),
277        };
278        let vec = vec![1.0f32, -2.5, 3.25];
279        let mut payload = to_cbor_bytes(&s);
280        let cbor_len = payload.len();
281        payload.extend_from_slice(&f32_slice_to_le_bytes(&vec));
282
283        let (back, consumed): (Sample, usize) = from_cbor_prefix(&payload).expect("prefix");
284        assert_eq!(back, s);
285        assert_eq!(consumed, cbor_len);
286        let got = le_bytes_to_f32_vec(&payload[consumed..]).expect("vec");
287        assert_eq!(got, vec);
288    }
289
290    #[test]
291    fn ragged_vector_section_rejected() {
292        assert!(matches!(
293            le_bytes_to_f32_vec(&[0u8, 1, 2]),
294            Err(CborError::RaggedVector(3))
295        ));
296    }
297}