1use std::collections::BTreeMap;
15use std::fmt;
16use std::str;
17
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20use thiserror::Error;
21pub const FLAGS_SET_METHOD: &str = Method::FlagsSet.as_str();
23
24pub const SHORTCUT_EXECUTE_METHOD: &str = Method::ShortcutExecute.as_str();
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum SizeValue {
34 Cells(u16),
36 Percent(u8),
38}
39
40impl Serialize for SizeValue {
41 fn serialize<S: serde::Serializer>(
42 &self,
43 serializer: S,
44 ) -> std::result::Result<S::Ok, S::Error> {
45 match *self {
46 Self::Cells(n) => serializer.serialize_u16(n),
47 Self::Percent(n) => serializer.serialize_str(&format!("{n}%")),
48 }
49 }
50}
51
52impl<'de> Deserialize<'de> for SizeValue {
53 fn deserialize<D: serde::Deserializer<'de>>(
54 deserializer: D,
55 ) -> std::result::Result<Self, D::Error> {
56 struct Visitor;
57 impl serde::de::Visitor<'_> for Visitor {
58 type Value = SizeValue;
59 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str("a cell count number or a percent string like \"50%\"")
61 }
62 fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<Self::Value, E> {
63 u16::try_from(v)
64 .map(SizeValue::Cells)
65 .map_err(|_| E::custom("size value exceeds u16"))
66 }
67 fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<Self::Value, E> {
68 if v < 0 {
69 return Err(E::custom("size value must be non-negative"));
70 }
71 self.visit_u64(v.cast_unsigned())
72 }
73 fn visit_str<E: serde::de::Error>(
74 self,
75 v: &str,
76 ) -> std::result::Result<Self::Value, E> {
77 let stripped = v
78 .strip_suffix('%')
79 .ok_or_else(|| E::custom(format!("invalid percent size: {v}")))?;
80 if stripped.is_empty() || !stripped.bytes().all(|b| b.is_ascii_digit()) {
81 return Err(E::custom(format!("invalid percent size: {v}")));
82 }
83 let n: u32 = stripped.parse().map_err(E::custom)?;
84 Ok(SizeValue::Percent(
85 u8::try_from(n.min(100)).map_err(E::custom)?,
86 ))
87 }
88 }
89 deserializer.deserialize_any(Visitor)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
95#[serde(rename_all = "kebab-case")]
96pub enum OverlayAnchor {
97 #[default]
99 Center,
100 TopLeft,
102 TopRight,
104 BottomLeft,
106 BottomRight,
108 TopCenter,
110 BottomCenter,
112 LeftCenter,
114 RightCenter,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase", default)]
121pub struct OverlayMargin {
122 pub top: u16,
124 pub right: u16,
126 pub bottom: u16,
128 pub left: u16,
130}
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(untagged)]
134pub enum OverlayMarginWire {
135 Uniform(u16),
137 Sides(OverlayMargin),
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase", default)]
144pub struct OverlaySpec {
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub width: Option<SizeValue>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub min_width: Option<u16>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub max_height: Option<SizeValue>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub anchor: Option<OverlayAnchor>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub offset_x: Option<i16>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub offset_y: Option<i16>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub row: Option<SizeValue>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub col: Option<SizeValue>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub margin: Option<OverlayMarginWire>,
172 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
174 pub non_capturing: bool,
175}
176
177pub const PROTOCOL_VERSION: u32 = 1;
179
180pub const COMPATIBILITY_VERSION: &str = "0.80.10";
182
183pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
185
186pub type FrameId = u64;
191
192pub type Result<T, E = ProtocolError> = std::result::Result<T, E>;
194#[derive(Debug, Error, Clone, PartialEq, Eq)]
196pub enum ProtocolError {
197 #[error("frame exceeds maximum size of {MAX_FRAME_BYTES} bytes")]
199 FrameTooLarge,
200 #[error("invalid UTF-8 in protocol stream: {0}")]
202 InvalidUtf8(String),
203 #[error("invalid JSON frame: {0}")]
205 InvalidJson(String),
206 #[error("malformed frame: {0}")]
208 MalformedFrame(String),
209 #[error("invalid frame: {0}")]
211 InvalidFrame(String),
212 #[error("protocol version mismatch: remote={remote} local={local}")]
214 VersionMismatch {
215 remote: u32,
217 local: u32,
219 },
220 #[error("compatibility version mismatch: remote={remote} local={local}")]
222 CompatibilityMismatch {
223 remote: String,
225 local: String,
227 },
228 #[error("unknown protocol method: {0}")]
230 UnknownMethod(String),
231 #[error("truncated protocol frame at end of stream")]
233 Truncated,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum FrameKind {
240 Req,
242 Res,
244 #[default]
246 Event,
247 Error,
249}
250
251impl FrameKind {
252 #[must_use]
254 pub const fn as_str(self) -> &'static str {
255 match self {
256 Self::Req => "req",
257 Self::Res => "res",
258 Self::Event => "event",
259 Self::Error => "error",
260 }
261 }
262
263 #[must_use]
265 pub fn parse(raw: &str) -> Option<Self> {
266 match raw {
267 "req" => Some(Self::Req),
268 "res" => Some(Self::Res),
269 "event" => Some(Self::Event),
270 "error" => Some(Self::Error),
271 _ => None,
272 }
273 }
274
275 #[must_use]
277 pub const fn requires_nonzero_id(self) -> bool {
278 matches!(self, Self::Req | Self::Res)
279 }
280}
281
282impl fmt::Display for FrameKind {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 f.write_str(self.as_str())
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub enum Method {
297 Hello,
299 ToolUpdate,
301 ProviderEvent,
303 UiSlot,
305 DisposeSlot,
307 ExtensionError,
309 Select,
311 Confirm,
313 Input,
315 Editor,
317 Notify,
319 TerminalInput,
321 #[serde(rename = "flags.set")]
323 FlagsSet,
324 #[serde(rename = "shortcut.execute")]
326 ShortcutExecute,
327 UiEvent,
329 Measure,
331 Render,
333}
334
335impl Method {
336 pub const ALL: &'static [Self] = &[
338 Self::Hello,
339 Self::ToolUpdate,
340 Self::ProviderEvent,
341 Self::UiSlot,
342 Self::DisposeSlot,
343 Self::ExtensionError,
344 Self::Select,
345 Self::Confirm,
346 Self::Input,
347 Self::Editor,
348 Self::Notify,
349 Self::TerminalInput,
350 Self::FlagsSet,
351 Self::ShortcutExecute,
352 Self::UiEvent,
353 Self::Measure,
354 Self::Render,
355 ];
356
357 #[must_use]
359 pub const fn as_str(self) -> &'static str {
360 match self {
361 Self::Hello => "hello",
362 Self::ToolUpdate => "toolUpdate",
363 Self::ProviderEvent => "providerEvent",
364 Self::UiSlot => "uiSlot",
365 Self::DisposeSlot => "disposeSlot",
366 Self::ExtensionError => "extensionError",
367 Self::Select => "select",
368 Self::Confirm => "confirm",
369 Self::Input => "input",
370 Self::Editor => "editor",
371 Self::Notify => "notify",
372 Self::TerminalInput => "terminalInput",
373 Self::FlagsSet => "flags.set",
374 Self::ShortcutExecute => "shortcut.execute",
375 Self::UiEvent => "uiEvent",
376 Self::Measure => "measure",
377 Self::Render => "render",
378 }
379 }
380
381 #[must_use]
383 pub fn parse(raw: &str) -> Option<Self> {
384 match raw {
385 "hello" => Some(Self::Hello),
386 "toolUpdate" => Some(Self::ToolUpdate),
387 "providerEvent" => Some(Self::ProviderEvent),
388 "uiSlot" => Some(Self::UiSlot),
389 "disposeSlot" => Some(Self::DisposeSlot),
390 "extensionError" => Some(Self::ExtensionError),
391 "select" => Some(Self::Select),
392 "confirm" => Some(Self::Confirm),
393 "input" => Some(Self::Input),
394 "editor" => Some(Self::Editor),
395 "notify" => Some(Self::Notify),
396 "terminalInput" => Some(Self::TerminalInput),
397 "flags.set" => Some(Self::FlagsSet),
398 "shortcut.execute" => Some(Self::ShortcutExecute),
399 "uiEvent" => Some(Self::UiEvent),
400 "measure" => Some(Self::Measure),
401 "render" => Some(Self::Render),
402 _ => None,
403 }
404 }
405
406 #[must_use]
408 pub fn is_allowlisted(raw: &str) -> bool {
409 Self::parse(raw).is_some()
410 }
411}
412
413impl fmt::Display for Method {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 f.write_str(self.as_str())
416 }
417}
418#[must_use]
420pub const fn ui_slot_method() -> &'static str {
421 "uiSlot"
422}
423
424#[must_use]
426pub const fn dispose_slot_method() -> &'static str {
427 "disposeSlot"
428}
429
430#[must_use]
432pub const fn tool_update_method() -> &'static str {
433 "toolUpdate"
434}
435
436#[must_use]
438pub const fn provider_event_method() -> &'static str {
439 "providerEvent"
440}
441
442#[must_use]
444pub const fn extension_error_method() -> &'static str {
445 "extensionError"
446}
447
448pub struct FrameValidationError;
450
451impl FrameValidationError {
452 #[must_use]
454 pub const fn message_for(_frame: &Frame) -> &'static str {
455 "invalid protocol frame"
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct Frame {
463 pub id: FrameId,
465 pub kind: FrameKind,
467 pub method: String,
469 #[serde(default)]
471 pub payload: Value,
472}
473
474impl Frame {
475 #[must_use]
477 pub fn new(id: FrameId, kind: FrameKind, method: Method, payload: Value) -> Self {
478 Self {
479 id,
480 kind,
481 method: method.as_str().to_owned(),
482 payload,
483 }
484 }
485
486 #[must_use]
488 pub fn request(id: FrameId, method: Method, payload: Value) -> Self {
489 Self::new(id, FrameKind::Req, method, payload)
490 }
491
492 #[must_use]
494 pub fn response(id: FrameId, method: Method, payload: Value) -> Self {
495 Self::new(id, FrameKind::Res, method, payload)
496 }
497
498 #[must_use]
500 pub fn event(id: FrameId, method: Method, payload: Value) -> Self {
501 Self::new(id, FrameKind::Event, method, payload)
502 }
503
504 pub fn error_frame(id: FrameId, method: Method, error: &ErrorPayload) -> Result<Self> {
511 let payload = serde_json::to_value(error)
512 .map_err(|e| ProtocolError::InvalidJson(format!("serialize error payload: {e}")))?;
513 Ok(Self::new(id, FrameKind::Error, method, payload))
514 }
515
516 #[must_use]
518 pub fn method_enum(&self) -> Option<Method> {
519 Method::parse(&self.method)
520 }
521
522 pub fn validate(&self, require_allowlisted: bool) -> Result<()> {
529 if self.kind.requires_nonzero_id() && self.id == 0 {
530 return Err(ProtocolError::InvalidFrame(format!(
531 "kind {} requires nonzero id",
532 self.kind
533 )));
534 }
535 if self.method.is_empty() {
536 return Err(ProtocolError::InvalidFrame(
537 "method must be a non-empty string".to_owned(),
538 ));
539 }
540 if require_allowlisted && !Method::is_allowlisted(&self.method) {
541 return Err(ProtocolError::UnknownMethod(self.method.clone()));
542 }
543 match &self.payload {
545 Value::Null | Value::Object(_) | Value::Array(_) => {}
546 Value::Bool(_) | Value::Number(_) | Value::String(_) => {
547 return Err(ProtocolError::InvalidFrame(
548 "payload must be a JSON object or array".to_owned(),
549 ));
550 }
551 }
552 if self.method == Method::UiSlot.as_str() {
553 let slot: UiSlot = serde_json::from_value(self.payload.clone()).map_err(|error| {
554 ProtocolError::InvalidFrame(format!("invalid uiSlot payload: {error}"))
555 })?;
556 slot.validate()?;
557 }
558 Ok(())
559 }
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
564#[serde(rename_all = "camelCase")]
565pub struct Hello {
566 pub protocol_version: u32,
568 pub compatibility_version: String,
570}
571
572impl Hello {
573 #[must_use]
575 pub fn local() -> Self {
576 Self {
577 protocol_version: PROTOCOL_VERSION,
578 compatibility_version: COMPATIBILITY_VERSION.to_owned(),
579 }
580 }
581
582 pub fn validate_remote(&self) -> Result<()> {
588 if self.protocol_version != PROTOCOL_VERSION {
589 return Err(ProtocolError::VersionMismatch {
590 remote: self.protocol_version,
591 local: PROTOCOL_VERSION,
592 });
593 }
594 if self.compatibility_version != COMPATIBILITY_VERSION {
595 return Err(ProtocolError::CompatibilityMismatch {
596 remote: self.compatibility_version.clone(),
597 local: COMPATIBILITY_VERSION.to_owned(),
598 });
599 }
600 Ok(())
601 }
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct HelloAck {
608 pub protocol_version: u32,
610 pub compatibility_version: String,
612}
613
614impl HelloAck {
615 #[must_use]
617 pub fn local() -> Self {
618 Self {
619 protocol_version: PROTOCOL_VERSION,
620 compatibility_version: COMPATIBILITY_VERSION.to_owned(),
621 }
622 }
623}
624
625#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628pub struct ErrorPayload {
629 pub code: String,
631 pub message: String,
633 pub retryable: bool,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub data: Option<Value>,
639}
640
641impl ErrorPayload {
642 #[must_use]
644 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
645 Self {
646 code: code.into(),
647 message: message.into(),
648 retryable: false,
649 data: None,
650 }
651 }
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
656#[serde(rename_all = "camelCase", default)]
657pub struct Style {
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub bold: Option<bool>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub dim: Option<bool>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub italic: Option<bool>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub underline: Option<bool>,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub reverse: Option<bool>,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub strikethrough: Option<bool>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
678 pub fg: Option<WireColor>,
679 #[serde(default, skip_serializing_if = "Option::is_none")]
681 pub bg: Option<WireColor>,
682 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub link: Option<Hyperlink>,
685}
686
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689#[serde(tag = "type", rename_all = "camelCase")]
690pub enum WireColor {
691 Named {
693 name: NamedColor,
695 },
696 Indexed {
698 index: u8,
700 },
701 Rgb {
703 r: u8,
705 g: u8,
707 b: u8,
709 },
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
714#[serde(rename_all = "camelCase")]
715pub enum NamedColor {
716 Black,
718 Red,
720 Green,
722 Yellow,
724 Blue,
726 Magenta,
728 Cyan,
730 White,
732 BrightBlack,
734 BrightRed,
736 BrightGreen,
738 BrightYellow,
740 BrightBlue,
742 BrightMagenta,
744 BrightCyan,
746 BrightWhite,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
752#[serde(rename_all = "camelCase")]
753pub struct Hyperlink {
754 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub id: Option<String>,
757 pub uri: String,
759}
760
761impl Hyperlink {
762 pub const MAX_ID_BYTES: usize = 128;
764 pub const MAX_URI_BYTES: usize = 2048;
766
767 pub fn validate(&self) -> Result<()> {
773 if let Some(id) = &self.id
774 && id.len() > Self::MAX_ID_BYTES
775 {
776 return Err(ProtocolError::InvalidFrame(format!(
777 "hyperlink id exceeds {} bytes",
778 Self::MAX_ID_BYTES
779 )));
780 }
781 if self.uri.len() > Self::MAX_URI_BYTES {
782 return Err(ProtocolError::InvalidFrame(format!(
783 "hyperlink uri exceeds {} bytes",
784 Self::MAX_URI_BYTES
785 )));
786 }
787 let ok = self.uri.starts_with("http://") || self.uri.starts_with("https://");
788 if !ok {
789 return Err(ProtocolError::InvalidFrame(
790 "hyperlink uri must use http or https".to_owned(),
791 ));
792 }
793 Ok(())
794 }
795}
796impl UiSlot {
797 pub fn validate(&self) -> Result<()> {
804 for line in &self.runs {
805 for run in line {
806 if let Some(link) = &run.style.link {
807 link.validate()?;
808 }
809 }
810 }
811 Ok(())
812 }
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(rename_all = "camelCase")]
818pub struct StyledRun {
819 pub text: String,
821 #[serde(default, skip_serializing_if = "is_default_style")]
823 pub style: Style,
824}
825
826fn is_default_style(style: &Style) -> bool {
827 style == &Style::default()
828}
829
830#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
832#[serde(rename_all = "camelCase")]
833pub enum SlotPlacement {
834 #[default]
836 Header,
837 Footer,
839 AboveEditor,
841 BelowEditor,
843 Editor,
845 MessageRenderer,
847 Overlay,
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
853#[serde(rename_all = "camelCase")]
854pub struct SlotCursor {
855 pub col: u16,
857 pub row: u16,
859}
860
861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase")]
864pub struct UiSlot {
865 pub key: String,
867 pub generation: u64,
869 pub placement: SlotPlacement,
871 pub height: u16,
873 pub runs: Vec<Vec<StyledRun>>,
875 #[serde(default)]
877 pub focusable: bool,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub cursor: Option<SlotCursor>,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub overlay_options: Option<OverlaySpec>,
884}
885
886#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(rename_all = "camelCase")]
889pub struct DisposeSlot {
890 pub key: String,
892 #[serde(default, skip_serializing_if = "Option::is_none")]
894 pub generation: Option<u64>,
895}
896
897#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct ExtensionErrorEvent {
901 pub code: String,
903 pub message: String,
905 #[serde(default)]
907 pub retryable: bool,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub data: Option<Value>,
911}
912
913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
915#[serde(rename_all = "camelCase")]
916pub struct ToolUpdate {
917 pub tool_call_id: String,
919 pub tool_name: String,
921 pub partial_result: Value,
923}
924
925#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
927#[serde(rename_all = "camelCase")]
928pub struct ProviderEvent {
929 pub provider_id: String,
931 pub call_id: String,
933 pub event: String,
935 #[serde(default)]
937 pub data: Value,
938}
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase", default)]
943pub struct KeyModifiersWire {
944 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub shift: Option<bool>,
947 #[serde(default, skip_serializing_if = "Option::is_none")]
949 pub alt: Option<bool>,
950 #[serde(default, skip_serializing_if = "Option::is_none")]
952 pub ctrl: Option<bool>,
953 #[serde(default, skip_serializing_if = "Option::is_none")]
955 pub super_key: Option<bool>,
956}
957
958#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
960#[serde(rename_all = "camelCase")]
961pub enum KeyEventKindWire {
962 #[default]
964 Press,
965 Release,
967 Repeat,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973#[serde(tag = "type", rename_all = "camelCase")]
974pub enum UiEventWire {
975 Key {
977 code: String,
979 #[serde(default)]
981 modifiers: KeyModifiersWire,
982 #[serde(default)]
984 kind: KeyEventKindWire,
985 },
986 Paste {
988 text: String,
990 },
991 FocusGained,
993 FocusLost,
995 Resize {
997 width: u16,
999 height: u16,
1001 },
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(untagged)]
1007pub enum FlagValueWire {
1008 Boolean(bool),
1010 String(String),
1012}
1013
1014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1016#[serde(rename_all = "camelCase")]
1017pub struct FlagsSetRequest {
1018 pub values: BTreeMap<String, FlagValueWire>,
1020}
1021
1022#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct FlagsSetResponse {
1026 pub ok: bool,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct ShortcutExecuteRequest {
1034 pub key: String,
1036}
1037
1038#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct ShortcutExecuteResponse {
1042 pub handled: bool,
1044}
1045
1046#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct UiEventRequest {
1050 pub key: String,
1052 pub generation: u64,
1054 pub event: UiEventWire,
1056 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub data: Option<String>,
1059}
1060
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1063#[serde(rename_all = "camelCase")]
1064pub struct UiEventResponse {
1065 pub delivered: bool,
1067}
1068
1069#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1071#[serde(rename_all = "camelCase", default)]
1072pub struct TerminalInputResult {
1073 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1075 pub consume: bool,
1076 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub data: Option<String>,
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase", default)]
1084pub struct DialogOptions {
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub timeout_ms: Option<u64>,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092#[serde(rename_all = "camelCase")]
1093pub struct SelectRequest {
1094 pub title: String,
1096 pub options: Vec<String>,
1098 #[serde(default, flatten)]
1100 pub options_meta: DialogOptions,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1105#[serde(rename_all = "camelCase")]
1106pub struct SelectResponse {
1107 #[serde(default, skip_serializing_if = "Option::is_none")]
1109 pub value: Option<String>,
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1114#[serde(rename_all = "camelCase")]
1115pub struct ConfirmRequest {
1116 pub title: String,
1118 pub message: String,
1120 #[serde(default, flatten)]
1122 pub options_meta: DialogOptions,
1123}
1124
1125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1127#[serde(rename_all = "camelCase")]
1128pub struct ConfirmResponse {
1129 pub confirmed: bool,
1131}
1132
1133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1135#[serde(rename_all = "camelCase")]
1136pub struct InputRequest {
1137 pub title: String,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub placeholder: Option<String>,
1142 #[serde(default, flatten)]
1144 pub options_meta: DialogOptions,
1145}
1146
1147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150pub struct InputResponse {
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub value: Option<String>,
1154}
1155
1156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1158#[serde(rename_all = "camelCase")]
1159pub struct EditorRequest {
1160 pub title: String,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1164 pub prefill: Option<String>,
1165}
1166
1167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1169#[serde(rename_all = "camelCase")]
1170pub struct EditorResponse {
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub value: Option<String>,
1174}
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub enum NotifyLevel {
1180 #[default]
1182 Info,
1183 Warning,
1185 Error,
1187}
1188
1189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191#[serde(rename_all = "camelCase")]
1192pub struct NotifyRequest {
1193 pub message: String,
1195 #[serde(default, rename = "type")]
1197 pub level: NotifyLevel,
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1202#[serde(rename_all = "camelCase")]
1203pub struct SlotRenderRequest {
1204 pub key: String,
1206 pub width: u16,
1208 pub theme_generation: u64,
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub struct MeasureResponse {
1216 pub height: u16,
1218}
1219
1220pub fn encode_frame(frame: &Frame) -> Result<Vec<u8>> {
1228 frame.validate(false)?;
1229 let mut bytes = serde_json::to_vec(frame)
1230 .map_err(|e| ProtocolError::InvalidJson(format!("encode frame: {e}")))?;
1231 if bytes.len() > MAX_FRAME_BYTES {
1232 return Err(ProtocolError::FrameTooLarge);
1233 }
1234 bytes.push(b'\n');
1235 Ok(bytes)
1236}
1237
1238pub fn encode_frame_string(frame: &Frame) -> Result<String> {
1244 let bytes = encode_frame(frame)?;
1245 String::from_utf8(bytes).map_err(|e| ProtocolError::InvalidUtf8(e.to_string()))
1246}
1247
1248pub fn decode_frame_line(line: &[u8]) -> Result<Frame> {
1254 if line.len() > MAX_FRAME_BYTES {
1255 return Err(ProtocolError::FrameTooLarge);
1256 }
1257 let text = str::from_utf8(line).map_err(|e| ProtocolError::InvalidUtf8(e.to_string()))?;
1258 decode_frame_str(text)
1259}
1260
1261pub fn decode_frame_str(line: &str) -> Result<Frame> {
1267 let trimmed = line.trim_end_matches('\r');
1268 if trimmed.is_empty() {
1269 return Err(ProtocolError::MalformedFrame("empty line".to_owned()));
1270 }
1271 if trimmed.len() > MAX_FRAME_BYTES {
1272 return Err(ProtocolError::FrameTooLarge);
1273 }
1274 let frame: Frame =
1275 serde_json::from_str(trimmed).map_err(|e| ProtocolError::InvalidJson(e.to_string()))?;
1276 frame.validate(false)?;
1277 Ok(frame)
1278}
1279
1280pub fn decode_frame_str_strict(line: &str) -> Result<Frame> {
1286 let frame = decode_frame_str(line)?;
1287 frame.validate(true)?;
1288 Ok(frame)
1289}
1290
1291#[derive(Debug, Default)]
1297pub struct FrameDecoder {
1298 buf: Vec<u8>,
1299 max_frame_bytes: usize,
1300}
1301
1302impl FrameDecoder {
1303 #[must_use]
1305 pub fn new() -> Self {
1306 Self {
1307 buf: Vec::new(),
1308 max_frame_bytes: MAX_FRAME_BYTES,
1309 }
1310 }
1311
1312 #[must_use]
1314 pub fn with_max_frame_bytes(max_frame_bytes: usize) -> Self {
1315 Self {
1316 buf: Vec::new(),
1317 max_frame_bytes,
1318 }
1319 }
1320
1321 #[must_use]
1323 pub fn buffered_len(&self) -> usize {
1324 self.buf.len()
1325 }
1326
1327 pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<Frame>> {
1336 let mut out = Vec::new();
1337 let mut offset = 0usize;
1338 while offset < chunk.len() {
1339 if let Some(rel) = chunk[offset..].iter().position(|&b| b == b'\n') {
1341 let line_end_in_chunk = offset + rel;
1342 let pending = self.buf.len() + (line_end_in_chunk - offset);
1343 if pending > self.max_frame_bytes {
1344 self.buf.clear();
1345 return Err(ProtocolError::FrameTooLarge);
1346 }
1347 self.buf
1348 .extend_from_slice(&chunk[offset..line_end_in_chunk]);
1349 if self.buf.last() == Some(&b'\r') {
1351 self.buf.pop();
1352 }
1353 let line = std::mem::take(&mut self.buf);
1354 out.push(decode_frame_line(&line)?);
1355 offset = line_end_in_chunk + 1;
1356 } else {
1357 let pending = self.buf.len() + (chunk.len() - offset);
1358 if pending > self.max_frame_bytes {
1359 self.buf.clear();
1360 return Err(ProtocolError::FrameTooLarge);
1361 }
1362 self.buf.extend_from_slice(&chunk[offset..]);
1364 break;
1365 }
1366 }
1367 Ok(out)
1368 }
1369
1370 pub fn finish(&mut self) -> Result<Option<Frame>> {
1378 if self.buf.is_empty() {
1379 return Ok(None);
1380 }
1381 let leftover = std::mem::take(&mut self.buf);
1383 if leftover.iter().all(u8::is_ascii_whitespace) {
1384 return Ok(None);
1385 }
1386 Err(ProtocolError::Truncated)
1387 }
1388
1389 pub fn finish_with_final_line(&mut self) -> Result<Option<Frame>> {
1395 if self.buf.is_empty() {
1396 return Ok(None);
1397 }
1398 if self.buf.len() > self.max_frame_bytes {
1399 self.buf.clear();
1400 return Err(ProtocolError::FrameTooLarge);
1401 }
1402 if self.buf.last() == Some(&b'\r') {
1403 self.buf.pop();
1404 }
1405 let line = std::mem::take(&mut self.buf);
1406 if line.is_empty() {
1407 return Ok(None);
1408 }
1409 Ok(Some(decode_frame_line(&line)?))
1410 }
1411
1412 pub fn reset(&mut self) {
1414 self.buf.clear();
1415 }
1416}
1417
1418pub fn to_payload<T: Serialize>(value: &T) -> Result<Value> {
1424 serde_json::to_value(value).map_err(|e| ProtocolError::InvalidJson(e.to_string()))
1425}
1426
1427pub fn from_payload<T: for<'de> Deserialize<'de>>(payload: &Value) -> Result<T> {
1433 serde_json::from_value(payload.clone()).map_err(|e| ProtocolError::InvalidJson(e.to_string()))
1434}
1435
1436#[must_use]
1438pub fn empty_object() -> Value {
1439 Value::Object(Map::new())
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444 use super::*;
1445
1446 const FIXTURES: &str = include_str!("../tests/fixtures/protocol/frames.jsonl");
1447
1448 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
1449
1450 fn sample_hello_req() -> Result<Frame> {
1451 Ok(Frame::request(
1452 1,
1453 Method::Hello,
1454 to_payload(&Hello::local())?,
1455 ))
1456 }
1457
1458 #[test]
1459 fn versions_are_stable() {
1460 assert_eq!(PROTOCOL_VERSION, 1);
1461 assert_eq!(COMPATIBILITY_VERSION, "0.80.10");
1462 assert_eq!(MAX_FRAME_BYTES, 8 * 1024 * 1024);
1463 }
1464
1465 #[test]
1466 fn method_allowlist_roundtrip() {
1467 for method in Method::ALL {
1468 assert_eq!(Method::parse(method.as_str()), Some(*method));
1469 }
1470 assert_eq!(FLAGS_SET_METHOD, Method::FlagsSet.as_str());
1471 assert_eq!(SHORTCUT_EXECUTE_METHOD, Method::ShortcutExecute.as_str());
1472 assert!(Method::parse("notAMethod").is_none());
1473 }
1474
1475 #[test]
1476 fn frame_id_rules() -> TestResult {
1477 let mut frame = sample_hello_req()?;
1478 frame.id = 0;
1479 assert!(matches!(
1480 frame.validate(false),
1481 Err(ProtocolError::InvalidFrame(_))
1482 ));
1483 Frame::event(0, Method::Notify, empty_object()).validate(false)?;
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn hello_version_gate() -> TestResult {
1489 Hello::local().validate_remote()?;
1490 let bad = Hello {
1491 protocol_version: 99,
1492 compatibility_version: COMPATIBILITY_VERSION.to_owned(),
1493 };
1494 assert!(matches!(
1495 bad.validate_remote(),
1496 Err(ProtocolError::VersionMismatch {
1497 remote: 99,
1498 local: 1
1499 })
1500 ));
1501 let bad_compat = Hello {
1502 protocol_version: 1,
1503 compatibility_version: "0.0.0".to_owned(),
1504 };
1505 assert!(matches!(
1506 bad_compat.validate_remote(),
1507 Err(ProtocolError::CompatibilityMismatch { .. })
1508 ));
1509 Ok(())
1510 }
1511
1512 #[test]
1513 fn encode_decode_roundtrip_typed() -> TestResult {
1514 let hello = sample_hello_req()?;
1515 let line = encode_frame_string(&hello)?;
1516 assert!(line.ends_with('\n'));
1517 let decoded = decode_frame_str(line.trim_end())?;
1518 assert_eq!(decoded, hello);
1519 assert_eq!(from_payload::<Hello>(&decoded.payload)?, Hello::local());
1520
1521 let ack = Frame::response(1, Method::Hello, to_payload(&HelloAck::local())?);
1522 let ack_line = encode_frame_string(&ack)?;
1523 let decoded_ack = decode_frame_str(ack_line.trim_end())?;
1524 assert_eq!(
1525 from_payload::<HelloAck>(&decoded_ack.payload)?,
1526 HelloAck::local()
1527 );
1528 Ok(())
1529 }
1530
1531 fn sample_slot() -> UiSlot {
1532 UiSlot {
1533 key: "widget.demo".to_owned(),
1534 generation: 3,
1535 placement: SlotPlacement::AboveEditor,
1536 height: 2,
1537 runs: vec![
1538 vec![StyledRun {
1539 text: "hi".to_owned(),
1540 style: Style {
1541 bold: Some(true),
1542 fg: Some(WireColor::Named {
1543 name: NamedColor::Green,
1544 }),
1545 ..Style::default()
1546 },
1547 }],
1548 vec![StyledRun {
1549 text: "link".to_owned(),
1550 style: Style {
1551 underline: Some(true),
1552 link: Some(Hyperlink {
1553 id: Some("a".to_owned()),
1554 uri: "https://example.com".to_owned(),
1555 }),
1556 fg: Some(WireColor::Rgb { r: 1, g: 2, b: 3 }),
1557 ..Style::default()
1558 },
1559 }],
1560 ],
1561 focusable: true,
1562 cursor: Some(SlotCursor { col: 1, row: 0 }),
1563 overlay_options: Some(OverlaySpec {
1564 width: Some(SizeValue::Percent(50)),
1565 anchor: Some(OverlayAnchor::TopCenter),
1566 margin: Some(OverlayMarginWire::Uniform(2)),
1567 non_capturing: true,
1568 ..OverlaySpec::default()
1569 }),
1570 }
1571 }
1572
1573 #[test]
1574 fn ui_slot_and_style_roundtrip() -> TestResult {
1575 let slot = sample_slot();
1576 let frame = Frame::event(0, Method::UiSlot, to_payload(&slot)?);
1577 let line = encode_frame_string(&frame)?;
1578 let decoded = decode_frame_str(line.trim_end())?;
1579 let back: UiSlot = from_payload(&decoded.payload)?;
1580 assert_eq!(back, slot);
1581 back.validate()?;
1582 Ok(())
1583 }
1584
1585 #[test]
1586 fn overlay_margin_accepts_uniform_and_sides() -> TestResult {
1587 let uniform: OverlaySpec = serde_json::from_value(serde_json::json!({"margin": 3}))?;
1588 assert_eq!(uniform.margin, Some(OverlayMarginWire::Uniform(3)));
1589
1590 let sides: OverlaySpec = serde_json::from_value(serde_json::json!({
1591 "margin": {"top": 1, "right": 2, "bottom": 3, "left": 4}
1592 }))?;
1593 assert_eq!(
1594 sides.margin,
1595 Some(OverlayMarginWire::Sides(OverlayMargin {
1596 top: 1,
1597 right: 2,
1598 bottom: 3,
1599 left: 4,
1600 }))
1601 );
1602 Ok(())
1603 }
1604
1605 #[test]
1606 fn ui_slot_rejects_forbidden_and_oversized_links() {
1607 for link in [
1608 serde_json::json!({"uri": "javascript:alert(1)"}),
1609 serde_json::json!({"uri": "file:///tmp/x"}),
1610 serde_json::json!({"uri": format!("https://example.com/{}", "x".repeat(2048))}),
1611 serde_json::json!({"id": "x".repeat(129), "uri": "https://example.com"}),
1612 ] {
1613 let frame = Frame::event(
1614 0,
1615 Method::UiSlot,
1616 serde_json::json!({
1617 "key": "bad",
1618 "generation": 1,
1619 "placement": "aboveEditor",
1620 "height": 1,
1621 "runs": [[{"text": "bad", "style": {"link": link}}]]
1622 }),
1623 );
1624 assert!(matches!(
1625 frame.validate(false),
1626 Err(ProtocolError::InvalidFrame(_))
1627 ));
1628 }
1629 }
1630
1631 #[test]
1632 fn dialog_payloads_roundtrip() -> TestResult {
1633 let select = SelectRequest {
1634 title: "Pick".to_owned(),
1635 options: vec!["a".to_owned(), "b".to_owned()],
1636 options_meta: DialogOptions {
1637 timeout_ms: Some(1000),
1638 },
1639 };
1640 let frame = Frame::request(7, Method::Select, to_payload(&select)?);
1641 let line = encode_frame_string(&frame)?;
1642 let decoded = decode_frame_str(line.trim_end())?;
1643 assert_eq!(from_payload::<SelectRequest>(&decoded.payload)?, select);
1644
1645 let confirm = ConfirmResponse { confirmed: true };
1646 let frame = Frame::response(7, Method::Confirm, to_payload(&confirm)?);
1647 let line = encode_frame_string(&frame)?;
1648 let decoded = decode_frame_str(line.trim_end())?;
1649 assert!(from_payload::<ConfirmResponse>(&decoded.payload)?.confirmed);
1650 Ok(())
1651 }
1652
1653 #[test]
1654 fn ui_event_wire_variants() -> TestResult {
1655 let events = [
1656 UiEventWire::Key {
1657 code: "enter".to_owned(),
1658 modifiers: KeyModifiersWire {
1659 ctrl: Some(true),
1660 ..KeyModifiersWire::default()
1661 },
1662 kind: KeyEventKindWire::Press,
1663 },
1664 UiEventWire::Paste {
1665 text: "a\nb".to_owned(),
1666 },
1667 UiEventWire::FocusGained,
1668 UiEventWire::FocusLost,
1669 UiEventWire::Resize {
1670 width: 80,
1671 height: 24,
1672 },
1673 ];
1674 for event in events {
1675 let frame = Frame::request(2, Method::UiEvent, to_payload(&event)?);
1676 let line = encode_frame_string(&frame)?;
1677 let decoded = decode_frame_str(line.trim_end())?;
1678 assert_eq!(from_payload::<UiEventWire>(&decoded.payload)?, event);
1679 }
1680 Ok(())
1681 }
1682
1683 #[test]
1684 fn decoder_fragmentation_and_multiple() -> TestResult {
1685 let first = sample_hello_req()?;
1686 let second = Frame::response(1, Method::Hello, to_payload(&HelloAck::local())?);
1687 let mut bytes = encode_frame(&first)?;
1688 bytes.extend(encode_frame(&second)?);
1689 let mut decoder = FrameDecoder::new();
1690 let mut got = Vec::new();
1691 for byte in bytes {
1692 got.extend(decoder.push(&[byte])?);
1693 }
1694 assert!(decoder.finish()?.is_none());
1695 assert_eq!(got, vec![first, second]);
1696 Ok(())
1697 }
1698
1699 #[test]
1700 fn decoder_crlf() -> TestResult {
1701 let frame = sample_hello_req()?;
1702 let mut line = serde_json::to_vec(&frame)?;
1703 line.extend_from_slice(b"\r\n");
1704 let mut decoder = FrameDecoder::new();
1705 let got = decoder.push(&line)?;
1706 assert_eq!(got.first(), Some(&frame));
1707 assert_eq!(got.len(), 1);
1708 Ok(())
1709 }
1710
1711 #[test]
1712 fn decoder_final_line_without_newline() -> TestResult {
1713 let frame = sample_hello_req()?;
1714 let line = serde_json::to_vec(&frame)?;
1715 let mut decoder = FrameDecoder::new();
1716 assert!(decoder.push(&line)?.is_empty());
1717 assert_eq!(decoder.finish_with_final_line()?, Some(frame));
1718
1719 let mut strict = FrameDecoder::new();
1720 assert!(strict.push(&line)?.is_empty());
1721 assert!(matches!(strict.finish(), Err(ProtocolError::Truncated)));
1722 Ok(())
1723 }
1724
1725 #[test]
1726 fn decoder_invalid_utf8_and_json() {
1727 let mut decoder = FrameDecoder::new();
1728 assert!(matches!(
1729 decoder.push(b"\xff\n"),
1730 Err(ProtocolError::InvalidUtf8(_))
1731 ));
1732 let mut decoder = FrameDecoder::new();
1733 assert!(matches!(
1734 decoder.push(b"{not-json}\n"),
1735 Err(ProtocolError::InvalidJson(_))
1736 ));
1737 }
1738
1739 #[test]
1740 fn decoder_oversized_before_growth() -> TestResult {
1741 let limit = 64;
1742 let mut decoder = FrameDecoder::with_max_frame_bytes(limit);
1743 assert_eq!(
1744 decoder.push(&vec![b'a'; limit + 1]),
1745 Err(ProtocolError::FrameTooLarge)
1746 );
1747 assert_eq!(decoder.buffered_len(), 0);
1748
1749 let mut decoder = FrameDecoder::with_max_frame_bytes(limit);
1750 assert!(decoder.push(&vec![b'b'; limit / 2])?.is_empty());
1751 assert_eq!(
1752 decoder.push(&vec![b'c'; limit]),
1753 Err(ProtocolError::FrameTooLarge)
1754 );
1755 Ok(())
1756 }
1757
1758 #[test]
1759 fn strict_unknown_method() -> TestResult {
1760 let frame = Frame {
1761 id: 1,
1762 kind: FrameKind::Req,
1763 method: "notAllowlisted".to_owned(),
1764 payload: empty_object(),
1765 };
1766 let line = encode_frame_string(&frame)?;
1767 assert!(decode_frame_str(line.trim_end()).is_ok());
1768 assert!(matches!(
1769 decode_frame_str_strict(line.trim_end()),
1770 Err(ProtocolError::UnknownMethod(_))
1771 ));
1772 Ok(())
1773 }
1774
1775 #[test]
1776 fn error_payload_shape() -> TestResult {
1777 let error = ErrorPayload {
1778 code: "extension_error".to_owned(),
1779 message: "boom".to_owned(),
1780 retryable: false,
1781 data: Some(serde_json::json!({"path": "x.ts"})),
1782 };
1783 let frame = Frame::error_frame(9, Method::ExtensionError, &error)?;
1784 let line = encode_frame_string(&frame)?;
1785 let decoded = decode_frame_str(line.trim_end())?;
1786 assert_eq!(from_payload::<ErrorPayload>(&decoded.payload)?, error);
1787 Ok(())
1788 }
1789
1790 #[test]
1791 fn shared_fixtures_field_and_discriminant_parity() -> TestResult {
1792 let mut count = 0usize;
1793 for line in FIXTURES.lines() {
1794 if line.trim().is_empty() || line.trim_start().starts_with('#') {
1795 continue;
1796 }
1797 let frame = decode_frame_str_strict(line)?;
1798 let encoded = encode_frame_string(&frame)?;
1799 let again = decode_frame_str_strict(encoded.trim_end())?;
1800 assert_eq!(again, frame);
1801 let method = frame.method_enum();
1802 assert!(method.is_some(), "strict decode accepted unknown method");
1803 assert!(method.is_some_and(|method| Method::ALL.contains(&method)));
1804 count += 1;
1805 }
1806 assert!(count >= 8);
1807 Ok(())
1808 }
1809
1810 #[test]
1811 fn eight_mib_limit_constant_and_encode_guard() {
1812 let frame = Frame {
1813 id: 1,
1814 kind: FrameKind::Req,
1815 method: Method::Notify.as_str().to_owned(),
1816 payload: serde_json::json!({"blob": "x".repeat(MAX_FRAME_BYTES)}),
1817 };
1818 assert_eq!(encode_frame(&frame), Err(ProtocolError::FrameTooLarge));
1819 }
1820
1821 #[test]
1822 fn hyperlink_validation() -> TestResult {
1823 Hyperlink {
1824 id: None,
1825 uri: "https://ok".to_owned(),
1826 }
1827 .validate()?;
1828 assert!(
1829 Hyperlink {
1830 id: None,
1831 uri: "javascript:alert(1)".to_owned(),
1832 }
1833 .validate()
1834 .is_err()
1835 );
1836 assert!(
1837 Hyperlink {
1838 id: Some("a".repeat(Hyperlink::MAX_ID_BYTES + 1)),
1839 uri: "https://ok".to_owned(),
1840 }
1841 .validate()
1842 .is_err()
1843 );
1844 Ok(())
1845 }
1846
1847 #[test]
1848 fn extension_control_payloads_roundtrip() -> TestResult {
1849 let flags = FlagsSetRequest {
1850 values: BTreeMap::from([
1851 ("plan".to_owned(), FlagValueWire::Boolean(true)),
1852 (
1853 "profile".to_owned(),
1854 FlagValueWire::String("fast".to_owned()),
1855 ),
1856 ]),
1857 };
1858 let payload = to_payload(&flags)?;
1859 assert_eq!(from_payload::<FlagsSetRequest>(&payload)?, flags);
1860
1861 let shortcut = ShortcutExecuteRequest {
1862 key: "ctrl+alt+p".to_owned(),
1863 };
1864 let payload = to_payload(&shortcut)?;
1865 assert_eq!(from_payload::<ShortcutExecuteRequest>(&payload)?, shortcut);
1866
1867 let ui = UiEventRequest {
1868 key: "overlay.1".to_owned(),
1869 generation: 2,
1870 event: UiEventWire::Paste {
1871 text: "hello".to_owned(),
1872 },
1873 data: Some("hello".to_owned()),
1874 };
1875 let frame = Frame::request(9, Method::UiEvent, to_payload(&ui)?);
1876 let decoded = decode_frame_str(encode_frame_string(&frame)?.trim_end())?;
1877 assert_eq!(from_payload::<UiEventRequest>(&decoded.payload)?, ui);
1878 Ok(())
1879 }
1880}