1use std::io::{self, Read, Write};
2
3use serde::{Deserialize, Serialize};
4
5pub const PROTOCOL_VERSION: u32 = 1;
8
9pub const CAP_NOTIFY: &str = "notify";
14pub const CAP_XDG_OPEN: &str = "xdg_open";
15pub const CAP_CLIPBOARD: &str = "clipboard";
16pub const CAP_HOST_EXEC: &str = "host_exec";
17
18pub const ALL_CAPABILITIES: &[&str] = &[CAP_NOTIFY, CAP_XDG_OPEN, CAP_CLIPBOARD, CAP_HOST_EXEC];
20
21#[derive(Debug, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "snake_case")]
24pub enum GuestMessage {
25 Hello {
26 protocol_version: u32,
27 guest_version: String,
28 container: String,
29 capabilities: Vec<String>,
30 },
31 Notify {
32 summary: String,
33 body: String,
34 urgency: String,
35 #[serde(default)]
36 actions: Vec<NotifyAction>,
37 #[serde(default)]
38 app_name: String,
39 },
40 XdgOpen {
41 uri: String,
42 },
43 ClipboardSet {
44 text: String,
45 },
46 ClipboardGet,
47 HostExec {
48 cmd: String,
49 args: Vec<String>,
50 },
51 RegisterSession,
54 Busy,
56 IdleTimeout,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone)]
61pub struct NotifyAction {
62 pub key: String,
63 pub label: String,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68#[serde(tag = "type", rename_all = "snake_case")]
69pub enum HostMessage {
70 HelloAck {
71 accepted: Vec<String>,
72 rejected: Vec<String>,
73 #[serde(default)]
74 idle_timeout_secs: u64,
75 },
76 ClipboardData {
77 text: String,
78 },
79 HostExecStdout {
80 data: String,
81 },
82 HostExecStderr {
83 data: String,
84 },
85 HostExecDone {
86 exit_code: i32,
87 },
88 NotifyActionResult {
89 notification_id: u32,
90 action_key: String,
91 },
92 Ping,
93 Shutdown,
94 CheckIdle,
97 Error {
100 reason: String,
101 },
102}
103
104pub fn write_frame<W: Write>(w: &mut W, msg: &impl Serialize) -> io::Result<()> {
106 let json = serde_json::to_vec(msg)?;
107 let len = u32::try_from(json.len())
108 .expect("frame payload exceeds 4 GiB")
109 .to_be_bytes();
110 w.write_all(&len)?;
111 w.write_all(&json)?;
112 w.flush()?;
113 Ok(())
114}
115
116const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
118
119pub fn read_frame<R: Read>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
121 let mut len_buf = [0u8; 4];
122 match r.read_exact(&mut len_buf) {
123 Ok(()) => {}
124 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
125 Err(e) => return Err(e),
126 }
127 let len = u32::from_be_bytes(len_buf) as usize;
128 if len > MAX_FRAME_SIZE {
129 return Err(io::Error::new(
130 io::ErrorKind::InvalidData,
131 format!("frame too large: {len} bytes (max {MAX_FRAME_SIZE})"),
132 ));
133 }
134 let mut buf = vec![0u8; len];
135 r.read_exact(&mut buf)?;
136 Ok(Some(buf))
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn hello_serializes_with_type_tag() {
145 let msg = GuestMessage::Hello {
146 protocol_version: 1,
147 guest_version: "0.2.0".into(),
148 container: "myenv".into(),
149 capabilities: vec!["notify".into()],
150 };
151 let json = serde_json::to_string(&msg).unwrap();
152 assert!(json.contains("\"type\":\"hello\""));
153 }
154
155 #[test]
156 fn frame_length_prefix_matches_payload() {
157 let msg = GuestMessage::ClipboardGet;
158 let mut buf = Vec::new();
159 write_frame(&mut buf, &msg).unwrap();
160 let len = u32::from_be_bytes(buf[..4].try_into().unwrap()) as usize;
161 assert_eq!(len, buf[4..].len());
162 }
163
164 #[test]
165 fn roundtrip_notify_message() {
166 let msg = GuestMessage::Notify {
167 summary: "hello".into(),
168 body: "world".into(),
169 urgency: "normal".into(),
170 actions: vec![],
171 app_name: String::new(),
172 };
173 let mut buf = Vec::new();
174 write_frame(&mut buf, &msg).unwrap();
175
176 let payload = read_frame(&mut &buf[..]).unwrap().unwrap();
177 let decoded: GuestMessage = serde_json::from_slice(&payload).unwrap();
178 match decoded {
179 GuestMessage::Notify {
180 summary,
181 body,
182 urgency,
183 actions,
184 app_name: _,
185 } => {
186 assert_eq!(summary, "hello");
187 assert_eq!(body, "world");
188 assert_eq!(urgency, "normal");
189 assert!(actions.is_empty());
190 }
191 _ => panic!("wrong message type"),
192 }
193 }
194
195 #[test]
196 fn roundtrip_clipboard_set() {
197 let msg = GuestMessage::ClipboardSet {
198 text: "clipboard content".into(),
199 };
200 let mut buf = Vec::new();
201 write_frame(&mut buf, &msg).unwrap();
202
203 let payload = read_frame(&mut &buf[..]).unwrap().unwrap();
204 let decoded: GuestMessage = serde_json::from_slice(&payload).unwrap();
205 match decoded {
206 GuestMessage::ClipboardSet { text } => {
207 assert_eq!(text, "clipboard content");
208 }
209 _ => panic!("wrong message type"),
210 }
211 }
212}