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#[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
85impl 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
451pub fn encode_client_request(request: &ClientRequest) -> rootcause::Result<ProtocolFrame> {
456 ProtocolFrame::try_from(request)
457}
458
459pub 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
472pub fn encode_server_event(event: &ServerEvent) -> rootcause::Result<ProtocolFrame> {
477 ProtocolFrame::try_from(event)
478}
479
480pub 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 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}