1use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_EXEC_TIMEOUT_NS: u64 = 5_000_000_000;
10
11pub const MAX_OUTPUT_BYTES: usize = 16 * 1024 * 1024;
13
14pub const FRAME_EXEC_CHUNK: u8 = 0x01;
16
17pub const FRAME_EXEC_EXIT: u8 = 0x02;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ExecRequest {
23 pub cmd: Vec<String>,
25 pub timeout_ns: u64,
27 #[serde(default)]
29 pub env: Vec<String>,
30 #[serde(default)]
32 pub working_dir: Option<String>,
33 #[serde(default)]
35 pub stdin: Option<Vec<u8>>,
36 #[serde(default)]
38 pub user: Option<String>,
39 #[serde(default)]
41 pub streaming: bool,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ExecOutput {
47 pub stdout: Vec<u8>,
49 pub stderr: Vec<u8>,
51 pub exit_code: i32,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum StreamType {
58 Stdout,
60 Stderr,
62}
63
64impl std::fmt::Display for StreamType {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 StreamType::Stdout => write!(f, "stdout"),
68 StreamType::Stderr => write!(f, "stderr"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ExecChunk {
76 pub stream: StreamType,
78 pub data: Vec<u8>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ExecExit {
85 pub exit_code: i32,
87}
88
89#[derive(Debug, Clone)]
91pub enum ExecEvent {
92 Chunk(ExecChunk),
94 Exit(ExecExit),
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
100pub struct ExecMetrics {
101 pub duration_ms: u64,
103 #[serde(default)]
105 pub peak_memory_bytes: Option<u64>,
106 pub stdout_bytes: u64,
108 pub stderr_bytes: u64,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct FileRequest {
115 pub op: FileOp,
117 pub guest_path: String,
119 #[serde(default)]
121 pub data: Option<String>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126pub enum FileOp {
127 Upload,
129 Download,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FileResponse {
136 pub success: bool,
138 #[serde(default)]
140 pub data: Option<String>,
141 #[serde(default)]
143 pub size: u64,
144 #[serde(default)]
146 pub error: Option<String>,
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_exec_request_serialization_roundtrip() {
155 let req = ExecRequest {
156 cmd: vec!["ls".to_string(), "-la".to_string()],
157 timeout_ns: 3_000_000_000,
158 env: vec!["FOO=bar".to_string()],
159 working_dir: Some("/tmp".to_string()),
160 stdin: None,
161 user: None,
162 streaming: false,
163 };
164 let json = serde_json::to_string(&req).unwrap();
165 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
166 assert_eq!(parsed.cmd, vec!["ls", "-la"]);
167 assert_eq!(parsed.timeout_ns, 3_000_000_000);
168 assert_eq!(parsed.env, vec!["FOO=bar"]);
169 assert_eq!(parsed.working_dir, Some("/tmp".to_string()));
170 assert!(parsed.stdin.is_none());
171 assert!(parsed.user.is_none());
172 assert!(!parsed.streaming);
173 }
174
175 #[test]
176 fn test_exec_request_streaming_flag() {
177 let req = ExecRequest {
178 cmd: vec!["tail".to_string(), "-f".to_string()],
179 timeout_ns: 0,
180 env: vec![],
181 working_dir: None,
182 stdin: None,
183 user: None,
184 streaming: true,
185 };
186 let json = serde_json::to_string(&req).unwrap();
187 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
188 assert!(parsed.streaming);
189 }
190
191 #[test]
192 fn test_exec_output_serialization_roundtrip() {
193 let output = ExecOutput {
194 stdout: b"hello\n".to_vec(),
195 stderr: b"warning\n".to_vec(),
196 exit_code: 0,
197 };
198 let json = serde_json::to_string(&output).unwrap();
199 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
200 assert_eq!(parsed.stdout, b"hello\n");
201 assert_eq!(parsed.stderr, b"warning\n");
202 assert_eq!(parsed.exit_code, 0);
203 }
204
205 #[test]
206 fn test_exec_output_non_zero_exit() {
207 let output = ExecOutput {
208 stdout: vec![],
209 stderr: b"not found\n".to_vec(),
210 exit_code: 127,
211 };
212 let json = serde_json::to_string(&output).unwrap();
213 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
214 assert_eq!(parsed.exit_code, 127);
215 assert!(parsed.stdout.is_empty());
216 }
217
218 #[test]
219 fn test_default_timeout_constant() {
220 assert_eq!(DEFAULT_EXEC_TIMEOUT_NS, 5_000_000_000);
221 }
222
223 #[test]
224 fn test_max_output_bytes_constant() {
225 assert_eq!(MAX_OUTPUT_BYTES, 16 * 1024 * 1024);
226 }
227
228 #[test]
229 fn test_exec_request_empty_cmd() {
230 let req = ExecRequest {
231 cmd: vec![],
232 timeout_ns: 0,
233 env: vec![],
234 working_dir: None,
235 stdin: None,
236 user: None,
237 streaming: false,
238 };
239 let json = serde_json::to_string(&req).unwrap();
240 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
241 assert!(parsed.cmd.is_empty());
242 assert_eq!(parsed.timeout_ns, 0);
243 assert!(parsed.env.is_empty());
244 assert!(parsed.working_dir.is_none());
245 assert!(parsed.user.is_none());
246 }
247
248 #[test]
249 fn test_exec_request_backward_compatible_deserialization() {
250 let json = r#"{"cmd":["ls"],"timeout_ns":0}"#;
252 let parsed: ExecRequest = serde_json::from_str(json).unwrap();
253 assert_eq!(parsed.cmd, vec!["ls"]);
254 assert!(parsed.env.is_empty());
255 assert!(parsed.working_dir.is_none());
256 assert!(parsed.stdin.is_none());
257 assert!(parsed.user.is_none());
258 assert!(!parsed.streaming);
259 }
260
261 #[test]
262 fn test_exec_request_with_stdin() {
263 let req = ExecRequest {
264 cmd: vec!["sh".to_string()],
265 timeout_ns: 0,
266 env: vec![],
267 working_dir: None,
268 stdin: Some(b"echo hello\n".to_vec()),
269 user: None,
270 streaming: false,
271 };
272 let json = serde_json::to_string(&req).unwrap();
273 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
274 assert_eq!(parsed.stdin, Some(b"echo hello\n".to_vec()));
275 }
276
277 #[test]
278 fn test_exec_request_with_user() {
279 let req = ExecRequest {
280 cmd: vec!["whoami".to_string()],
281 timeout_ns: 0,
282 env: vec![],
283 working_dir: None,
284 stdin: None,
285 user: Some("root".to_string()),
286 streaming: false,
287 };
288 let json = serde_json::to_string(&req).unwrap();
289 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
290 assert_eq!(parsed.user, Some("root".to_string()));
291 }
292
293 #[test]
294 fn test_exec_request_with_user_uid_gid() {
295 let req = ExecRequest {
296 cmd: vec!["id".to_string()],
297 timeout_ns: 0,
298 env: vec![],
299 working_dir: None,
300 stdin: None,
301 user: Some("1000:1000".to_string()),
302 streaming: false,
303 };
304 let json = serde_json::to_string(&req).unwrap();
305 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
306 assert_eq!(parsed.user, Some("1000:1000".to_string()));
307 }
308
309 #[test]
310 fn test_exec_output_empty() {
311 let output = ExecOutput {
312 stdout: vec![],
313 stderr: vec![],
314 exit_code: 0,
315 };
316 assert!(output.stdout.is_empty());
317 assert!(output.stderr.is_empty());
318 assert_eq!(output.exit_code, 0);
319 }
320
321 #[test]
324 fn test_stream_type_display() {
325 assert_eq!(StreamType::Stdout.to_string(), "stdout");
326 assert_eq!(StreamType::Stderr.to_string(), "stderr");
327 }
328
329 #[test]
330 fn test_exec_chunk_serde_roundtrip() {
331 let chunk = ExecChunk {
332 stream: StreamType::Stdout,
333 data: b"hello world\n".to_vec(),
334 };
335 let json = serde_json::to_string(&chunk).unwrap();
336 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
337 assert_eq!(parsed.stream, StreamType::Stdout);
338 assert_eq!(parsed.data, b"hello world\n");
339 }
340
341 #[test]
342 fn test_exec_chunk_stderr() {
343 let chunk = ExecChunk {
344 stream: StreamType::Stderr,
345 data: b"error: not found\n".to_vec(),
346 };
347 let json = serde_json::to_string(&chunk).unwrap();
348 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
349 assert_eq!(parsed.stream, StreamType::Stderr);
350 }
351
352 #[test]
353 fn test_exec_exit_serde_roundtrip() {
354 let exit = ExecExit { exit_code: 42 };
355 let json = serde_json::to_string(&exit).unwrap();
356 let parsed: ExecExit = serde_json::from_str(&json).unwrap();
357 assert_eq!(parsed.exit_code, 42);
358 }
359
360 #[test]
361 fn test_exec_metrics_default() {
362 let m = ExecMetrics::default();
363 assert_eq!(m.duration_ms, 0);
364 assert!(m.peak_memory_bytes.is_none());
365 assert_eq!(m.stdout_bytes, 0);
366 assert_eq!(m.stderr_bytes, 0);
367 }
368
369 #[test]
370 fn test_exec_metrics_serde_roundtrip() {
371 let m = ExecMetrics {
372 duration_ms: 1234,
373 peak_memory_bytes: Some(65536),
374 stdout_bytes: 100,
375 stderr_bytes: 50,
376 };
377 let json = serde_json::to_string(&m).unwrap();
378 let parsed: ExecMetrics = serde_json::from_str(&json).unwrap();
379 assert_eq!(parsed.duration_ms, 1234);
380 assert_eq!(parsed.peak_memory_bytes, Some(65536));
381 assert_eq!(parsed.stdout_bytes, 100);
382 assert_eq!(parsed.stderr_bytes, 50);
383 }
384
385 #[test]
388 fn test_file_request_upload() {
389 let req = FileRequest {
390 op: FileOp::Upload,
391 guest_path: "/tmp/test.txt".to_string(),
392 data: Some("aGVsbG8=".to_string()),
393 };
394 let json = serde_json::to_string(&req).unwrap();
395 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
396 assert_eq!(parsed.op, FileOp::Upload);
397 assert_eq!(parsed.guest_path, "/tmp/test.txt");
398 assert_eq!(parsed.data.as_deref(), Some("aGVsbG8="));
399 }
400
401 #[test]
402 fn test_file_request_download() {
403 let req = FileRequest {
404 op: FileOp::Download,
405 guest_path: "/etc/hostname".to_string(),
406 data: None,
407 };
408 let json = serde_json::to_string(&req).unwrap();
409 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
410 assert_eq!(parsed.op, FileOp::Download);
411 assert!(parsed.data.is_none());
412 }
413
414 #[test]
415 fn test_file_response_success() {
416 let resp = FileResponse {
417 success: true,
418 data: Some("Y29udGVudA==".to_string()),
419 size: 7,
420 error: None,
421 };
422 let json = serde_json::to_string(&resp).unwrap();
423 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
424 assert!(parsed.success);
425 assert_eq!(parsed.size, 7);
426 assert!(parsed.error.is_none());
427 }
428
429 #[test]
430 fn test_file_response_error() {
431 let resp = FileResponse {
432 success: false,
433 data: None,
434 size: 0,
435 error: Some("file not found".to_string()),
436 };
437 let json = serde_json::to_string(&resp).unwrap();
438 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
439 assert!(!parsed.success);
440 assert_eq!(parsed.error.as_deref(), Some("file not found"));
441 }
442
443 #[test]
444 fn test_frame_exec_constants() {
445 assert_eq!(FRAME_EXEC_CHUNK, 0x01);
446 assert_eq!(FRAME_EXEC_EXIT, 0x02);
447 }
448}