1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10pub const EXEC_VSOCK_PORT: u32 = a3s_transport::ports::EXEC_SERVER;
12
13pub const PORT_FWD_VSOCK_PORT: u32 = 4093;
15
16pub const WINDOWS_CONTROL_SIGNAL_FRAME: u8 = 5;
22
23pub const WINDOWS_STOP_REQUEST_FILE: &str = "stop.signal";
25
26pub const WINDOWS_STOP_REQUEST_TEMP_FILE: &str = "stop.signal.tmp";
28
29pub const DEFAULT_EXEC_TIMEOUT_NS: u64 = 5_000_000_000;
31
32pub const MAX_OUTPUT_BYTES: usize = 16 * 1024 * 1024;
34
35pub const MAX_ONE_SHOT_OUTPUT_BYTES: usize = 1024 * 1024;
42
43pub const FRAME_EXEC_CHUNK: u8 = 0x01;
45
46pub const FRAME_EXEC_EXIT: u8 = 0x02;
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct ExecRequest {
52 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub request_id: Option<String>,
61 pub cmd: Vec<String>,
63 pub timeout_ns: u64,
65 #[serde(default)]
67 pub env: Vec<String>,
68 #[serde(default)]
70 pub working_dir: Option<String>,
71 #[serde(default)]
73 pub rootfs: Option<String>,
74 #[serde(default)]
76 pub stdin: Option<Vec<u8>>,
77 #[serde(default)]
79 pub stdin_streaming: bool,
80 #[serde(default)]
82 pub user: Option<String>,
83 #[serde(default)]
85 pub streaming: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct ExecOutput {
91 pub stdout: Vec<u8>,
93 pub stderr: Vec<u8>,
95 pub exit_code: i32,
97 #[serde(default)]
100 pub truncated: bool,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum StreamType {
106 Stdout,
108 Stderr,
110}
111
112impl std::fmt::Display for StreamType {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 StreamType::Stdout => write!(f, "stdout"),
116 StreamType::Stderr => write!(f, "stderr"),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ExecChunk {
124 pub stream: StreamType,
126 pub data: Vec<u8>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ExecExit {
133 pub exit_code: i32,
135 #[serde(default)]
139 pub oom_killed: bool,
140}
141
142#[derive(Debug, Clone)]
145pub enum ExecEvent {
146 Chunk(ExecChunk),
148 FlushAck,
153 Exit(ExecExit),
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct ExecMetrics {
160 pub duration_ms: u64,
162 #[serde(default)]
164 pub peak_memory_bytes: Option<u64>,
165 pub stdout_bytes: u64,
167 pub stderr_bytes: u64,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct FileRequest {
174 pub op: FileOp,
176 pub guest_path: String,
178 #[serde(default)]
180 pub data: Option<String>,
181 #[serde(default)]
183 pub user: Option<String>,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188pub enum FileOp {
189 Upload,
191 Download,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct FileResponse {
198 pub success: bool,
200 #[serde(default)]
202 pub data: Option<String>,
203 #[serde(default)]
205 pub size: u64,
206 #[serde(default)]
208 pub error: Option<String>,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213pub enum FilesystemOp {
214 Stat,
216 MakeDir,
218 Move,
220 ListDir,
222 Remove,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct FilesystemRequest {
229 pub op: FilesystemOp,
231 pub path: String,
233 #[serde(default)]
235 pub destination: Option<String>,
236 #[serde(default)]
238 pub depth: u32,
239 #[serde(default)]
241 pub user: Option<String>,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub enum FilesystemEntryKind {
247 Unspecified,
249 File,
251 Directory,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct FilesystemEntry {
258 pub name: String,
259 pub kind: FilesystemEntryKind,
260 pub path: String,
261 pub size: i64,
262 pub mode: u32,
263 pub permissions: String,
264 pub owner: String,
265 pub group: String,
266 pub modified_seconds: i64,
267 pub modified_nanos: i32,
268 #[serde(default)]
269 pub symlink_target: Option<String>,
270 #[serde(default)]
271 pub metadata: BTreeMap<String, String>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct FilesystemResponse {
277 pub success: bool,
278 #[serde(default)]
279 pub entry: Option<FilesystemEntry>,
280 #[serde(default)]
281 pub entries: Vec<FilesystemEntry>,
282 #[serde(default)]
283 pub error: Option<String>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(tag = "request_type", content = "request", rename_all = "snake_case")]
293pub enum GuestSessionRequest {
294 File(FileRequest),
296 Filesystem(FilesystemRequest),
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_exec_request_serialization_roundtrip() {
306 let req = ExecRequest {
307 request_id: Some("exec-1".to_string()),
308 cmd: vec!["ls".to_string(), "-la".to_string()],
309 timeout_ns: 3_000_000_000,
310 env: vec!["FOO=bar".to_string()],
311 working_dir: Some("/tmp".to_string()),
312 rootfs: Some("/run/a3s/cri/rootfs/sb/c/rootfs".to_string()),
313 stdin: None,
314 stdin_streaming: false,
315 user: None,
316 streaming: false,
317 };
318 let json = serde_json::to_string(&req).unwrap();
319 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
320 assert_eq!(parsed.cmd, vec!["ls", "-la"]);
321 assert_eq!(parsed.request_id.as_deref(), Some("exec-1"));
322 assert_eq!(parsed.timeout_ns, 3_000_000_000);
323 assert_eq!(parsed.env, vec!["FOO=bar"]);
324 assert_eq!(parsed.working_dir, Some("/tmp".to_string()));
325 assert_eq!(
326 parsed.rootfs,
327 Some("/run/a3s/cri/rootfs/sb/c/rootfs".to_string())
328 );
329 assert!(parsed.stdin.is_none());
330 assert!(!parsed.stdin_streaming);
331 assert!(parsed.user.is_none());
332 assert!(!parsed.streaming);
333 }
334
335 #[test]
336 fn test_exec_request_streaming_flag() {
337 let req = ExecRequest {
338 request_id: None,
339 cmd: vec!["tail".to_string(), "-f".to_string()],
340 timeout_ns: 0,
341 env: vec![],
342 working_dir: None,
343 rootfs: None,
344 stdin: None,
345 stdin_streaming: false,
346 user: None,
347 streaming: true,
348 };
349 let json = serde_json::to_string(&req).unwrap();
350 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
351 assert!(parsed.streaming);
352 assert!(!parsed.stdin_streaming);
353 }
354
355 #[test]
356 fn test_exec_request_stdin_streaming_flag() {
357 let req = ExecRequest {
358 request_id: None,
359 cmd: vec!["cat".to_string()],
360 timeout_ns: 0,
361 env: vec![],
362 working_dir: None,
363 rootfs: None,
364 stdin: None,
365 stdin_streaming: true,
366 user: None,
367 streaming: true,
368 };
369 let json = serde_json::to_string(&req).unwrap();
370 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
371 assert!(parsed.stdin_streaming);
372 }
373
374 #[test]
375 fn test_exec_output_serialization_roundtrip() {
376 let output = ExecOutput {
377 stdout: b"hello\n".to_vec(),
378 stderr: b"warning\n".to_vec(),
379 exit_code: 0,
380 truncated: true,
381 };
382 let json = serde_json::to_string(&output).unwrap();
383 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
384 assert_eq!(parsed.stdout, b"hello\n");
385 assert_eq!(parsed.stderr, b"warning\n");
386 assert_eq!(parsed.exit_code, 0);
387 assert!(parsed.truncated);
388 }
389
390 #[test]
391 fn test_exec_output_non_zero_exit() {
392 let output = ExecOutput {
393 stdout: vec![],
394 stderr: b"not found\n".to_vec(),
395 exit_code: 127,
396 truncated: false,
397 };
398 let json = serde_json::to_string(&output).unwrap();
399 let parsed: ExecOutput = serde_json::from_str(&json).unwrap();
400 assert_eq!(parsed.exit_code, 127);
401 assert!(parsed.stdout.is_empty());
402 }
403
404 #[test]
405 fn test_default_timeout_constant() {
406 assert_eq!(DEFAULT_EXEC_TIMEOUT_NS, 5_000_000_000);
407 }
408
409 #[test]
410 fn test_max_output_bytes_constant() {
411 assert_eq!(MAX_OUTPUT_BYTES, 16 * 1024 * 1024);
412 assert_eq!(MAX_ONE_SHOT_OUTPUT_BYTES, 1024 * 1024);
413 }
414
415 #[test]
416 fn maximum_one_shot_output_fits_one_transport_frame() {
417 let output = ExecOutput {
418 stdout: vec![u8::MAX; MAX_ONE_SHOT_OUTPUT_BYTES],
419 stderr: vec![u8::MAX; MAX_ONE_SHOT_OUTPUT_BYTES],
420 exit_code: i32::MIN,
421 truncated: true,
422 };
423 let encoded = serde_json::to_vec(&output).unwrap();
424 assert!(encoded.len() <= a3s_transport::MAX_PAYLOAD_SIZE as usize);
425 }
426
427 #[test]
428 fn test_exec_request_empty_cmd() {
429 let req = ExecRequest {
430 request_id: None,
431 cmd: vec![],
432 timeout_ns: 0,
433 env: vec![],
434 working_dir: None,
435 rootfs: None,
436 stdin: None,
437 stdin_streaming: false,
438 user: None,
439 streaming: false,
440 };
441 let json = serde_json::to_string(&req).unwrap();
442 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
443 assert!(parsed.cmd.is_empty());
444 assert!(parsed.request_id.is_none());
445 assert_eq!(parsed.timeout_ns, 0);
446 assert!(parsed.env.is_empty());
447 assert!(parsed.working_dir.is_none());
448 assert!(parsed.rootfs.is_none());
449 assert!(!parsed.stdin_streaming);
450 assert!(parsed.user.is_none());
451 }
452
453 #[test]
454 fn test_exec_request_backward_compatible_deserialization() {
455 let json = r#"{"cmd":["ls"],"timeout_ns":0}"#;
457 let parsed: ExecRequest = serde_json::from_str(json).unwrap();
458 assert_eq!(parsed.cmd, vec!["ls"]);
459 assert!(parsed.request_id.is_none());
460 assert!(parsed.env.is_empty());
461 assert!(parsed.working_dir.is_none());
462 assert!(parsed.rootfs.is_none());
463 assert!(parsed.stdin.is_none());
464 assert!(!parsed.stdin_streaming);
465 assert!(parsed.user.is_none());
466 assert!(!parsed.streaming);
467 }
468
469 #[test]
470 fn test_exec_request_with_stdin() {
471 let req = ExecRequest {
472 request_id: None,
473 cmd: vec!["sh".to_string()],
474 timeout_ns: 0,
475 env: vec![],
476 working_dir: None,
477 rootfs: None,
478 stdin: Some(b"echo hello\n".to_vec()),
479 stdin_streaming: false,
480 user: None,
481 streaming: false,
482 };
483 let json = serde_json::to_string(&req).unwrap();
484 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
485 assert_eq!(parsed.stdin, Some(b"echo hello\n".to_vec()));
486 assert!(!parsed.stdin_streaming);
487 }
488
489 #[test]
490 fn test_exec_request_with_user() {
491 let req = ExecRequest {
492 request_id: None,
493 cmd: vec!["whoami".to_string()],
494 timeout_ns: 0,
495 env: vec![],
496 working_dir: None,
497 rootfs: None,
498 stdin: None,
499 stdin_streaming: false,
500 user: Some("root".to_string()),
501 streaming: false,
502 };
503 let json = serde_json::to_string(&req).unwrap();
504 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
505 assert_eq!(parsed.user, Some("root".to_string()));
506 }
507
508 #[test]
509 fn test_exec_request_with_user_uid_gid() {
510 let req = ExecRequest {
511 request_id: None,
512 cmd: vec!["id".to_string()],
513 timeout_ns: 0,
514 env: vec![],
515 working_dir: None,
516 rootfs: None,
517 stdin: None,
518 stdin_streaming: false,
519 user: Some("1000:1000".to_string()),
520 streaming: false,
521 };
522 let json = serde_json::to_string(&req).unwrap();
523 let parsed: ExecRequest = serde_json::from_str(&json).unwrap();
524 assert_eq!(parsed.user, Some("1000:1000".to_string()));
525 }
526
527 #[test]
528 fn test_exec_output_empty() {
529 let output = ExecOutput {
530 stdout: vec![],
531 stderr: vec![],
532 exit_code: 0,
533 truncated: false,
534 };
535 assert!(output.stdout.is_empty());
536 assert!(output.stderr.is_empty());
537 assert_eq!(output.exit_code, 0);
538 assert!(!output.truncated);
539 }
540
541 #[test]
542 fn test_exec_output_backward_compatible_deserialization() {
543 let parsed: ExecOutput =
544 serde_json::from_str(r#"{"stdout":[],"stderr":[],"exit_code":0}"#).unwrap();
545 assert!(!parsed.truncated);
546 }
547
548 #[test]
551 fn test_stream_type_display() {
552 assert_eq!(StreamType::Stdout.to_string(), "stdout");
553 assert_eq!(StreamType::Stderr.to_string(), "stderr");
554 }
555
556 #[test]
557 fn test_exec_chunk_serde_roundtrip() {
558 let chunk = ExecChunk {
559 stream: StreamType::Stdout,
560 data: b"hello world\n".to_vec(),
561 };
562 let json = serde_json::to_string(&chunk).unwrap();
563 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
564 assert_eq!(parsed.stream, StreamType::Stdout);
565 assert_eq!(parsed.data, b"hello world\n");
566 }
567
568 #[test]
569 fn test_exec_chunk_stderr() {
570 let chunk = ExecChunk {
571 stream: StreamType::Stderr,
572 data: b"error: not found\n".to_vec(),
573 };
574 let json = serde_json::to_string(&chunk).unwrap();
575 let parsed: ExecChunk = serde_json::from_str(&json).unwrap();
576 assert_eq!(parsed.stream, StreamType::Stderr);
577 }
578
579 #[test]
580 fn test_exec_exit_serde_roundtrip() {
581 let exit = ExecExit {
582 exit_code: 42,
583 oom_killed: false,
584 };
585 let json = serde_json::to_string(&exit).unwrap();
586 let parsed: ExecExit = serde_json::from_str(&json).unwrap();
587 assert_eq!(parsed.exit_code, 42);
588 }
589
590 #[test]
591 fn test_exec_metrics_default() {
592 let m = ExecMetrics::default();
593 assert_eq!(m.duration_ms, 0);
594 assert!(m.peak_memory_bytes.is_none());
595 assert_eq!(m.stdout_bytes, 0);
596 assert_eq!(m.stderr_bytes, 0);
597 }
598
599 #[test]
600 fn test_exec_metrics_serde_roundtrip() {
601 let m = ExecMetrics {
602 duration_ms: 1234,
603 peak_memory_bytes: Some(65536),
604 stdout_bytes: 100,
605 stderr_bytes: 50,
606 };
607 let json = serde_json::to_string(&m).unwrap();
608 let parsed: ExecMetrics = serde_json::from_str(&json).unwrap();
609 assert_eq!(parsed.duration_ms, 1234);
610 assert_eq!(parsed.peak_memory_bytes, Some(65536));
611 assert_eq!(parsed.stdout_bytes, 100);
612 assert_eq!(parsed.stderr_bytes, 50);
613 }
614
615 #[test]
618 fn test_file_request_upload() {
619 let req = FileRequest {
620 op: FileOp::Upload,
621 guest_path: "/tmp/test.txt".to_string(),
622 data: Some("aGVsbG8=".to_string()),
623 user: Some("1000:1000".to_string()),
624 };
625 let json = serde_json::to_string(&req).unwrap();
626 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
627 assert_eq!(parsed.op, FileOp::Upload);
628 assert_eq!(parsed.guest_path, "/tmp/test.txt");
629 assert_eq!(parsed.data.as_deref(), Some("aGVsbG8="));
630 }
631
632 #[test]
633 fn test_file_request_download() {
634 let req = FileRequest {
635 op: FileOp::Download,
636 guest_path: "/etc/hostname".to_string(),
637 data: None,
638 user: None,
639 };
640 let json = serde_json::to_string(&req).unwrap();
641 let parsed: FileRequest = serde_json::from_str(&json).unwrap();
642 assert_eq!(parsed.op, FileOp::Download);
643 assert!(parsed.data.is_none());
644 }
645
646 #[test]
647 fn test_file_response_success() {
648 let resp = FileResponse {
649 success: true,
650 data: Some("Y29udGVudA==".to_string()),
651 size: 7,
652 error: None,
653 };
654 let json = serde_json::to_string(&resp).unwrap();
655 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
656 assert!(parsed.success);
657 assert_eq!(parsed.size, 7);
658 assert!(parsed.error.is_none());
659 }
660
661 #[test]
662 fn test_file_response_error() {
663 let resp = FileResponse {
664 success: false,
665 data: None,
666 size: 0,
667 error: Some("file not found".to_string()),
668 };
669 let json = serde_json::to_string(&resp).unwrap();
670 let parsed: FileResponse = serde_json::from_str(&json).unwrap();
671 assert!(!parsed.success);
672 assert_eq!(parsed.error.as_deref(), Some("file not found"));
673 }
674
675 #[test]
676 fn file_session_request_has_an_unambiguous_wire_discriminator() {
677 let request = GuestSessionRequest::File(FileRequest {
678 op: FileOp::Download,
679 guest_path: "/tmp/data.bin".to_string(),
680 data: None,
681 user: None,
682 });
683
684 let value = serde_json::to_value(&request).unwrap();
685 assert_eq!(value["request_type"], "file");
686 assert_eq!(value["request"]["op"], "Download");
687 assert_eq!(value["request"]["guest_path"], "/tmp/data.bin");
688 assert!(serde_json::from_value::<GuestSessionRequest>(value).is_ok());
689 }
690
691 #[test]
692 fn filesystem_session_request_has_an_unambiguous_wire_discriminator() {
693 let request = GuestSessionRequest::Filesystem(FilesystemRequest {
694 op: FilesystemOp::Move,
695 path: "~/before".to_string(),
696 destination: Some("~/after".to_string()),
697 depth: 0,
698 user: Some("user".to_string()),
699 });
700
701 let value = serde_json::to_value(&request).unwrap();
702 assert_eq!(value["request_type"], "filesystem");
703 assert_eq!(value["request"]["op"], "Move");
704 assert_eq!(value["request"]["destination"], "~/after");
705 assert!(serde_json::from_value::<GuestSessionRequest>(value).is_ok());
706 }
707
708 #[test]
709 fn test_frame_exec_constants() {
710 assert_eq!(FRAME_EXEC_CHUNK, 0x01);
711 assert_eq!(FRAME_EXEC_EXIT, 0x02);
712 }
713}