Skip to main content

shuru_proto/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Binary framing for all vsock communication.
5///
6/// Frame format: `[u32 BE length][u8 type][payload...]`
7/// Length = size of type byte + payload (excludes the 4-byte length prefix).
8/// Max frame size: 1 MB.
9pub mod frame;
10
11// --- Exec protocol ---
12
13#[derive(Serialize, Deserialize)]
14pub struct ExecRequest {
15    pub argv: Vec<String>,
16    #[serde(default)]
17    pub env: HashMap<String, String>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub tty: Option<bool>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub rows: Option<u16>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub cols: Option<u16>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub cwd: Option<String>,
26}
27
28// --- Port forwarding protocol ---
29
30/// A host:guest port mapping for port forwarding over vsock.
31#[derive(Debug, Clone)]
32pub struct PortMapping {
33    pub host_port: u16,
34    pub guest_port: u16,
35}
36
37/// Sent by the host over vsock to request forwarding to a guest port.
38#[derive(Serialize, Deserialize)]
39pub struct ForwardRequest {
40    pub port: u16,
41}
42
43/// Sent by the guest in response to a ForwardRequest.
44#[derive(Serialize, Deserialize)]
45pub struct ForwardResponse {
46    pub status: String,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub message: Option<String>,
49}
50
51// --- Mount protocol ---
52
53/// Sent by the host over vsock to instruct the guest to mount a virtiofs device.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct MountRequest {
56    pub tag: String,
57    pub guest_path: String,
58    /// When true (default), guest mounts via overlay (writes go to tmpfs).
59    /// When false, guest mounts VirtioFS directly (writes go to host).
60    #[serde(default = "default_true")]
61    pub read_only: bool,
62}
63
64/// Sent by the guest in response to a MountRequest.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct MountResponse {
67    pub tag: String,
68    pub ok: bool,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub error: Option<String>,
71}
72
73// --- File I/O protocol ---
74
75#[derive(Serialize, Deserialize)]
76pub struct ReadFileRequest {
77    pub path: String,
78}
79
80#[derive(Serialize, Deserialize)]
81pub struct WriteFileRequest {
82    pub path: String,
83    pub len: u64,
84}
85
86#[derive(Serialize, Deserialize)]
87pub struct WriteFileResponse {
88    pub ok: bool,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub error: Option<String>,
91}
92
93// --- Filesystem operations protocol ---
94
95#[derive(Serialize, Deserialize)]
96pub struct FsOkResponse {
97    pub ok: bool,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub error: Option<String>,
100}
101
102#[derive(Serialize, Deserialize)]
103pub struct DownloadRequest {
104    pub url: String,
105    pub path: String,
106    /// If true, decompress .tar.gz and extract to `path` as a directory.
107    #[serde(default)]
108    pub extract: bool,
109    /// When extracting, strip N leading path components from each entry,
110    /// mirroring `tar --strip-components=N`. Defaults to 0 (keep paths as-is).
111    /// Use `1` for releases wrapped in a single top-level directory
112    /// (Node.js, Pi, etc.) so `node-v22/bin/node` becomes `bin/node`.
113    #[serde(default)]
114    pub strip_components: u32,
115}
116
117#[derive(Serialize, Deserialize)]
118pub struct DownloadProgress {
119    pub bytes_downloaded: u64,
120    pub total_bytes: Option<u64>,
121}
122
123#[derive(Serialize, Deserialize)]
124pub struct MkdirRequest {
125    pub path: String,
126    #[serde(default = "default_true")]
127    pub recursive: bool,
128}
129
130#[derive(Serialize, Deserialize)]
131pub struct ReadDirRequest {
132    pub path: String,
133}
134
135#[derive(Serialize, Deserialize)]
136pub struct DirEntry {
137    pub name: String,
138    #[serde(rename = "type")]
139    pub entry_type: String,
140    pub size: u64,
141}
142
143#[derive(Serialize, Deserialize)]
144pub struct ReadDirResponse {
145    pub entries: Vec<DirEntry>,
146}
147
148#[derive(Serialize, Deserialize)]
149pub struct StatRequest {
150    pub path: String,
151}
152
153#[derive(Serialize, Deserialize)]
154pub struct StatResponse {
155    pub size: u64,
156    pub mode: u32,
157    pub mtime: u64,
158    pub is_dir: bool,
159    pub is_file: bool,
160    pub is_symlink: bool,
161}
162
163#[derive(Serialize, Deserialize)]
164pub struct RemoveRequest {
165    pub path: String,
166    #[serde(default)]
167    pub recursive: bool,
168}
169
170/// Discard overlay changes for a file: removes it from the overlay upper dir,
171/// revealing the original host version from the lower layer.
172#[derive(Serialize, Deserialize)]
173pub struct DiscardRequest {
174    /// Path relative to the overlay mount (e.g., "/workspace/src/main.rs")
175    pub path: String,
176}
177
178#[derive(Serialize, Deserialize)]
179pub struct RenameRequest {
180    pub old_path: String,
181    pub new_path: String,
182}
183
184#[derive(Serialize, Deserialize)]
185pub struct CopyRequest {
186    pub src: String,
187    pub dst: String,
188    #[serde(default)]
189    pub recursive: bool,
190}
191
192#[derive(Serialize, Deserialize)]
193pub struct ChmodRequest {
194    pub path: String,
195    pub mode: u32,
196}
197
198// --- File watching protocol ---
199
200#[derive(Serialize, Deserialize)]
201pub struct WatchRequest {
202    pub path: String,
203    #[serde(default = "default_true")]
204    pub recursive: bool,
205}
206
207fn default_true() -> bool {
208    true
209}
210
211/// Binary watch event kinds.
212pub mod watch_kind {
213    pub const CREATE: u8 = 0x01;
214    pub const MODIFY: u8 = 0x02;
215    pub const DELETE: u8 = 0x03;
216    pub const RENAME: u8 = 0x04;
217}
218
219/// A filesystem watch event. Binary format: `[u8 kind][path bytes...]`
220#[derive(Debug, Clone)]
221pub struct WatchEvent {
222    pub kind: u8,
223    pub path: String,
224}
225
226impl WatchEvent {
227    /// Encode to binary payload for a WATCH_EVENT frame.
228    pub fn encode(&self) -> Vec<u8> {
229        let mut buf = Vec::with_capacity(1 + self.path.len());
230        buf.push(self.kind);
231        buf.extend_from_slice(self.path.as_bytes());
232        buf
233    }
234
235    /// Decode from binary payload of a WATCH_EVENT frame.
236    pub fn decode(payload: &[u8]) -> Option<Self> {
237        if payload.is_empty() {
238            return None;
239        }
240        let kind = payload[0];
241        let path = std::str::from_utf8(&payload[1..]).ok()?.to_string();
242        Some(Self { kind, path })
243    }
244}
245
246pub const VSOCK_PORT: u32 = 1024;
247pub const VSOCK_PORT_FORWARD: u32 = 1025;
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn mount_request_read_only_true_by_default() {
255        let json = r#"{"tag":"mount0","guest_path":"/workspace"}"#;
256        let req: MountRequest = serde_json::from_str(json).unwrap();
257        assert!(req.read_only);
258    }
259
260    #[test]
261    fn mount_request_read_only_false_roundtrips() {
262        let req = MountRequest {
263            tag: "mount0".into(),
264            guest_path: "/workspace".into(),
265            read_only: false,
266        };
267        let json = serde_json::to_string(&req).unwrap();
268        let req2: MountRequest = serde_json::from_str(&json).unwrap();
269        assert!(!req2.read_only);
270    }
271}