Skip to main content

musli_web/
format.rs

1//! Runtime dispatch over the [`Format`]s which message bodies can be encoded
2//! with.
3//!
4//! Dispatch has to happen at runtime rather than through generics, since a
5//! server adapts to whichever format a client asks for. See the [wire format]
6//! for details.
7//!
8//! [wire format]: crate::api#wire-format
9
10use core::fmt;
11
12use alloc::vec::Vec;
13
14use musli::alloc::Global;
15use musli::mode::Binary;
16use musli::reader::SliceReader;
17use musli::{Decode, Encode};
18
19use crate::api::{DecodeBody, EncodeBody, Format};
20
21/// Encode the fixed envelope of a message.
22///
23/// The envelope is always encoded with [`musli::packed`] regardless of which
24/// format has been negotiated for bodies, which is what allows both peers to
25/// read it unconditionally.
26// NB: Only the modules which speak the protocol have any use for envelopes.
27#[cfg_attr(
28    not(any(feature = "ws", feature = "client", feature = "web03")),
29    allow(dead_code)
30)]
31#[inline]
32pub(crate) fn encode_envelope<T>(out: &mut Vec<u8>, value: &T) -> Result<(), Error>
33where
34    T: ?Sized + Encode<Binary>,
35{
36    musli::packed::encode(out, value).map_err(Error::packed)?;
37    Ok(())
38}
39
40/// Decode the fixed envelope of a message, advancing `at` past it.
41#[cfg_attr(
42    not(any(feature = "ws", feature = "client", feature = "web03")),
43    allow(dead_code)
44)]
45#[inline]
46pub(crate) fn decode_envelope<'de, T>(buf: &'de [u8], at: &mut usize) -> Result<T, Error>
47where
48    T: Decode<'de, Binary, Global>,
49{
50    let Some(tail) = buf.get(*at..) else {
51        return Err(Error::new(ErrorKind::Overflow {
52            at: *at,
53            len: buf.len(),
54        }));
55    };
56
57    let mut reader = SliceReader::new(tail);
58    let value = musli::packed::decode(&mut reader).map_err(Error::packed)?;
59    *at += tail.len() - reader.remaining();
60    Ok(value)
61}
62
63/// Encode `value` with the given format, appending it to `out`.
64macro_rules! encode_with {
65    ($module:ident, $out:expr, $value:expr, $variant:ident) => {{
66        musli::$module::encode($out, $value)
67            .map(|_| ())
68            .map_err(Error::$variant)
69    }};
70}
71
72/// Decode a value with the given format from `buf` at `at`, advancing `at` past
73/// what was consumed.
74macro_rules! decode_with {
75    ($module:ident, $tail:expr, $at:expr, $len:expr, $variant:ident) => {{
76        let mut reader = SliceReader::new($tail);
77        let value = musli::$module::decode(&mut reader).map_err(Error::$variant)?;
78        *$at += $tail.len() - reader.remaining();
79        let _ = $len;
80        Ok(value)
81    }};
82}
83
84impl Format {
85    /// Test if this build of the crate has support for the format.
86    ///
87    /// Formats are gated behind features, so a peer might genuinely be unable
88    /// to speak a format which the other side asks for. This is what the
89    /// [negotiation protocol] uses to decide whether a request can be honored.
90    ///
91    /// [negotiation protocol]: crate::api#negotiating-the-format
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use musli_web::api::Format;
97    ///
98    /// // The default format is always available.
99    /// assert!(Format::DEFAULT.is_supported());
100    /// ```
101    #[inline]
102    pub const fn is_supported(self) -> bool {
103        match self {
104            Format::Packed => cfg!(feature = "format-packed"),
105            Format::Storage => cfg!(feature = "format-storage"),
106            Format::Wire => cfg!(feature = "format-wire"),
107            Format::Descriptive => cfg!(feature = "format-descriptive"),
108            Format::Json => cfg!(feature = "format-json"),
109        }
110    }
111
112    /// Iterate over every format this build of the crate supports.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use musli_web::api::Format;
118    ///
119    /// assert!(Format::supported().any(|f| f == Format::DEFAULT));
120    /// ```
121    #[inline]
122    pub fn supported() -> impl Iterator<Item = Format> {
123        Format::ALL.iter().copied().filter(|f| f.is_supported())
124    }
125
126    /// Encode `value` with this format, appending it to `out`.
127    #[cfg_attr(
128        not(any(feature = "ws", feature = "client", feature = "web03")),
129        allow(dead_code)
130    )]
131    pub(crate) fn encode<T>(self, out: &mut Vec<u8>, value: &T) -> Result<(), Error>
132    where
133        T: ?Sized + EncodeBody,
134    {
135        match self {
136            #[cfg(feature = "format-packed")]
137            Format::Packed => encode_with!(packed, out, value, packed),
138            #[cfg(feature = "format-storage")]
139            Format::Storage => encode_with!(storage, out, value, storage),
140            #[cfg(feature = "format-wire")]
141            Format::Wire => encode_with!(wire, out, value, wire),
142            #[cfg(feature = "format-descriptive")]
143            Format::Descriptive => encode_with!(descriptive, out, value, descriptive),
144            #[cfg(feature = "format-json")]
145            Format::Json => musli::json::encode(out, value)
146                .map(|_| ())
147                .map_err(Error::json),
148            #[allow(unreachable_patterns)]
149            _ => Err(Error::unsupported(self)),
150        }
151    }
152
153    /// Decode a value with this format from `buf` starting at `at`, advancing
154    /// `at` past what was consumed.
155    ///
156    /// Advancing `at` is what allows several payloads to be decoded in sequence
157    /// out of a single message.
158    #[cfg_attr(
159        not(any(feature = "ws", feature = "client", feature = "web03")),
160        allow(dead_code)
161    )]
162    pub(crate) fn decode<'de, T>(self, buf: &'de [u8], at: &mut usize) -> Result<T, Error>
163    where
164        T: DecodeBody<'de>,
165    {
166        let Some(tail) = buf.get(*at..) else {
167            return Err(Error::new(ErrorKind::Overflow {
168                at: *at,
169                len: buf.len(),
170            }));
171        };
172
173        match self {
174            #[cfg(feature = "format-packed")]
175            Format::Packed => decode_with!(packed, tail, at, buf.len(), packed),
176            #[cfg(feature = "format-storage")]
177            Format::Storage => decode_with!(storage, tail, at, buf.len(), storage),
178            #[cfg(feature = "format-wire")]
179            Format::Wire => decode_with!(wire, tail, at, buf.len(), wire),
180            #[cfg(feature = "format-descriptive")]
181            Format::Descriptive => decode_with!(descriptive, tail, at, buf.len(), descriptive),
182            #[cfg(feature = "format-json")]
183            Format::Json => {
184                // NB: The borrow here is load-bearing. A `&mut &[u8]` selects
185                // the parser which keeps the referenced slice up to date as it
186                // parses, which is how the consumed length is recovered for a
187                // format that is not read through a `Reader`. Passing the slice
188                // by value selects a parser which does not write back, and the
189                // position would silently never advance.
190                let mut rest = tail;
191                let cursor = &mut rest;
192                let value = musli::json::decode(cursor).map_err(Error::json)?;
193                *at += tail.len() - rest.len();
194                Ok(value)
195            }
196            #[allow(unreachable_patterns)]
197            _ => Err(Error::unsupported(self)),
198        }
199    }
200}
201
202/// An error raised when encoding or decoding a message.
203#[derive(Debug)]
204pub struct Error {
205    kind: ErrorKind,
206}
207
208impl Error {
209    #[inline]
210    const fn new(kind: ErrorKind) -> Self {
211        Self { kind }
212    }
213
214    /// Construct an error indicating that the given format is not supported by
215    /// this build of the crate.
216    #[inline]
217    pub(crate) const fn unsupported(format: Format) -> Self {
218        Self::new(ErrorKind::Unsupported(format))
219    }
220
221    /// Test if the error is caused by a format which is not supported, and if
222    /// so return it.
223    #[inline]
224    pub fn unsupported_format(&self) -> Option<Format> {
225        match self.kind {
226            ErrorKind::Unsupported(format) => Some(format),
227            _ => None,
228        }
229    }
230}
231
232macro_rules! error_kinds {
233    ($($(#[$meta:meta])* $variant:ident, $ctor:ident, $ty:path;)*) => {
234        #[derive(Debug)]
235        enum ErrorKind {
236            Unsupported(Format),
237            Overflow { at: usize, len: usize },
238            $($(#[$meta])* $variant($ty),)*
239        }
240
241        impl Error {
242            $(
243                $(#[$meta])*
244                #[inline]
245                fn $ctor(error: $ty) -> Self {
246                    Self::new(ErrorKind::$variant(error))
247                }
248            )*
249        }
250
251        impl fmt::Display for Error {
252            #[inline]
253            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254                match &self.kind {
255                    ErrorKind::Unsupported(format) => {
256                        write!(f, "Format `{format}` is not supported")
257                    }
258                    ErrorKind::Overflow { at, len } => {
259                        write!(f, "Offset {at} is out of bounds for a message of {len} bytes")
260                    }
261                    $($(#[$meta])* ErrorKind::$variant(..) => {
262                        write!(f, concat!("Error in the `", stringify!($ctor), "` format"))
263                    })*
264                }
265            }
266        }
267
268        impl core::error::Error for Error {
269            #[inline]
270            fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
271                match &self.kind {
272                    $($(#[$meta])* ErrorKind::$variant(error) => Some(error),)*
273                    _ => None,
274                }
275            }
276        }
277    };
278}
279
280error_kinds! {
281    // NB: Always available, since it is what the envelope is encoded with.
282    Packed, packed, musli::packed::Error;
283    #[cfg(feature = "format-storage")]
284    Storage, storage, musli::storage::Error;
285    #[cfg(feature = "format-wire")]
286    Wire, wire, musli::wire::Error;
287    #[cfg(feature = "format-descriptive")]
288    Descriptive, descriptive, musli::descriptive::Error;
289    #[cfg(feature = "format-json")]
290    Json, json, musli::json::Error;
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    use alloc::vec::Vec;
298
299    use musli::{Decode, Encode};
300
301    use crate::api::Format;
302
303    #[derive(Debug, PartialEq, Encode, Decode)]
304    struct Message<'de> {
305        message: &'de str,
306        tick: u32,
307    }
308
309    /// Every supported format must round-trip a value.
310    #[test]
311    fn round_trip() {
312        for format in Format::supported() {
313            let mut buf = Vec::new();
314            let expected = Message {
315                message: "hello",
316                tick: 42,
317            };
318
319            format.encode(&mut buf, &expected).unwrap();
320
321            let mut at = 0;
322            let actual: Message<'_> = format.decode(&buf, &mut at).unwrap();
323
324            assert_eq!(actual, expected, "round trip failed for `{format}`");
325            assert_eq!(at, buf.len(), "`{format}` did not consume the whole body");
326        }
327    }
328
329    /// Several payloads must be decodable in sequence out of one buffer, which
330    /// is what `RawPacket::decode` relies on.
331    #[test]
332    fn sequential_payloads() {
333        for format in Format::supported() {
334            let mut buf = Vec::new();
335
336            let first = Message {
337                message: "first",
338                tick: 1,
339            };
340
341            let second = Message {
342                message: "second",
343                tick: 2,
344            };
345
346            format.encode(&mut buf, &first).unwrap();
347            let boundary = buf.len();
348            format.encode(&mut buf, &second).unwrap();
349
350            let mut at = 0;
351            let a: Message<'_> = format.decode(&buf, &mut at).unwrap();
352            assert_eq!(a, first, "first payload failed for `{format}`");
353            assert_eq!(at, boundary, "`{format}` misreported the first boundary");
354
355            let b: Message<'_> = format.decode(&buf, &mut at).unwrap();
356            assert_eq!(b, second, "second payload failed for `{format}`");
357            assert_eq!(at, buf.len(), "`{format}` did not consume both payloads");
358        }
359    }
360
361    /// A body must follow the fixed envelope in the same frame, for every
362    /// format, since that is the shape of every message on the wire.
363    #[test]
364    fn envelope_then_body() {
365        use crate::api::{ChannelId, RequestHeader};
366
367        for format in Format::supported() {
368            let header = RequestHeader {
369                serial: 7,
370                id: 11,
371                format: format.to_u8(),
372                channel: ChannelId::from_u16(3),
373            };
374
375            let mut buf = Vec::new();
376            encode_envelope(&mut buf, &header).unwrap();
377
378            let expected = Message {
379                message: "body",
380                tick: 9,
381            };
382
383            format.encode(&mut buf, &expected).unwrap();
384
385            let mut at = 0;
386            let decoded: RequestHeader = decode_envelope(&buf, &mut at).unwrap();
387
388            assert_eq!(decoded.serial, 7);
389            assert_eq!(decoded.id, 11);
390            assert_eq!(decoded.format, format.to_u8());
391
392            let body: Message<'_> = format.decode(&buf, &mut at).unwrap();
393            assert_eq!(body, expected, "body failed for `{format}`");
394            assert_eq!(at, buf.len());
395        }
396    }
397
398    /// JSON must be keyed by field name, which is the point of offering it.
399    #[test]
400    #[cfg(feature = "format-json")]
401    fn json_is_human_readable() {
402        let mut buf = Vec::new();
403
404        Format::Json
405            .encode(
406                &mut buf,
407                &Message {
408                    message: "hello",
409                    tick: 42,
410                },
411            )
412            .unwrap();
413
414        assert_eq!(
415            core::str::from_utf8(&buf).unwrap(),
416            r#"{"message":"hello","tick":42}"#
417        );
418    }
419
420    /// A format the crate was not built with must fail cleanly rather than
421    /// misbehaving.
422    #[test]
423    fn unsupported_is_reported() {
424        for format in Format::ALL.iter().copied() {
425            if format.is_supported() {
426                continue;
427            }
428
429            let mut buf = Vec::new();
430            let error = format.encode(&mut buf, &1u32).unwrap_err();
431            assert_eq!(error.unsupported_format(), Some(format));
432        }
433    }
434}