atagen

headroom

public
Star0
Watch0
da174e47c3dfe446a07b3dd77fdfcd414277a44e
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git

Blame: crates/headroom-ipc/src/codec.rs

dd6b8e786e1fatagen2026-05-27 15:49 UTC1
//! length-prefixed json framing: 4-byte big-endian length, then that many bytes of utf-8 json.
dd6b8e786e1fatagen2026-05-27 15:49 UTC2
dd6b8e786e1fatagen2026-05-27 15:49 UTC3
use std::io::{Read, Write};
dd6b8e786e1fatagen2026-05-27 15:49 UTC4
dd6b8e786e1fatagen2026-05-27 15:49 UTC5
use serde::{de::DeserializeOwned, Serialize};
dd6b8e786e1fatagen2026-05-27 15:49 UTC6
dd6b8e786e1fatagen2026-05-27 15:49 UTC7
use crate::error::Error;
dd6b8e786e1fatagen2026-05-27 15:49 UTC8
dd6b8e786e1fatagen2026-05-27 15:49 UTC9
/// default frame payload cap.
dd6b8e786e1fatagen2026-05-27 15:49 UTC10
pub const DEFAULT_MAX_FRAME_BYTES: usize = 1024 * 1024; // 1 MiB
dd6b8e786e1fatagen2026-05-27 15:49 UTC11
dd6b8e786e1fatagen2026-05-27 15:49 UTC12
/// floor on the frame cap; a smaller cap is misuse, not normal traffic.
dd6b8e786e1fatagen2026-05-27 15:49 UTC13
pub const MIN_MAX_FRAME_BYTES: usize = 256;
dd6b8e786e1fatagen2026-05-27 15:49 UTC14
dd6b8e786e1fatagen2026-05-27 15:49 UTC15
/// stateless framing codec; owns no buffers, callers supply reader/writer.
dd6b8e786e1fatagen2026-05-27 15:49 UTC16
#[derive(Debug, Clone, Copy)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC17
pub struct Codec {
dd6b8e786e1fatagen2026-05-27 15:49 UTC18
    max_frame_bytes: usize,
dd6b8e786e1fatagen2026-05-27 15:49 UTC19
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC20
dd6b8e786e1fatagen2026-05-27 15:49 UTC21
impl Default for Codec {
dd6b8e786e1fatagen2026-05-27 15:49 UTC22
    fn default() -> Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC23
        Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC24
            max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
dd6b8e786e1fatagen2026-05-27 15:49 UTC25
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC26
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC27
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC28
dd6b8e786e1fatagen2026-05-27 15:49 UTC29
impl Codec {
dd6b8e786e1fatagen2026-05-27 15:49 UTC30
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC31
    pub fn new() -> Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC32
        Self::default()
dd6b8e786e1fatagen2026-05-27 15:49 UTC33
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC34
dd6b8e786e1fatagen2026-05-27 15:49 UTC35
    /// cap clamped up to [`MIN_MAX_FRAME_BYTES`].
dd6b8e786e1fatagen2026-05-27 15:49 UTC36
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC37
    pub fn with_max_frame_size(bytes: usize) -> Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC38
        Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC39
            max_frame_bytes: bytes.max(MIN_MAX_FRAME_BYTES),
dd6b8e786e1fatagen2026-05-27 15:49 UTC40
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC41
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC42
dd6b8e786e1fatagen2026-05-27 15:49 UTC43
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC44
    pub fn max_frame_bytes(self) -> usize {
dd6b8e786e1fatagen2026-05-27 15:49 UTC45
        self.max_frame_bytes
dd6b8e786e1fatagen2026-05-27 15:49 UTC46
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC47
dd6b8e786e1fatagen2026-05-27 15:49 UTC48
    /// # Errors
dd6b8e786e1fatagen2026-05-27 15:49 UTC49
    /// - [`Error::Json`] on serialize failure.
dd6b8e786e1fatagen2026-05-27 15:49 UTC50
    /// - [`Error::FrameTooLarge`] if the serialized form exceeds the cap.
dd6b8e786e1fatagen2026-05-27 15:49 UTC51
    /// - [`Error::Io`] on write failure.
dd6b8e786e1fatagen2026-05-27 15:49 UTC52
    pub fn write<W: Write, T: Serialize>(self, mut w: W, msg: &T) -> Result<(), Error> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC53
        // serialize first to know the exact length
dd6b8e786e1fatagen2026-05-27 15:49 UTC54
        let buf = serde_json::to_vec(msg)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC55
        if buf.len() > self.max_frame_bytes {
dd6b8e786e1fatagen2026-05-27 15:49 UTC56
            return Err(Error::FrameTooLarge {
dd6b8e786e1fatagen2026-05-27 15:49 UTC57
                actual: buf.len(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC58
                limit: self.max_frame_bytes,
dd6b8e786e1fatagen2026-05-27 15:49 UTC59
            });
dd6b8e786e1fatagen2026-05-27 15:49 UTC60
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC61
        let len = u32::try_from(buf.len()).expect("buf.len() <= max_frame_bytes <= u32::MAX");
dd6b8e786e1fatagen2026-05-27 15:49 UTC62
        w.write_all(&len.to_be_bytes())?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC63
        w.write_all(&buf)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC64
        w.flush()?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC65
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC66
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC67
dd6b8e786e1fatagen2026-05-27 15:49 UTC68
    /// # Errors
dd6b8e786e1fatagen2026-05-27 15:49 UTC69
    /// - [`Error::Closed`] on EOF before any length-prefix byte (graceful close).
dd6b8e786e1fatagen2026-05-27 15:49 UTC70
    /// - [`Error::Io`] on partial reads or other i/o failure.
dd6b8e786e1fatagen2026-05-27 15:49 UTC71
    /// - [`Error::FrameTooLarge`] if the announced length exceeds the cap.
dd6b8e786e1fatagen2026-05-27 15:49 UTC72
    /// - [`Error::Json`] on deserialize failure.
dd6b8e786e1fatagen2026-05-27 15:49 UTC73
    pub fn read<R: Read, T: DeserializeOwned>(self, mut r: R) -> Result<T, Error> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC74
        let mut len_buf = [0u8; 4];
dd6b8e786e1fatagen2026-05-27 15:49 UTC75
        match read_full_or_eof(&mut r, &mut len_buf)? {
dd6b8e786e1fatagen2026-05-27 15:49 UTC76
            ReadOutcome::Full => {}
dd6b8e786e1fatagen2026-05-27 15:49 UTC77
            ReadOutcome::ZeroAtStart => return Err(Error::Closed),
dd6b8e786e1fatagen2026-05-27 15:49 UTC78
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC79
        let len = u32::from_be_bytes(len_buf) as usize;
dd6b8e786e1fatagen2026-05-27 15:49 UTC80
        if len > self.max_frame_bytes {
dd6b8e786e1fatagen2026-05-27 15:49 UTC81
            return Err(Error::FrameTooLarge {
dd6b8e786e1fatagen2026-05-27 15:49 UTC82
                actual: len,
dd6b8e786e1fatagen2026-05-27 15:49 UTC83
                limit: self.max_frame_bytes,
dd6b8e786e1fatagen2026-05-27 15:49 UTC84
            });
dd6b8e786e1fatagen2026-05-27 15:49 UTC85
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC86
        let mut buf = vec![0u8; len];
dd6b8e786e1fatagen2026-05-27 15:49 UTC87
        r.read_exact(&mut buf)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC88
        Ok(serde_json::from_slice(&buf)?)
dd6b8e786e1fatagen2026-05-27 15:49 UTC89
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC90
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC91
dd6b8e786e1fatagen2026-05-27 15:49 UTC92
enum ReadOutcome {
dd6b8e786e1fatagen2026-05-27 15:49 UTC93
    Full,
dd6b8e786e1fatagen2026-05-27 15:49 UTC94
    ZeroAtStart,
dd6b8e786e1fatagen2026-05-27 15:49 UTC95
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC96
dd6b8e786e1fatagen2026-05-27 15:49 UTC97
fn read_full_or_eof<R: Read>(r: &mut R, buf: &mut [u8]) -> Result<ReadOutcome, std::io::Error> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC98
    let mut read = 0;
dd6b8e786e1fatagen2026-05-27 15:49 UTC99
    while read < buf.len() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC100
        match r.read(&mut buf[read..]) {
dd6b8e786e1fatagen2026-05-27 15:49 UTC101
            Ok(0) => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC102
                if read == 0 {
dd6b8e786e1fatagen2026-05-27 15:49 UTC103
                    return Ok(ReadOutcome::ZeroAtStart);
dd6b8e786e1fatagen2026-05-27 15:49 UTC104
                }
dd6b8e786e1fatagen2026-05-27 15:49 UTC105
                return Err(std::io::Error::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC106
                    std::io::ErrorKind::UnexpectedEof,
dd6b8e786e1fatagen2026-05-27 15:49 UTC107
                    "eof mid-length-prefix",
dd6b8e786e1fatagen2026-05-27 15:49 UTC108
                ));
dd6b8e786e1fatagen2026-05-27 15:49 UTC109
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC110
            Ok(n) => read += n,
dd6b8e786e1fatagen2026-05-27 15:49 UTC111
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
dd6b8e786e1fatagen2026-05-27 15:49 UTC112
            Err(e) => return Err(e),
dd6b8e786e1fatagen2026-05-27 15:49 UTC113
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC114
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC115
    Ok(ReadOutcome::Full)
dd6b8e786e1fatagen2026-05-27 15:49 UTC116
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC117
dd6b8e786e1fatagen2026-05-27 15:49 UTC118
#[cfg(test)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC119
mod tests {
dd6b8e786e1fatagen2026-05-27 15:49 UTC120
    use super::*;
dd6b8e786e1fatagen2026-05-27 15:49 UTC121
    use crate::{Event, Op, Request, Response, ServerFrame, Topic};
dd6b8e786e1fatagen2026-05-27 15:49 UTC122
    use std::io::Cursor;
dd6b8e786e1fatagen2026-05-27 15:49 UTC123
dd6b8e786e1fatagen2026-05-27 15:49 UTC124
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC125
    fn write_read_request_roundtrip() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC126
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC127
        let req = Request::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC128
            42,
dd6b8e786e1fatagen2026-05-27 15:49 UTC129
            Op::ProfileUse {
dd6b8e786e1fatagen2026-05-27 15:49 UTC130
                name: "night".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC131
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC132
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC133
dd6b8e786e1fatagen2026-05-27 15:49 UTC134
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC135
        codec.write(&mut buf, &req).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC136
dd6b8e786e1fatagen2026-05-27 15:49 UTC137
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC138
        let back: Request = codec.read(&mut cur).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC139
        assert_eq!(back, req);
dd6b8e786e1fatagen2026-05-27 15:49 UTC140
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC141
dd6b8e786e1fatagen2026-05-27 15:49 UTC142
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC143
    fn server_frame_round_trip_response() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC144
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC145
        let resp = Response::ok(1, &serde_json::json!({"name": "default"})).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC146
        let frame = ServerFrame::Response(resp.clone());
dd6b8e786e1fatagen2026-05-27 15:49 UTC147
dd6b8e786e1fatagen2026-05-27 15:49 UTC148
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC149
        codec.write(&mut buf, &frame).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC150
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC151
        let back: ServerFrame = codec.read(&mut cur).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC152
        match back {
dd6b8e786e1fatagen2026-05-27 15:49 UTC153
            ServerFrame::Response(r) => assert_eq!(r, resp),
dd6b8e786e1fatagen2026-05-27 15:49 UTC154
            ServerFrame::Event(_) => panic!("decoded as event"),
dd6b8e786e1fatagen2026-05-27 15:49 UTC155
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC156
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC157
dd6b8e786e1fatagen2026-05-27 15:49 UTC158
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC159
    fn server_frame_round_trip_event() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC160
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC161
        let ev = Event::new(Topic::Daemon, "shutdown", &serde_json::json!({})).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC162
        let frame = ServerFrame::Event(ev.clone());
dd6b8e786e1fatagen2026-05-27 15:49 UTC163
dd6b8e786e1fatagen2026-05-27 15:49 UTC164
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC165
        codec.write(&mut buf, &frame).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC166
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC167
        let back: ServerFrame = codec.read(&mut cur).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC168
        match back {
dd6b8e786e1fatagen2026-05-27 15:49 UTC169
            ServerFrame::Event(e) => assert_eq!(e, ev),
dd6b8e786e1fatagen2026-05-27 15:49 UTC170
            ServerFrame::Response(_) => panic!("decoded as response"),
dd6b8e786e1fatagen2026-05-27 15:49 UTC171
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC172
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC173
dd6b8e786e1fatagen2026-05-27 15:49 UTC174
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC175
    fn rejects_oversized_frames_on_write() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC176
        let codec = Codec::with_max_frame_size(MIN_MAX_FRAME_BYTES);
dd6b8e786e1fatagen2026-05-27 15:49 UTC177
        // A big string that will serialize > 256 bytes.
dd6b8e786e1fatagen2026-05-27 15:49 UTC178
        let req = Request::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC179
            1,
dd6b8e786e1fatagen2026-05-27 15:49 UTC180
            Op::SettingSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC181
                key: "x".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC182
                value: serde_json::Value::String("a".repeat(1024)),
dd6b8e786e1fatagen2026-05-27 15:49 UTC183
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC184
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC185
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC186
        let err = codec.write(&mut buf, &req).unwrap_err();
dd6b8e786e1fatagen2026-05-27 15:49 UTC187
        assert!(matches!(err, Error::FrameTooLarge { .. }));
dd6b8e786e1fatagen2026-05-27 15:49 UTC188
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC189
dd6b8e786e1fatagen2026-05-27 15:49 UTC190
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC191
    fn rejects_oversized_frames_on_read() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC192
        let codec = Codec::with_max_frame_size(MIN_MAX_FRAME_BYTES);
dd6b8e786e1fatagen2026-05-27 15:49 UTC193
        // Hand-craft a length prefix that exceeds the cap.
dd6b8e786e1fatagen2026-05-27 15:49 UTC194
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC195
        let bad_len: u32 = MIN_MAX_FRAME_BYTES as u32 + 1;
dd6b8e786e1fatagen2026-05-27 15:49 UTC196
        buf.extend_from_slice(&bad_len.to_be_bytes());
dd6b8e786e1fatagen2026-05-27 15:49 UTC197
        // No need to follow with payload; we expect early rejection.
dd6b8e786e1fatagen2026-05-27 15:49 UTC198
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC199
        let err = codec.read::<_, serde_json::Value>(&mut cur).unwrap_err();
dd6b8e786e1fatagen2026-05-27 15:49 UTC200
        assert!(matches!(err, Error::FrameTooLarge { .. }));
dd6b8e786e1fatagen2026-05-27 15:49 UTC201
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC202
dd6b8e786e1fatagen2026-05-27 15:49 UTC203
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC204
    fn graceful_eof_at_frame_boundary() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC205
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC206
        let mut cur = Cursor::new(Vec::<u8>::new());
dd6b8e786e1fatagen2026-05-27 15:49 UTC207
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
dd6b8e786e1fatagen2026-05-27 15:49 UTC208
        assert!(matches!(err, Error::Closed));
dd6b8e786e1fatagen2026-05-27 15:49 UTC209
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC210
dd6b8e786e1fatagen2026-05-27 15:49 UTC211
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC212
    fn mid_frame_eof_is_io_error() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC213
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC214
        // Half a length prefix.
dd6b8e786e1fatagen2026-05-27 15:49 UTC215
        let mut cur = Cursor::new(vec![0u8, 0u8]);
dd6b8e786e1fatagen2026-05-27 15:49 UTC216
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
dd6b8e786e1fatagen2026-05-27 15:49 UTC217
        assert!(matches!(err, Error::Io(_)));
dd6b8e786e1fatagen2026-05-27 15:49 UTC218
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC219
dd6b8e786e1fatagen2026-05-27 15:49 UTC220
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC221
    fn rejects_invalid_json_payload() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC222
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC223
        let payload = b"not-json";
dd6b8e786e1fatagen2026-05-27 15:49 UTC224
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC225
        buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
dd6b8e786e1fatagen2026-05-27 15:49 UTC226
        buf.extend_from_slice(payload);
dd6b8e786e1fatagen2026-05-27 15:49 UTC227
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC228
        let err = codec.read::<_, Request>(&mut cur).unwrap_err();
dd6b8e786e1fatagen2026-05-27 15:49 UTC229
        assert!(matches!(err, Error::Json(_)));
dd6b8e786e1fatagen2026-05-27 15:49 UTC230
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC231
dd6b8e786e1fatagen2026-05-27 15:49 UTC232
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC233
    fn back_to_back_frames() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC234
        let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC235
        let a = Request::new(1, Op::Status);
dd6b8e786e1fatagen2026-05-27 15:49 UTC236
        let b = Request::new(2, Op::ProfileList);
dd6b8e786e1fatagen2026-05-27 15:49 UTC237
dd6b8e786e1fatagen2026-05-27 15:49 UTC238
        let mut buf = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC239
        codec.write(&mut buf, &a).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC240
        codec.write(&mut buf, &b).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC241
dd6b8e786e1fatagen2026-05-27 15:49 UTC242
        let mut cur = Cursor::new(&buf);
dd6b8e786e1fatagen2026-05-27 15:49 UTC243
        let ra: Request = codec.read(&mut cur).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC244
        let rb: Request = codec.read(&mut cur).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC245
        assert_eq!(ra, a);
dd6b8e786e1fatagen2026-05-27 15:49 UTC246
        assert_eq!(rb, b);
dd6b8e786e1fatagen2026-05-27 15:49 UTC247
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC248
}