Skip to main content

muxr_core/protocol/
wire.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3
4use bytes::Bytes;
5use bytes::BytesMut;
6use compact_str::CompactString;
7use rkyv::util::AlignedVec;
8use rootcause::report;
9use serde::Serialize;
10
11use super::AttachAccepted;
12use super::AttachRequest;
13use super::ClientKey;
14use super::ClientMouseEvent;
15use super::ClientMousePosition;
16use super::LayoutSnapshot;
17use super::PaneRegionsSnapshot;
18use super::PaneScrollDirection;
19use super::PaneScrollLineMove;
20use super::RenderBaseline;
21use super::RenderCell;
22use super::RenderCellWidth;
23use super::RenderCursor;
24use super::RenderDiff;
25use super::RenderHyperlink;
26use super::RenderRowSpan;
27use super::RenderStyle;
28use super::RenderUpdate;
29use super::TabId;
30use super::TerminalSize;
31use super::pane_render::RenderHyperlinkPresence;
32use crate::SessionName;
33
34#[repr(u8)]
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36enum ProtocolFrameKind {
37    Domain = 0,
38    HyperlinkTableRender = 1,
39}
40
41impl TryFrom<u8> for ProtocolFrameKind {
42    type Error = rootcause::Report;
43
44    fn try_from(value: u8) -> Result<Self, Self::Error> {
45        match value {
46            0 => Ok(Self::Domain),
47            1 => Ok(Self::HyperlinkTableRender),
48            _ => Err(report!("invalid muxr protocol frame kind").attach(format!("kind={value}"))),
49        }
50    }
51}
52
53/// Owned muxr protocol frame bytes.
54///
55/// This intentionally wraps [`Bytes`] instead of exposing it directly from the encoder API: muxr keeps a domain-owned
56/// protocol type at the core boundary, while transports can still take the `Bytes` buffer without copying the payload.
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ProtocolFrame(Bytes);
59
60impl ProtocolFrame {
61    #[must_use]
62    pub fn as_bytes(&self) -> &[u8] {
63        &self.0
64    }
65
66    #[must_use]
67    pub fn into_bytes(self) -> Bytes {
68        self.0
69    }
70
71    fn from_payload(kind: ProtocolFrameKind, payload: &[u8]) -> Self {
72        let mut frame = BytesMut::with_capacity(1_usize.saturating_add(payload.len()));
73        frame.extend_from_slice(&[kind as u8]);
74        frame.extend_from_slice(payload);
75        Self(frame.freeze())
76    }
77}
78
79impl AsRef<[u8]> for ProtocolFrame {
80    fn as_ref(&self) -> &[u8] {
81        self.as_bytes()
82    }
83}
84
85/// Frames raw rkyv payload bytes by prepending muxr's frame kind.
86impl From<&[u8]> for ProtocolFrame {
87    fn from(payload: &[u8]) -> Self {
88        Self::from_payload(ProtocolFrameKind::Domain, payload)
89    }
90}
91
92impl TryFrom<&ClientRequest> for ProtocolFrame {
93    type Error = rootcause::Report;
94
95    fn try_from(request: &ClientRequest) -> Result<Self, Self::Error> {
96        let payload = rkyv::to_bytes::<rkyv::rancor::Error>(request)
97            .map_err(|error| report!("failed to serialize muxr protocol frame").attach(format!("{error:?}")))?;
98        Ok(Self::from(payload.as_slice()))
99    }
100}
101
102impl TryFrom<&ServerEvent> for ProtocolFrame {
103    type Error = rootcause::Report;
104
105    fn try_from(event: &ServerEvent) -> Result<Self, Self::Error> {
106        if let ServerEvent::Render(update) = event
107            && self::render_update_hyperlink_presence(update) == RenderHyperlinkPresence::Present
108        {
109            let wire_update = HyperlinkTableRenderUpdate::from_domain(update)?;
110            let payload = rkyv::to_bytes::<rkyv::rancor::Error>(&wire_update)
111                .map_err(|error| report!("failed to serialize muxr protocol frame").attach(format!("{error:?}")))?;
112            return Ok(Self::from_payload(
113                ProtocolFrameKind::HyperlinkTableRender,
114                payload.as_slice(),
115            ));
116        }
117
118        let payload = rkyv::to_bytes::<rkyv::rancor::Error>(event)
119            .map_err(|error| report!("failed to serialize muxr protocol frame").attach(format!("{error:?}")))?;
120        Ok(Self::from(payload.as_slice()))
121    }
122}
123
124#[derive(rkyv::Archive, Clone, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
125#[serde(tag = "code", content = "msg", rename_all = "snake_case")]
126pub enum ServerError {
127    ClientAlreadyAttached,
128    SessionMismatch { expected: SessionName, actual: SessionName },
129    UnexpectedRequest { request: Box<ClientRequest> },
130}
131
132impl ServerError {
133    #[must_use]
134    pub fn unexpected_request(request: ClientRequest) -> Self {
135        Self::UnexpectedRequest {
136            request: Box::new(request),
137        }
138    }
139
140    #[must_use]
141    pub const fn code(&self) -> &'static str {
142        match self {
143            Self::ClientAlreadyAttached => "client_already_attached",
144            Self::SessionMismatch { .. } => "session_mismatch",
145            Self::UnexpectedRequest { .. } => "unexpected_request",
146        }
147    }
148
149    #[must_use]
150    pub fn msg(&self) -> String {
151        match self {
152            Self::ClientAlreadyAttached => "a muxr client is already attached to this session".to_owned(),
153            Self::SessionMismatch { expected, actual } => format!("expected session {expected}, got {actual}"),
154            Self::UnexpectedRequest { request } => format!("unexpected client request during attach: {request:?}"),
155        }
156    }
157}
158
159#[derive(rkyv::Archive, Clone, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
160pub enum ClientRequest {
161    Attach(AttachRequest),
162    DeleteSession,
163    Ping,
164    Pong,
165    Detach,
166    RenderResync,
167    Resize(TerminalSize),
168    Input(Vec<u8>),
169    Paste(Vec<u8>),
170    Key(ClientKey),
171    Mouse(ClientMouseEvent),
172    OpenFile {
173        pane_id: super::PaneId,
174        path: String,
175        line: Option<u32>,
176        column: Option<u32>,
177    },
178    ScrollPaneLineAt {
179        position: ClientMousePosition,
180        direction: PaneScrollDirection,
181    },
182    FocusPaneAt(ClientMousePosition),
183    FocusTab(TabId),
184}
185
186#[derive(rkyv::Archive, Clone, Debug, rkyv::Deserialize, Eq, PartialEq, Serialize, rkyv::Serialize)]
187pub enum ServerEvent {
188    Attached(AttachAccepted),
189    Deleted,
190    Ping,
191    Pong,
192    Layout(LayoutSnapshot),
193    SidebarLayout(LayoutSnapshot),
194    PaneRegions(PaneRegionsSnapshot),
195    Render(RenderUpdate),
196    ScrollPaneLineResult {
197        position: ClientMousePosition,
198        direction: PaneScrollDirection,
199        movement: PaneScrollLineMove,
200    },
201    Error(ServerError),
202    Detached,
203}
204
205#[derive(rkyv::Archive, Debug, rkyv::Deserialize, rkyv::Serialize)]
206struct HyperlinkTableRenderUpdate {
207    frame: HyperlinkTableRenderFrame,
208    hyperlinks: Vec<String>,
209}
210
211impl HyperlinkTableRenderUpdate {
212    fn from_domain(update: &RenderUpdate) -> rootcause::Result<Self> {
213        let mut table = HyperlinkTableBuilder::default();
214        let frame = HyperlinkTableRenderFrame::from_domain(update, &mut table)?;
215        Ok(Self {
216            frame,
217            hyperlinks: table.into_uris(),
218        })
219    }
220
221    fn into_domain(self) -> rootcause::Result<RenderUpdate> {
222        let mut hyperlinks = ResolvedHyperlinks::new(self.hyperlinks)?;
223        let update = self.frame.into_domain(&mut hyperlinks)?;
224        hyperlinks.validate_all_used()?;
225        Ok(update)
226    }
227}
228
229#[derive(rkyv::Archive, Debug, rkyv::Deserialize, rkyv::Serialize)]
230enum HyperlinkTableRenderFrame {
231    Baseline {
232        cursor: RenderCursor,
233        rows: Vec<HyperlinkTableRenderRowSpan>,
234        seq: u64,
235        size: TerminalSize,
236    },
237    Diff {
238        base_seq: u64,
239        cursor: RenderCursor,
240        rows: Vec<HyperlinkTableRenderRowSpan>,
241        seq: u64,
242        size: TerminalSize,
243    },
244}
245
246impl HyperlinkTableRenderFrame {
247    fn from_domain(update: &RenderUpdate, table: &mut HyperlinkTableBuilder) -> rootcause::Result<Self> {
248        Ok(match update {
249            RenderUpdate::Baseline(baseline) => Self::Baseline {
250                cursor: baseline.cursor().clone(),
251                rows: HyperlinkTableRenderRowSpan::from_domain_rows(baseline.rows(), table)?,
252                seq: baseline.seq(),
253                size: baseline.size().clone(),
254            },
255            RenderUpdate::Diff(diff) => Self::Diff {
256                base_seq: diff.base_seq(),
257                cursor: diff.cursor().clone(),
258                rows: HyperlinkTableRenderRowSpan::from_domain_rows(diff.rows(), table)?,
259                seq: diff.seq(),
260                size: diff.size().clone(),
261            },
262        })
263    }
264
265    fn into_domain(self, hyperlinks: &mut ResolvedHyperlinks) -> rootcause::Result<RenderUpdate> {
266        match self {
267            Self::Baseline {
268                cursor,
269                rows,
270                seq,
271                size,
272            } => Ok(RenderUpdate::Baseline(RenderBaseline::new(
273                seq,
274                size,
275                cursor,
276                HyperlinkTableRenderRowSpan::into_domain_rows(rows, hyperlinks)?,
277            )?)),
278            Self::Diff {
279                base_seq,
280                cursor,
281                rows,
282                seq,
283                size,
284            } => Ok(RenderUpdate::Diff(RenderDiff::new(
285                base_seq,
286                seq,
287                size,
288                cursor,
289                HyperlinkTableRenderRowSpan::into_domain_rows(rows, hyperlinks)?,
290            )?)),
291        }
292    }
293}
294
295#[derive(rkyv::Archive, Debug, rkyv::Deserialize, rkyv::Serialize)]
296struct HyperlinkTableRenderRowSpan {
297    cells: Vec<HyperlinkTableRenderCell>,
298    col: u16,
299    row: u16,
300}
301
302impl HyperlinkTableRenderRowSpan {
303    fn from_domain_rows(rows: &[RenderRowSpan], table: &mut HyperlinkTableBuilder) -> rootcause::Result<Vec<Self>> {
304        rows.iter().map(|row| Self::from_domain(row, table)).collect()
305    }
306
307    fn from_domain(row: &RenderRowSpan, table: &mut HyperlinkTableBuilder) -> rootcause::Result<Self> {
308        Ok(Self {
309            cells: row
310                .cells()
311                .iter()
312                .map(|cell| HyperlinkTableRenderCell::from_domain(cell, table))
313                .collect::<rootcause::Result<Vec<_>>>()?,
314            col: row.col(),
315            row: row.row(),
316        })
317    }
318
319    fn into_domain_rows(rows: Vec<Self>, hyperlinks: &mut ResolvedHyperlinks) -> rootcause::Result<Vec<RenderRowSpan>> {
320        rows.into_iter().map(|row| row.into_domain(hyperlinks)).collect()
321    }
322
323    fn into_domain(self, hyperlinks: &mut ResolvedHyperlinks) -> rootcause::Result<RenderRowSpan> {
324        RenderRowSpan::new(
325            self.row,
326            self.col,
327            self.cells
328                .into_iter()
329                .map(|cell| cell.into_domain(hyperlinks))
330                .collect::<rootcause::Result<Vec<_>>>()?,
331        )
332    }
333}
334
335#[derive(rkyv::Archive, Debug, rkyv::Deserialize, rkyv::Serialize)]
336struct HyperlinkTableRenderCell {
337    hyperlink_id: Option<u32>,
338    style: RenderStyle,
339    text: CompactString,
340    width: RenderCellWidth,
341}
342
343impl HyperlinkTableRenderCell {
344    fn from_domain(cell: &RenderCell, table: &mut HyperlinkTableBuilder) -> rootcause::Result<Self> {
345        Ok(Self {
346            hyperlink_id: cell.hyperlink().map(|hyperlink| table.id_for(hyperlink)).transpose()?,
347            style: cell.style(),
348            text: CompactString::new(cell.text()),
349            width: cell.width(),
350        })
351    }
352
353    fn into_domain(self, hyperlinks: &mut ResolvedHyperlinks) -> rootcause::Result<RenderCell> {
354        let mut cell = match self.width {
355            RenderCellWidth::Narrow => RenderCell::narrow(&self.text, self.style),
356            RenderCellWidth::Wide => RenderCell::wide(&self.text, self.style),
357            RenderCellWidth::WideContinuation if self.text.is_empty() => RenderCell::wide_continuation(self.style),
358            RenderCellWidth::WideContinuation => {
359                return Err(report!("invalid muxr hyperlink-table render cell")
360                    .attach("reason=wide continuation must not carry text"));
361            }
362        };
363        if let Some(id) = self.hyperlink_id {
364            cell = cell.with_hyperlink(hyperlinks.resolve(id)?);
365        }
366        Ok(cell)
367    }
368}
369
370#[derive(Default)]
371struct HyperlinkTableBuilder {
372    by_uri: HashMap<String, u32>,
373    uris: Vec<String>,
374}
375
376impl HyperlinkTableBuilder {
377    fn id_for(&mut self, hyperlink: &RenderHyperlink) -> rootcause::Result<u32> {
378        if let Some(id) = self.by_uri.get(hyperlink.uri()) {
379            return Ok(*id);
380        }
381        let id = u32::try_from(self.uris.len())?
382            .checked_add(1)
383            .ok_or_else(|| report!("muxr hyperlink table id overflowed"))?;
384        let uri = hyperlink.uri().to_owned();
385        self.by_uri.insert(uri.clone(), id);
386        self.uris.push(uri);
387        Ok(id)
388    }
389
390    fn into_uris(self) -> Vec<String> {
391        self.uris
392    }
393}
394
395struct ResolvedHyperlinks {
396    hyperlinks: Vec<RenderHyperlink>,
397    references: Vec<HyperlinkReference>,
398}
399
400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
401enum HyperlinkReference {
402    Unreferenced,
403    Referenced,
404}
405
406impl ResolvedHyperlinks {
407    fn new(uris: Vec<String>) -> rootcause::Result<Self> {
408        let mut seen = HashSet::with_capacity(uris.len());
409        let mut hyperlinks = Vec::with_capacity(uris.len());
410        for uri in uris {
411            if !seen.insert(uri.clone()) {
412                return Err(report!("invalid muxr hyperlink table").attach("reason=duplicate uri"));
413            }
414            hyperlinks.push(RenderHyperlink::new(uri)?);
415        }
416        let references = vec![HyperlinkReference::Unreferenced; hyperlinks.len()];
417        Ok(Self { hyperlinks, references })
418    }
419
420    fn resolve(&mut self, id: u32) -> rootcause::Result<RenderHyperlink> {
421        let Some(index) = id.checked_sub(1).and_then(|index| usize::try_from(index).ok()) else {
422            return Err(report!("invalid muxr hyperlink table id")
423                .attach("reason=id must be nonzero")
424                .attach(format!("id={id}")));
425        };
426        let Some(hyperlink) = self.hyperlinks.get(index) else {
427            return Err(report!("invalid muxr hyperlink table id")
428                .attach("reason=id is outside hyperlink table")
429                .attach(format!("id={id}"))
430                .attach(format!("table_len={}", self.hyperlinks.len())));
431        };
432        let Some(reference) = self.references.get_mut(index) else {
433            return Err(report!("invalid muxr hyperlink table bookkeeping"));
434        };
435        *reference = HyperlinkReference::Referenced;
436        Ok(hyperlink.clone())
437    }
438
439    fn validate_all_used(&self) -> rootcause::Result<()> {
440        if self
441            .references
442            .iter()
443            .all(|reference| *reference == HyperlinkReference::Referenced)
444        {
445            return Ok(());
446        }
447        Err(report!("invalid muxr hyperlink table").attach("reason=unused uri"))
448    }
449}
450
451/// Encode a client request as a muxr protocol frame containing a rkyv payload.
452///
453/// # Errors
454/// - The request cannot be serialized.
455pub fn encode_client_request(request: &ClientRequest) -> rootcause::Result<ProtocolFrame> {
456    ProtocolFrame::try_from(request)
457}
458
459/// Decode a client request from one muxr protocol frame containing a rkyv payload.
460///
461/// # Errors
462/// - The frame is empty or not a valid client request frame.
463/// - The decoded request cannot be deserialized into valid domain values.
464pub fn decode_client_request(line: &[u8]) -> rootcause::Result<ClientRequest> {
465    let payload = self::decode_domain_payload(line)?;
466    let archived = rkyv::access::<rkyv::Archived<ClientRequest>, rkyv::rancor::Error>(&payload)
467        .map_err(|error| report!("failed to validate muxr protocol frame").attach(format!("{error:?}")))?;
468    rkyv::deserialize::<ClientRequest, rkyv::rancor::Error>(archived)
469        .map_err(|error| report!("failed to deserialize muxr protocol frame").attach(format!("{error:?}")))
470}
471
472/// Encode a server event as a muxr protocol frame containing a rkyv payload.
473///
474/// # Errors
475/// - The event cannot be serialized.
476pub fn encode_server_event(event: &ServerEvent) -> rootcause::Result<ProtocolFrame> {
477    ProtocolFrame::try_from(event)
478}
479
480/// Decode a server event from one muxr protocol frame containing a rkyv payload.
481///
482/// # Errors
483/// - The frame is empty or not a valid server event frame.
484/// - The decoded event cannot be deserialized into valid domain values.
485pub fn decode_server_event(line: &[u8]) -> rootcause::Result<ServerEvent> {
486    let (kind, payload) = self::protocol_payload(line)?;
487    let payload = self::align_protocol_payload(payload);
488    match kind {
489        ProtocolFrameKind::Domain => {
490            let archived = rkyv::access::<rkyv::Archived<ServerEvent>, rkyv::rancor::Error>(&payload)
491                .map_err(|error| report!("failed to validate muxr protocol frame").attach(format!("{error:?}")))?;
492            rkyv::deserialize::<ServerEvent, rkyv::rancor::Error>(archived)
493                .map_err(|error| report!("failed to deserialize muxr protocol frame").attach(format!("{error:?}")))
494        }
495        ProtocolFrameKind::HyperlinkTableRender => {
496            let archived = rkyv::access::<rkyv::Archived<HyperlinkTableRenderUpdate>, rkyv::rancor::Error>(&payload)
497                .map_err(|error| report!("failed to validate muxr protocol frame").attach(format!("{error:?}")))?;
498            let update = rkyv::deserialize::<HyperlinkTableRenderUpdate, rkyv::rancor::Error>(archived)
499                .map_err(|error| report!("failed to deserialize muxr protocol frame").attach(format!("{error:?}")))?
500                .into_domain()?;
501            Ok(ServerEvent::Render(update))
502        }
503    }
504}
505
506fn decode_domain_payload(frame: &[u8]) -> rootcause::Result<AlignedVec> {
507    let (kind, payload) = self::protocol_payload(frame)?;
508    if kind != ProtocolFrameKind::Domain {
509        return Err(report!("invalid muxr protocol frame kind")
510            .attach("expected=domain")
511            .attach(format!("actual={kind:?}")));
512    }
513    Ok(self::align_protocol_payload(payload))
514}
515
516fn protocol_payload(frame: &[u8]) -> rootcause::Result<(ProtocolFrameKind, &[u8])> {
517    let Some((&kind, payload)) = frame.split_first() else {
518        return Err(report!("empty muxr protocol frame"));
519    };
520    let kind = ProtocolFrameKind::try_from(kind)?;
521    if payload.is_empty() {
522        return Err(report!("empty muxr protocol payload"));
523    }
524    Ok((kind, payload))
525}
526
527fn align_protocol_payload(payload: &[u8]) -> AlignedVec {
528    // Socket buffers have arbitrary byte alignment; rkyv checked access requires aligned archived bytes.
529    let mut aligned = AlignedVec::with_capacity(payload.len());
530    aligned.extend_from_slice(payload);
531    aligned
532}
533
534const fn render_update_hyperlink_presence(update: &RenderUpdate) -> RenderHyperlinkPresence {
535    match update {
536        RenderUpdate::Baseline(baseline) => baseline.hyperlink_presence(),
537        RenderUpdate::Diff(diff) => diff.hyperlink_presence(),
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use rstest::rstest;
544    use test_that::prelude::*;
545
546    use super::super::keyboard_input::ClientKeyCode;
547    use super::super::keyboard_input::ClientKeyModifiers;
548    use super::super::pane_layout::PaneId;
549    use super::super::pane_layout::PaneMouseMode;
550    use super::super::pane_layout::PaneRegionSnapshot;
551    use super::super::pane_layout::PaneRegionsSnapshot;
552    use super::super::pane_layout::PaneSnapshot;
553    use super::super::pane_layout::TabSnapshot;
554    use super::super::pane_layout::test_helpers as pane_layout_test_helpers;
555    use super::super::pane_mouse::ClientMouseEventPhase;
556    use super::super::pane_render::RenderBaseline;
557    use super::super::pane_render::RenderCell;
558    use super::super::pane_render::RenderCursor;
559    use super::super::pane_render::RenderCursorShape;
560    use super::super::pane_render::RenderCursorVisibility;
561    use super::super::pane_render::RenderDiff;
562    use super::super::pane_render::RenderRowSpan;
563    use super::super::pane_render::RenderStyle;
564    use super::super::pane_render::test_helpers as pane_render_test_helpers;
565    use super::super::session_attach::AttachRequest;
566    use super::super::terminal::TerminalSize;
567    use super::super::tracked_process::TrackedProcessState;
568    use super::*;
569
570    #[test]
571    fn test_protocol_frame_from_when_payload_is_raw_prepends_domain_kind() {
572        let frame = ProtocolFrame::from(b"payload".as_slice());
573        let expected = b"\0payload";
574
575        assert_that!(frame.as_bytes(), eq(expected));
576        assert_that!(AsRef::<[u8]>::as_ref(&frame), eq(expected));
577        assert_that!(frame.into_bytes().as_ref(), eq(expected));
578    }
579
580    #[rstest]
581    #[case::attach(ClientRequest::Attach(client_attach_request()?))]
582    #[case::delete_session(ClientRequest::DeleteSession)]
583    #[case::ping(ClientRequest::Ping)]
584    #[case::pong(ClientRequest::Pong)]
585    #[case::detach(ClientRequest::Detach)]
586    #[case::render_resync(ClientRequest::RenderResync)]
587    #[case::resize(ClientRequest::Resize(terminal_size(120, 40)?))]
588    #[case::input(ClientRequest::Input(vec![b'a', b'b', b'\n']))]
589    #[case::paste(ClientRequest::Paste(vec![b'a', b'\n', b'b', b'\n']))]
590    #[case::key(ClientRequest::Key(client_key()))]
591    #[case::modified_enter_key(ClientRequest::Key(modified_enter_key()))]
592    #[case::mouse(ClientRequest::Mouse(ClientMouseEvent {
593            button: 0,
594            phase: ClientMouseEventPhase::Press,
595            position: ClientMousePosition { row: 2, col: 3 },
596        }))]
597    #[case::open_file(ClientRequest::OpenFile {
598            pane_id: PaneId::new(7)?,
599            path: "/tmp/missing file.rs".to_owned(),
600            line: Some(42),
601            column: Some(7),
602        })]
603    #[case::scroll_line(ClientRequest::ScrollPaneLineAt {
604            position: ClientMousePosition { row: 2, col: 3 },
605            direction: PaneScrollDirection::Down,
606    })]
607    #[case::focus_pane_at(ClientRequest::FocusPaneAt(ClientMousePosition { row: 2, col: 3 }))]
608    #[case::focus_tab(ClientRequest::FocusTab(TabId::new(2)?))]
609    fn test_client_request_codec_when_frame_round_trips_returns_original(
610        #[case] request: ClientRequest,
611    ) -> rootcause::Result<()> {
612        assert_that!(
613            decode_client_request(encode_client_request(&request)?.as_bytes())?,
614            eq(request)
615        );
616        Ok(())
617    }
618
619    #[rstest]
620    #[case::attached(ServerEvent::Attached(attach_accepted()?))]
621    #[case::deleted(ServerEvent::Deleted)]
622    #[case::ping(ServerEvent::Ping)]
623    #[case::pong(ServerEvent::Pong)]
624    #[case::layout(ServerEvent::Layout(layout_snapshot()?))]
625    #[case::sidebar_layout(ServerEvent::SidebarLayout(layout_snapshot()?))]
626    #[case::pane_regions(ServerEvent::PaneRegions(pane_regions_snapshot()?))]
627    #[case::render_baseline(ServerEvent::Render(RenderUpdate::Baseline(render_baseline()?)))]
628    #[case::render_linked_baseline(ServerEvent::Render(RenderUpdate::Baseline(linked_render_baseline()?)))]
629    #[case::render_diff(ServerEvent::Render(RenderUpdate::Diff(render_diff()?)))]
630    #[case::scroll_line_result(ServerEvent::ScrollPaneLineResult {
631            position: ClientMousePosition { row: 2, col: 3 },
632            direction: PaneScrollDirection::Down,
633            movement: PaneScrollLineMove::Unchanged,
634        })]
635    #[case::error(ServerEvent::Error(ServerError::unexpected_request(ClientRequest::Detach)))]
636    #[case::detached(ServerEvent::Detached)]
637    fn test_server_event_codec_when_frame_round_trips_returns_original(
638        #[case] event: ServerEvent,
639    ) -> rootcause::Result<()> {
640        assert_that!(decode_server_event(encode_server_event(&event)?.as_bytes())?, eq(event));
641        Ok(())
642    }
643
644    #[test]
645    fn test_server_event_codec_when_render_update_is_invalid_returns_error() -> rootcause::Result<()> {
646        let event = self::invalid_render_event()?;
647        let encoded = encode_server_event(&event)?;
648
649        assert_that!(decode_server_event(encoded.as_bytes()), err(anything()));
650        Ok(())
651    }
652
653    #[test]
654    fn test_server_event_codec_when_attached_layout_is_invalid_returns_error() -> rootcause::Result<()> {
655        let event = ServerEvent::Attached(AttachAccepted {
656            layout: pane_layout_test_helpers::raw_layout_snapshot(
657                TabId::new(99)?,
658                vec![self::tab_snapshot(
659                    1,
660                    "default",
661                    1,
662                    vec![self::pane_snapshot(1, "shell")?],
663                )?],
664            ),
665            pane_regions: self::pane_regions_snapshot()?,
666        });
667        let encoded = encode_server_event(&event)?;
668
669        assert_that!(decode_server_event(encoded.as_bytes()), err(anything()));
670        Ok(())
671    }
672
673    #[test]
674    fn test_server_event_codec_when_layout_event_is_invalid_returns_error() -> rootcause::Result<()> {
675        let event = ServerEvent::Layout(pane_layout_test_helpers::raw_layout_snapshot(
676            TabId::new(99)?,
677            vec![self::tab_snapshot(
678                1,
679                "default",
680                1,
681                vec![self::pane_snapshot(1, "shell")?],
682            )?],
683        ));
684        let encoded = encode_server_event(&event)?;
685
686        assert_that!(decode_server_event(encoded.as_bytes()), err(anything()));
687        Ok(())
688    }
689
690    #[test]
691    fn test_client_request_codec_when_frame_kind_is_invalid_returns_error() {
692        let encoded = b"not-muxr-rkyv";
693
694        assert_that!(decode_client_request(encoded), err(anything()));
695    }
696
697    #[test]
698    fn test_protocol_codec_when_frame_kind_is_unknown_returns_error() -> rootcause::Result<()> {
699        let encoded = [u8::MAX, b'x'];
700
701        let Err(error) = decode_server_event(&encoded) else {
702            return Err(report!("expected unknown frame kind rejection"));
703        };
704
705        assert_that!(
706            format!("{error:?}"),
707            contains_substring("invalid muxr protocol frame kind")
708        );
709        Ok(())
710    }
711
712    #[test]
713    fn test_hyperlink_table_render_when_links_repeat_encodes_deterministically_and_shares_decoded_uri()
714    -> rootcause::Result<()> {
715        let event = ServerEvent::Render(RenderUpdate::Baseline(linked_render_baseline()?));
716        let first = encode_server_event(&event)?;
717        let second = encode_server_event(&event)?;
718
719        assert_that!(second.as_bytes(), eq(first.as_bytes()));
720        let ServerEvent::Render(RenderUpdate::Baseline(decoded)) = decode_server_event(first.as_bytes())? else {
721            return Err(report!("expected decoded render baseline"));
722        };
723        let row = decoded
724            .rows()
725            .first()
726            .ok_or_else(|| report!("expected decoded render row"))?;
727        let first_link = row
728            .cells()
729            .first()
730            .and_then(RenderCell::hyperlink)
731            .ok_or_else(|| report!("expected first decoded hyperlink"))?;
732        let second_link = row
733            .cells()
734            .get(1)
735            .and_then(RenderCell::hyperlink)
736            .ok_or_else(|| report!("expected second decoded hyperlink"))?;
737        assert_that!(first_link.uri(), eq("https://example.com"));
738        assert_that!(first_link.shares_uri_with(second_link), eq(true));
739        Ok(())
740    }
741
742    #[rstest]
743    #[case::zero_id(vec!["https://example.com"], vec![0, 0])]
744    #[case::out_of_range_id(vec!["https://example.com"], vec![2, 2])]
745    #[case::duplicate_uri(vec!["https://example.com", "https://example.com"], vec![1, 2])]
746    #[case::unused_uri(vec!["https://example.com", "https://unused.example.com"], vec![1, 1])]
747    fn test_hyperlink_table_render_when_table_is_noncanonical_returns_error(
748        #[case] hyperlinks: Vec<&str>,
749        #[case] hyperlink_ids: Vec<u32>,
750    ) -> rootcause::Result<()> {
751        let wire_update = self::raw_hyperlink_table_render_update(hyperlinks, hyperlink_ids)?;
752        let encoded = self::encode_hyperlink_table_render_update(&wire_update)?;
753
754        assert_that!(decode_server_event(encoded.as_bytes()), err(anything()));
755        Ok(())
756    }
757
758    #[test]
759    fn test_hyperlink_table_render_when_url_frame_is_encoded_is_smaller_than_direct_domain_encoding()
760    -> rootcause::Result<()> {
761        let uri = "https://example.com/muxr/performance/reference";
762        let cells = (0..320)
763            .map(|_| self::render_cell("x").with_hyperlink_uri(uri))
764            .collect::<rootcause::Result<Vec<_>>>()?;
765        let event = ServerEvent::Render(RenderUpdate::Baseline(RenderBaseline::new(
766            1,
767            terminal_size(320, 1)?,
768            RenderCursor {
769                row: 0,
770                col: 0,
771                shape: RenderCursorShape::Default,
772                visibility: RenderCursorVisibility::Visible,
773            },
774            vec![RenderRowSpan::new(0, 0, cells)?],
775        )?));
776        let direct_payload = rkyv::to_bytes::<rkyv::rancor::Error>(&event)
777            .map_err(|error| report!("failed to serialize direct comparison frame").attach(format!("{error:?}")))?;
778
779        let table_frame = encode_server_event(&event)?;
780        let direct_size = 1_usize.saturating_add(direct_payload.len());
781
782        assert_that!(table_frame.as_bytes().len(), lt(direct_size));
783        Ok(())
784    }
785
786    fn client_attach_request() -> rootcause::Result<AttachRequest> {
787        Ok(AttachRequest {
788            session: "work".parse()?,
789            terminal_size: self::terminal_size(80, 24)?,
790        })
791    }
792
793    fn attach_accepted() -> rootcause::Result<AttachAccepted> {
794        Ok(AttachAccepted {
795            layout: self::layout_snapshot()?,
796            pane_regions: self::pane_regions_snapshot()?,
797        })
798    }
799
800    fn client_key() -> ClientKey {
801        ClientKey {
802            code: ClientKeyCode::Char('E'),
803            modifiers: ClientKeyModifiers::SHIFT_ALT,
804            raw_bytes: vec![b'\x1b', b'E'],
805        }
806    }
807
808    fn modified_enter_key() -> ClientKey {
809        ClientKey {
810            code: ClientKeyCode::Enter,
811            modifiers: ClientKeyModifiers::SHIFT,
812            raw_bytes: b"\x1b[13;2u".to_vec(),
813        }
814    }
815
816    fn layout_snapshot() -> rootcause::Result<LayoutSnapshot> {
817        let active_tab = TabId::new(1)?;
818        let active_pane = PaneId::new(1)?;
819        let pane = PaneSnapshot {
820            tracked_process_state: TrackedProcessState::None,
821            cwd: "/tmp".to_owned(),
822            cmd_label: None,
823            focus_seq: 1,
824            id: active_pane,
825            title: "shell".to_owned(),
826        };
827        let tab = TabSnapshot::new(active_tab, "default", active_pane, vec![pane])?;
828        LayoutSnapshot::new(active_tab, vec![tab])
829    }
830
831    fn pane_regions_snapshot() -> rootcause::Result<PaneRegionsSnapshot> {
832        PaneRegionsSnapshot::new(vec![PaneRegionSnapshot::new(
833            PaneId::new(1)?,
834            0,
835            0,
836            80,
837            24,
838            PaneMouseMode::None,
839            0,
840        )?])
841    }
842
843    fn tab_snapshot(
844        id: u32,
845        title: &str,
846        active_pane: u32,
847        panes: Vec<PaneSnapshot>,
848    ) -> rootcause::Result<TabSnapshot> {
849        TabSnapshot::new(TabId::new(id)?, title, PaneId::new(active_pane)?, panes)
850    }
851
852    fn pane_snapshot(id: u32, title: &str) -> rootcause::Result<PaneSnapshot> {
853        Ok(PaneSnapshot {
854            tracked_process_state: TrackedProcessState::None,
855            cwd: "/tmp".to_owned(),
856            cmd_label: None,
857            focus_seq: 1,
858            id: PaneId::new(id)?,
859            title: title.to_owned(),
860        })
861    }
862
863    fn terminal_size(cols: u16, rows: u16) -> rootcause::Result<TerminalSize> {
864        TerminalSize::new(cols, rows)
865    }
866
867    fn render_baseline() -> rootcause::Result<RenderBaseline> {
868        RenderBaseline::new(
869            1,
870            self::terminal_size(4, 2)?,
871            RenderCursor {
872                row: 1,
873                col: 2,
874                shape: RenderCursorShape::Default,
875                visibility: RenderCursorVisibility::Visible,
876            },
877            vec![
878                RenderRowSpan::new(
879                    0,
880                    0,
881                    vec![
882                        self::render_cell("a"),
883                        self::render_cell("b"),
884                        self::render_cell("c"),
885                        self::render_cell("d"),
886                    ],
887                )?,
888                RenderRowSpan::new(
889                    1,
890                    0,
891                    vec![
892                        self::render_cell("e"),
893                        self::render_cell("f"),
894                        self::render_cell("g"),
895                        self::render_cell("h"),
896                    ],
897                )?,
898            ],
899        )
900    }
901
902    fn linked_render_baseline() -> rootcause::Result<RenderBaseline> {
903        RenderBaseline::new(
904            1,
905            self::terminal_size(4, 2)?,
906            RenderCursor {
907                row: 1,
908                col: 2,
909                shape: RenderCursorShape::Default,
910                visibility: RenderCursorVisibility::Visible,
911            },
912            vec![
913                RenderRowSpan::new(
914                    0,
915                    0,
916                    vec![
917                        self::render_cell("a").with_hyperlink_uri("https://example.com")?,
918                        self::render_cell("b").with_hyperlink_uri("https://example.com")?,
919                        self::render_cell("c"),
920                        self::render_cell("d"),
921                    ],
922                )?,
923                RenderRowSpan::new(
924                    1,
925                    0,
926                    vec![
927                        self::render_cell("e"),
928                        self::render_cell("f"),
929                        self::render_cell("g"),
930                        self::render_cell("h"),
931                    ],
932                )?,
933            ],
934        )
935    }
936
937    fn render_diff() -> rootcause::Result<RenderDiff> {
938        RenderDiff::new(
939            1,
940            2,
941            self::terminal_size(4, 2)?,
942            RenderCursor {
943                row: 1,
944                col: 3,
945                shape: RenderCursorShape::Default,
946                visibility: RenderCursorVisibility::Visible,
947            },
948            vec![RenderRowSpan::new(
949                1,
950                1,
951                vec![self::render_cell("x"), self::render_cell("y")],
952            )?],
953        )
954    }
955
956    fn invalid_render_event() -> rootcause::Result<ServerEvent> {
957        Ok(ServerEvent::Render(RenderUpdate::Diff(
958            pane_render_test_helpers::raw_render_diff(
959                1,
960                2,
961                self::terminal_size(4, 2)?,
962                RenderCursor {
963                    row: 0,
964                    col: 0,
965                    shape: RenderCursorShape::Default,
966                    visibility: RenderCursorVisibility::Visible,
967                },
968                vec![pane_render_test_helpers::raw_render_row_span(
969                    0,
970                    0,
971                    vec![RenderCell::wide_continuation(RenderStyle::default())],
972                )],
973            ),
974        )))
975    }
976
977    fn render_cell(text: &str) -> RenderCell {
978        RenderCell::narrow(text, RenderStyle::default())
979    }
980
981    fn raw_hyperlink_table_render_update(
982        hyperlinks: Vec<&str>,
983        hyperlink_ids: Vec<u32>,
984    ) -> rootcause::Result<HyperlinkTableRenderUpdate> {
985        let cells = hyperlink_ids
986            .into_iter()
987            .map(|hyperlink_id| HyperlinkTableRenderCell {
988                hyperlink_id: Some(hyperlink_id),
989                style: RenderStyle::default(),
990                text: CompactString::new("x"),
991                width: RenderCellWidth::Narrow,
992            })
993            .collect::<Vec<_>>();
994        Ok(HyperlinkTableRenderUpdate {
995            frame: HyperlinkTableRenderFrame::Baseline {
996                cursor: RenderCursor {
997                    row: 0,
998                    col: 0,
999                    shape: RenderCursorShape::Default,
1000                    visibility: RenderCursorVisibility::Visible,
1001                },
1002                rows: vec![HyperlinkTableRenderRowSpan { cells, col: 0, row: 0 }],
1003                seq: 1,
1004                size: terminal_size(2, 1)?,
1005            },
1006            hyperlinks: hyperlinks.into_iter().map(str::to_owned).collect(),
1007        })
1008    }
1009
1010    fn encode_hyperlink_table_render_update(update: &HyperlinkTableRenderUpdate) -> rootcause::Result<ProtocolFrame> {
1011        let payload = rkyv::to_bytes::<rkyv::rancor::Error>(update)
1012            .map_err(|error| report!("failed to serialize raw hyperlink-table frame").attach(format!("{error:?}")))?;
1013        Ok(ProtocolFrame::from_payload(
1014            ProtocolFrameKind::HyperlinkTableRender,
1015            payload.as_slice(),
1016        ))
1017    }
1018}