Skip to main content

shep_core/protocol/
wire.rs

1//! Frame encoding: u32 length prefix + JSON payload
2//!
3//! One codec constructor + encode/decode helpers shared by daemon and
4//! client so framing parameters can never drift between the two.
5
6use core::fmt;
7
8use bytes::Bytes;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11use tokio_util::codec::LengthDelimitedCodec;
12
13/// Hard ceiling per frame; larger is a protocol violation
14pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
15
16/// Builds the shared length-delimited codec (u32 BE prefix, 16 MiB cap)
17#[must_use]
18pub fn codec() -> LengthDelimitedCodec {
19    LengthDelimitedCodec::builder()
20        .length_field_type::<u32>()
21        .max_frame_length(MAX_FRAME_BYTES)
22        .new_codec()
23}
24
25/// Serializes one value to a frame payload
26///
27/// # Errors
28///
29/// - [`WireError::Json`]: serialization failed (carries serde's message).
30/// - [`WireError::FrameTooLarge`]: payload exceeds [`MAX_FRAME_BYTES`].
31pub fn encode_frame<T: Serialize>(value: &T) -> Result<Bytes, WireError> {
32    let vec = serde_json::to_vec(value).map_err(|e| WireError::Json(e.to_string()))?;
33    if vec.len() > MAX_FRAME_BYTES {
34        return Err(WireError::FrameTooLarge(vec.len()));
35    }
36    Ok(Bytes::from(vec)) // zero-copy: Bytes takes the Vec's buffer
37}
38
39/// Deserializes one frame payload
40///
41/// # Errors
42///
43/// - [`WireError::Json`]: the payload is not valid JSON for `T`.
44pub fn decode_frame<T: DeserializeOwned>(frame: &[u8]) -> Result<T, WireError> {
45    serde_json::from_slice(frame).map_err(|e| WireError::Json(e.to_string()))
46}
47
48/// Enough of a reply to recover its id when the reply's own type does not
49/// decode.
50///
51/// `result` is required but its value is never looked at: its only job is to
52/// keep this struct from matching a frame that merely happens to carry an
53/// `id`, such as a future progress or flow-control frame. Without it,
54/// `reply_id` would misidentify that frame as an undecodable reply and fail
55/// a caller whose real reply is still in flight.
56#[derive(serde::Deserialize)]
57struct ReplyIdOnly {
58    id: u64,
59    #[allow(dead_code, reason = "present only to narrow the match; never read")]
60    result: serde::de::IgnoredAny,
61}
62
63/// If `frame` is a reply, the id it answers; `None` for anything else,
64/// including an event and a frame that is not JSON at all.
65///
66/// A reply whose `Response` this build cannot decode still has a caller
67/// waiting on it. The id is the only thing needed to fail that caller by
68/// name instead of leaving it to wait out its deadline for an answer that
69/// already arrived.
70#[must_use]
71pub fn reply_id(frame: &[u8]) -> Option<u64> {
72    decode_frame::<ReplyIdOnly>(frame).ok().map(|r| r.id)
73}
74
75/// Error type returned from [`encode_frame`] and [`decode_frame`]
76///
77/// `#[non_exhaustive]`: this type is on the peer-facing surface, and the
78/// protocol will grow past a JSON payload and a size cap eventually.
79#[non_exhaustive]
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum WireError {
82    /// JSON (de)serialization failed (carries the serde message)
83    Json(String),
84    /// Encoded payload exceeds [`MAX_FRAME_BYTES`] (carries actual size)
85    FrameTooLarge(usize),
86}
87
88impl fmt::Display for WireError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Json(m) => write!(f, "wire frame JSON error: {m}"),
92            Self::FrameTooLarge(n) => {
93                write!(
94                    f,
95                    "frame of {n} bytes exceeds the {MAX_FRAME_BYTES}-byte limit"
96                )
97            }
98        }
99    }
100}
101
102impl core::error::Error for WireError {}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::protocol::{Envelope, Request};
108
109    #[test]
110    fn encode_decode_round_trip() {
111        let env = Envelope {
112            id: 9,
113            deadline_ms: Some(5000),
114            body: Request::Ping,
115        };
116        let bytes = encode_frame(&env).unwrap();
117        let back: Envelope = decode_frame(&bytes).unwrap();
118        assert_eq!(back, env);
119    }
120
121    #[test]
122    fn decode_rejects_garbage_with_json_error() {
123        assert!(matches!(
124            decode_frame::<Envelope>(b"not json"),
125            Err(WireError::Json(_))
126        ));
127    }
128
129    #[test]
130    fn reply_id_reads_the_id_off_a_real_reply() {
131        assert_eq!(
132            reply_id(br#"{"id":7,"result":{"Ok":{"kind":"Pong"}}}"#),
133            Some(7)
134        );
135    }
136
137    #[test]
138    fn reply_id_is_none_for_an_event() {
139        assert_eq!(reply_id(br#"{"event":"from_the_future","data":{}}"#), None);
140    }
141
142    #[test]
143    fn reply_id_is_none_for_non_json() {
144        assert_eq!(reply_id(b"not json"), None);
145    }
146
147    #[test]
148    fn reply_id_is_none_for_a_future_progress_frame() {
149        // A frame that carries an `id` but is not shaped like a reply (no
150        // `result`) must not be mistaken for an undecodable reply: the
151        // caller it would falsely fail is still waiting on the real one.
152        assert_eq!(
153            reply_id(br#"{"kind":"progress","id":7,"percent":40}"#),
154            None
155        );
156    }
157
158    #[test]
159    fn codec_uses_u32_prefix_and_max_frame() {
160        let c = codec();
161        assert_eq!(c.max_frame_length(), MAX_FRAME_BYTES);
162    }
163
164    #[tokio::test]
165    async fn framed_stream_round_trip() {
166        use futures_util::{SinkExt, StreamExt};
167        use tokio_util::codec::{FramedRead, FramedWrite};
168
169        let (client, server) = tokio::io::duplex(64 * 1024);
170        let mut writer = FramedWrite::new(client, codec());
171        let mut reader = FramedRead::new(server, codec());
172
173        let env = Envelope {
174            id: 1,
175            deadline_ms: None,
176            body: Request::ListFlock,
177        };
178        writer.send(encode_frame(&env).unwrap()).await.unwrap();
179
180        let frame = reader.next().await.unwrap().unwrap();
181        let back: Envelope = decode_frame(&frame).unwrap();
182        assert_eq!(back, env);
183    }
184}