mimobox_core/sandbox.rs
1use std::path::{Path, PathBuf};
2use std::sync::mpsc::Receiver;
3use std::time::Duration;
4
5use crate::seccomp::SeccompProfile;
6
7fn default_cpu_period_us() -> u64 {
8 100_000
9}
10
11/// Structured error code for programmatic error matching.
12///
13/// Each variant has a stable string representation via [`ErrorCode::as_str()`],
14/// suitable for cross-language transport and log indexing.
15///
16/// # Examples
17///
18/// ```
19/// use mimobox_core::ErrorCode;
20///
21/// assert_eq!(ErrorCode::CommandTimeout.as_str(), "command_timeout");
22/// assert_eq!(ErrorCode::HttpDeniedHost.as_str(), "http_denied_host");
23/// ```
24#[non_exhaustive]
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ErrorCode {
27 /// The command exceeds its timeout.
28 CommandTimeout,
29 /// The command exits with a non-zero status code.
30 CommandExit(i32),
31 /// The command is forcibly terminated by the host.
32 CommandKilled,
33 /// The target file does not exist.
34 FileNotFound,
35 /// The target file lacks the required access permission.
36 FilePermissionDenied,
37 /// The target file or transferred content exceeds the size limit.
38 FileTooLarge,
39 /// The target path is not a directory.
40 NotDirectory,
41 /// The HTTP proxy target host is not in the allowlist.
42 HttpDeniedHost,
43 /// The HTTP proxy request times out.
44 HttpTimeout,
45 /// The HTTP response body exceeds the allowed size.
46 HttpBodyTooLarge,
47 /// The HTTP proxy fails to establish a connection.
48 HttpConnectFail,
49 /// The HTTP proxy TLS handshake fails.
50 HttpTlsFail,
51 /// The HTTP request URL is invalid.
52 HttpInvalidUrl,
53 /// The sandbox is not ready to execute commands.
54 SandboxNotReady,
55 /// The sandbox has been destroyed and cannot be reused.
56 SandboxDestroyed,
57 /// The sandbox creation flow fails.
58 SandboxCreateFailed,
59 /// The provided configuration is invalid.
60 InvalidConfig,
61 /// The current platform or backend does not support this capability.
62 UnsupportedPlatform,
63}
64
65impl ErrorCode {
66 /// Returns the stable string error code for cross-language transport and log indexing.
67 pub fn as_str(&self) -> &'static str {
68 #[allow(unreachable_patterns)]
69 match self {
70 Self::CommandTimeout => "command_timeout",
71 Self::CommandExit(_) => "command_exit",
72 Self::CommandKilled => "command_killed",
73 Self::FileNotFound => "file_not_found",
74 Self::FilePermissionDenied => "file_permission_denied",
75 Self::FileTooLarge => "file_too_large",
76 Self::NotDirectory => "not_directory",
77 Self::HttpDeniedHost => "http_denied_host",
78 Self::HttpTimeout => "http_timeout",
79 Self::HttpBodyTooLarge => "http_body_too_large",
80 Self::HttpConnectFail => "http_connect_fail",
81 Self::HttpTlsFail => "http_tls_fail",
82 Self::HttpInvalidUrl => "http_invalid_url",
83 Self::SandboxNotReady => "sandbox_not_ready",
84 Self::SandboxDestroyed => "sandbox_destroyed",
85 Self::SandboxCreateFailed => "sandbox_create_failed",
86 Self::InvalidConfig => "invalid_config",
87 Self::UnsupportedPlatform => "unsupported_platform",
88 _ => "unknown_error",
89 }
90 }
91}
92
93/// 文件类型枚举,用于目录条目的类型区分。
94#[non_exhaustive]
95#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
96pub enum FileType {
97 /// 普通文件。
98 File,
99 /// 目录。
100 Dir,
101 /// 符号链接。
102 Symlink,
103 /// 其他类型(设备、管道等)。
104 Other,
105}
106
107/// 目录条目,表示 list_dir 返回的单个条目。
108#[non_exhaustive]
109#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
110pub struct DirEntry {
111 /// 文件名。
112 pub name: String,
113 /// 文件类型。
114 pub file_type: FileType,
115 /// 文件大小(字节)。
116 pub size: u64,
117 /// 是否为符号链接。
118 pub is_symlink: bool,
119}
120
121impl DirEntry {
122 /// 创建目录条目。
123 pub fn new(name: String, file_type: FileType, size: u64, is_symlink: bool) -> Self {
124 Self {
125 name,
126 file_type,
127 size,
128 is_symlink,
129 }
130 }
131}
132
133/// 文件元信息,stat 方法返回的类型。
134#[non_exhaustive]
135#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
136pub struct FileStat {
137 /// 文件路径。
138 pub path: String,
139 /// 是否为目录。
140 pub is_dir: bool,
141 /// 是否为普通文件。
142 pub is_file: bool,
143 /// 文件大小(字节)。
144 pub size: u64,
145 /// 文件权限模式(Unix mode)。
146 pub mode: u32,
147 /// 最后修改时间(毫秒时间戳),可能不可用。
148 pub modified_ms: Option<u64>,
149}
150
151impl FileStat {
152 /// 创建文件元信息。
153 pub fn new(
154 path: String,
155 is_dir: bool,
156 is_file: bool,
157 size: u64,
158 mode: u32,
159 modified_ms: Option<u64>,
160 ) -> Self {
161 Self {
162 path,
163 is_dir,
164 is_file,
165 size,
166 mode,
167 modified_ms,
168 }
169 }
170}
171
172/// Sandbox configuration shared across all backends.
173///
174/// This struct describes the minimum capability set used by all sandbox
175/// implementations, including filesystem permissions, network policy,
176/// resource limits, and controlled HTTP proxy whitelist.
177///
178/// # Examples
179///
180/// ```
181/// use mimobox_core::SandboxConfig;
182///
183/// let config = SandboxConfig::default();
184/// assert!(config.deny_network);
185/// assert_eq!(config.timeout_secs, Some(30));
186/// ```
187#[non_exhaustive]
188#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
189pub struct SandboxConfig {
190 /// Read-only path list.
191 pub fs_readonly: Vec<PathBuf>,
192 /// Read-write path list.
193 pub fs_readwrite: Vec<PathBuf>,
194 /// Whether direct network access from sandboxed processes is denied.
195 pub deny_network: bool,
196 /// Memory limit in MB, enforced through cgroups v2 or `setrlimit`.
197 pub memory_limit_mb: Option<u64>,
198 /// CPU 时间配额(微秒),配合 `cpu_period_us` 使用。
199 ///
200 /// 例如 quota=50000, period=100000 表示最多使用 50% CPU。
201 /// 设为 `None` 表示不限制。
202 #[serde(default)]
203 pub cpu_quota_us: Option<u64>,
204 /// CPU 周期(微秒),默认 100000(100ms)。
205 #[serde(default = "default_cpu_period_us")]
206 pub cpu_period_us: u64,
207 /// Timeout in seconds.
208 pub timeout_secs: Option<u64>,
209 /// Seccomp filter policy.
210 pub seccomp_profile: SeccompProfile,
211 /// Whether sandboxed processes may create child processes (`fork`/`clone`).
212 /// Defaults to `false`; set to `true` only for shells and other child-process workloads.
213 pub allow_fork: bool,
214 /// HTTP proxy domain allowlist, including wildcards such as `*.openai.com`.
215 /// These domains remain reachable through the controlled proxy even when `deny_network = true`.
216 pub allowed_http_domains: Vec<String>,
217}
218
219impl Default for SandboxConfig {
220 fn default() -> Self {
221 Self {
222 fs_readonly: vec![
223 "/usr".into(),
224 "/lib".into(),
225 "/lib64".into(),
226 "/bin".into(),
227 "/sbin".into(),
228 "/dev".into(),
229 "/proc".into(),
230 "/etc".into(),
231 ],
232 fs_readwrite: vec!["/tmp".into()],
233 deny_network: true,
234 memory_limit_mb: Some(512),
235 cpu_quota_us: None,
236 cpu_period_us: default_cpu_period_us(),
237 timeout_secs: Some(30),
238 seccomp_profile: SeccompProfile::Essential,
239 allow_fork: false,
240 allowed_http_domains: Vec::new(),
241 }
242 }
243}
244
245impl SandboxConfig {
246 /// Set the CPU time quota in microseconds.
247 pub fn cpu_quota(mut self, quota_us: u64) -> Self {
248 self.cpu_quota_us = Some(quota_us);
249 self
250 }
251
252 /// Set the CPU period in microseconds.
253 pub fn cpu_period(mut self, period_us: u64) -> Self {
254 self.cpu_period_us = period_us;
255 self
256 }
257}
258
259/// Result of a sandbox command execution.
260///
261/// Contains raw stdout/stderr bytes, exit code, wall-clock elapsed time,
262/// and a timeout flag.
263#[derive(Debug)]
264pub struct SandboxResult {
265 /// Captured standard output.
266 pub stdout: Vec<u8>,
267 /// Captured standard error output.
268 pub stderr: Vec<u8>,
269 /// Child process exit code; may be `None` when the process does not exit normally.
270 pub exit_code: Option<i32>,
271 /// Total elapsed time for this execution.
272 pub elapsed: Duration,
273 /// Whether the execution was terminated because of a timeout.
274 pub timed_out: bool,
275}
276
277/// Internal storage for sandbox snapshots.
278///
279/// This enum supports two snapshot storage modes:
280/// 1. Stores snapshot content directly as in-memory bytes.
281/// 2. Stores only the snapshot file path and size, with the actual data managed externally as a file.
282#[derive(Debug, Clone, PartialEq, Eq)]
283enum SnapshotInner {
284 /// In-memory snapshot bytes.
285 Bytes(Vec<u8>),
286 /// File-backed snapshot reference.
287 File {
288 /// Snapshot file path.
289 path: PathBuf,
290 /// Snapshot file size in bytes.
291 size: usize,
292 },
293}
294
295/// Opaque sandbox snapshot handle.
296///
297/// Carries a backend-generated snapshot without parsing the internal format.
298/// Supports two storage modes: in-memory bytes and file-backed references.
299///
300/// # Examples
301///
302/// ```
303/// use mimobox_core::SandboxSnapshot;
304///
305/// let snapshot = SandboxSnapshot::from_bytes(b"snapshot-data").unwrap();
306/// assert_eq!(snapshot.size(), 13);
307/// assert_eq!(snapshot.as_bytes().unwrap(), b"snapshot-data");
308/// ```
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct SandboxSnapshot {
311 inner: SnapshotInner,
312}
313
314impl SandboxSnapshot {
315 /// Creates a snapshot from raw bytes.
316 pub fn from_bytes(data: &[u8]) -> Result<Self, SandboxError> {
317 if data.is_empty() {
318 return Err(SandboxError::ExecutionFailed(
319 "snapshot data must not be empty".to_string(),
320 ));
321 }
322
323 Ok(Self {
324 inner: SnapshotInner::Bytes(data.to_vec()),
325 })
326 }
327
328 /// Creates a snapshot from owned bytes without an extra copy.
329 pub fn from_owned_bytes(data: Vec<u8>) -> Result<Self, SandboxError> {
330 if data.is_empty() {
331 return Err(SandboxError::ExecutionFailed(
332 "snapshot data must not be empty".to_string(),
333 ));
334 }
335
336 Ok(Self {
337 inner: SnapshotInner::Bytes(data),
338 })
339 }
340
341 /// Creates a snapshot reference from a snapshot file.
342 ///
343 /// This constructor records only the path and file size without reading file content into memory.
344 pub fn from_file(path: PathBuf) -> Result<Self, SandboxError> {
345 let metadata = std::fs::metadata(&path)?;
346 if !metadata.is_file() {
347 return Err(SandboxError::InvalidSnapshot);
348 }
349
350 let size = usize::try_from(metadata.len()).map_err(|_| SandboxError::InvalidSnapshot)?;
351 if size == 0 {
352 return Err(SandboxError::InvalidSnapshot);
353 }
354
355 Ok(Self {
356 inner: SnapshotInner::File { path, size },
357 })
358 }
359
360 /// Returns the memory file path.
361 ///
362 /// Returns the corresponding path for file-backed snapshots, or `None` otherwise.
363 pub fn memory_file_path(&self) -> Option<&Path> {
364 match &self.inner {
365 SnapshotInner::Bytes(_) => None,
366 SnapshotInner::File { path, .. } => Some(path.as_path()),
367 }
368 }
369
370 /// Returns the snapshot byte slice without unnecessary copying.
371 ///
372 /// This operation is supported only for in-memory snapshots; file-backed snapshots return an error.
373 pub fn as_bytes(&self) -> Result<&[u8], SandboxError> {
374 match &self.inner {
375 SnapshotInner::Bytes(data) => Ok(data.as_slice()),
376 SnapshotInner::File { .. } => Err(SandboxError::InvalidSnapshot),
377 }
378 }
379
380 /// Serializes the snapshot into a byte copy.
381 ///
382 /// File-backed snapshots read file content from disk again.
383 pub fn to_bytes(&self) -> Result<Vec<u8>, SandboxError> {
384 match &self.inner {
385 SnapshotInner::Bytes(data) => Ok(data.clone()),
386 SnapshotInner::File { path, .. } => std::fs::read(path).map_err(Into::into),
387 }
388 }
389
390 /// Consumes the snapshot and returns the underlying bytes without an extra copy.
391 ///
392 /// File-backed snapshots read file content from disk again.
393 pub fn into_bytes(self) -> Result<Vec<u8>, SandboxError> {
394 match self.inner {
395 SnapshotInner::Bytes(data) => Ok(data),
396 SnapshotInner::File { path, .. } => std::fs::read(path).map_err(Into::into),
397 }
398 }
399
400 /// Returns the snapshot size in bytes.
401 pub fn size(&self) -> usize {
402 match &self.inner {
403 SnapshotInner::Bytes(data) => data.len(),
404 SnapshotInner::File { size, .. } => *size,
405 }
406 }
407}
408
409/// PTY terminal dimensions.
410///
411/// # Examples
412///
413/// ```
414/// use mimobox_core::PtySize;
415///
416/// let size = PtySize::default();
417/// assert_eq!(size.cols, 80);
418/// assert_eq!(size.rows, 24);
419/// ```
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub struct PtySize {
422 /// Terminal column count.
423 pub cols: u16,
424 /// Terminal row count.
425 pub rows: u16,
426}
427
428impl Default for PtySize {
429 fn default() -> Self {
430 Self { cols: 80, rows: 24 }
431 }
432}
433
434/// PTY session event.
435///
436/// Events delivered via the PTY output channel.
437#[derive(Debug)]
438pub enum PtyEvent {
439 /// Terminal output data.
440 Output(Vec<u8>),
441 /// Process exit event.
442 Exit(i32),
443}
444
445/// PTY session configuration.
446///
447/// Specifies the command to run, terminal size, environment variables,
448/// working directory, and timeout for an interactive PTY session.
449#[derive(Debug, Clone)]
450pub struct PtyConfig {
451 /// Command and arguments executed when starting the PTY session.
452 pub command: Vec<String>,
453 /// Initial terminal size.
454 pub size: PtySize,
455 /// Additional environment variables injected into the session.
456 pub env: std::collections::HashMap<String, String>,
457 /// Session working directory.
458 pub cwd: Option<String>,
459 /// Session timeout.
460 pub timeout: Option<Duration>,
461}
462
463/// PTY session trait for interactive terminal control.
464///
465/// Backend implementations provide concrete types satisfying this trait.
466/// SDK users interact with `mimobox_sdk::PtySession` instead.
467pub trait PtySession {
468 /// Sends input to the terminal (`stdin`).
469 fn send_input(&mut self, data: &[u8]) -> Result<(), SandboxError>;
470 /// Resizes the terminal.
471 fn resize(&mut self, size: PtySize) -> Result<(), SandboxError>;
472 /// Returns the output event receiver.
473 fn output_rx(&self) -> &Receiver<PtyEvent>;
474 /// Terminates the session.
475 fn kill(&mut self) -> Result<(), SandboxError>;
476 /// Waits for the process to exit and returns the exit code.
477 fn wait(&mut self) -> Result<i32, SandboxError>;
478}
479
480/// Sandbox error type.
481///
482/// Low-level backend errors. The SDK maps these to `mimobox_sdk::SdkError`
483/// with structured [`ErrorCode`] values.
484#[non_exhaustive]
485#[derive(Debug, thiserror::Error)]
486pub enum SandboxError {
487 /// The current platform does not support this sandbox implementation.
488 #[error("sandbox backend not supported on current platform")]
489 Unsupported,
490
491 /// The current backend does not support the requested operation.
492 #[error("operation not supported: {0}")]
493 UnsupportedOperation(String),
494
495 /// Namespace initialization fails.
496 #[error("namespace creation failed: {0}")]
497 NamespaceFailed(String),
498
499 /// The `pivot_root` call fails.
500 #[error("pivot_root failed: {0}")]
501 PivotRootFailed(String),
502
503 /// Filesystem mounting fails.
504 #[error("mount failed: {0}")]
505 MountFailed(String),
506
507 /// Landlock rule enforcement fails.
508 #[error("Landlock rule enforcement failed: {0}")]
509 LandlockFailed(String),
510
511 /// Seccomp rule enforcement fails.
512 #[error("Seccomp filter enforcement failed: {0}")]
513 SeccompFailed(String),
514
515 /// Command execution or protocol handling fails.
516 #[error("command execution failed: {0}")]
517 ExecutionFailed(String),
518
519 /// The snapshot content or access mode is invalid.
520 #[error("invalid sandbox snapshot")]
521 InvalidSnapshot,
522
523 /// The child process execution times out.
524 #[error("child process timed out")]
525 Timeout,
526
527 /// Pipe I/O fails.
528 #[error("pipe I/O error: {0}")]
529 PipeError(String),
530
531 /// A system call fails.
532 #[error("syscall error: {0}")]
533 Syscall(String),
534
535 /// A standard library I/O error occurs.
536 #[error("I/O error: {0}")]
537 Io(#[from] std::io::Error),
538}
539
540/// Sandbox lifecycle trait.
541///
542/// Each backend implements this trait to provide unified creation, execution,
543/// file transfer, snapshot, and destruction capabilities.
544///
545/// Most users should use `mimobox_sdk::Sandbox` instead of implementing
546/// this trait directly.
547///
548/// # Examples
549///
550/// ```rust,ignore
551/// use mimobox_core::{Sandbox, SandboxConfig};
552/// use mimobox_os::LinuxSandbox;
553///
554/// let mut sandbox = LinuxSandbox::new(SandboxConfig::default())?;
555/// let result = sandbox.execute(&["/bin/echo".into(), "hello".into()])?;
556/// assert_eq!(result.exit_code, Some(0));
557/// sandbox.destroy()?;
558/// ```
559pub trait Sandbox {
560 /// Creates a new sandbox instance with the given configuration.
561 fn new(config: SandboxConfig) -> Result<Self, SandboxError>
562 where
563 Self: Sized;
564
565 /// Executes a command inside the sandbox and waits for completion.
566 ///
567 /// # Examples
568 ///
569 /// ```rust,ignore
570 /// use mimobox_core::Sandbox;
571 /// use mimobox_os::LinuxSandbox;
572 ///
573 /// let mut sandbox = LinuxSandbox::new(Default::default())?;
574 /// let result = sandbox.execute(&["/bin/echo".into(), "hello".into()])?;
575 /// assert_eq!(result.exit_code, Some(0));
576 /// # Ok::<(), Box<dyn std::error::Error>>(())
577 /// ```
578 fn execute(&mut self, cmd: &[String]) -> Result<SandboxResult, SandboxError>;
579
580 /// Creates an interactive PTY session.
581 ///
582 /// # Examples
583 ///
584 /// ```rust,ignore
585 /// use mimobox_core::{PtyConfig, PtySize, Sandbox};
586 /// use mimobox_os::LinuxSandbox;
587 /// use std::collections::HashMap;
588 /// use std::time::Duration;
589 ///
590 /// let mut sandbox = LinuxSandbox::new(Default::default())?;
591 /// let mut session = sandbox.create_pty(PtyConfig {
592 /// command: vec!["/bin/sh".into()],
593 /// size: PtySize { cols: 80, rows: 24 },
594 /// env: HashMap::new(),
595 /// cwd: None,
596 /// timeout: Some(Duration::from_secs(10)),
597 /// })?;
598 /// session.send_input(b"echo hello\n")?;
599 /// # Ok::<(), Box<dyn std::error::Error>>(())
600 /// ```
601 fn create_pty(&mut self, config: PtyConfig) -> Result<Box<dyn PtySession>, SandboxError> {
602 let _ = config;
603 Err(SandboxError::UnsupportedOperation(
604 "PTY sessions not supported by current backend".to_string(),
605 ))
606 }
607
608 /// Reads file content from inside the sandbox.
609 fn read_file(&mut self, path: &str) -> Result<Vec<u8>, SandboxError> {
610 let _ = path;
611 Err(SandboxError::ExecutionFailed(
612 "file reading not supported by current backend".into(),
613 ))
614 }
615
616 /// Writes file content inside the sandbox.
617 fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), SandboxError> {
618 let _ = path;
619 let _ = data;
620 Err(SandboxError::ExecutionFailed(
621 "file writing not supported by current backend".into(),
622 ))
623 }
624
625 /// 列出指定路径下的目录条目。
626 fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>, SandboxError> {
627 let _ = path;
628 Err(SandboxError::ExecutionFailed(
629 "list_dir not supported by current backend".into(),
630 ))
631 }
632
633 /// 检查指定路径的文件是否存在。
634 fn file_exists(&mut self, path: &str) -> Result<bool, SandboxError> {
635 let _ = path;
636 Err(SandboxError::UnsupportedOperation(
637 "file_exists not supported by current backend".to_string(),
638 ))
639 }
640
641 /// 删除指定路径的文件或空目录。
642 ///
643 /// 注意:不支持递归删除(安全考虑)。
644 fn remove_file(&mut self, path: &str) -> Result<(), SandboxError> {
645 let _ = path;
646 Err(SandboxError::UnsupportedOperation(
647 "remove_file not supported by current backend".to_string(),
648 ))
649 }
650
651 /// 重命名/移动文件。
652 fn rename(&mut self, from: &str, to: &str) -> Result<(), SandboxError> {
653 let _ = from;
654 let _ = to;
655 Err(SandboxError::UnsupportedOperation(
656 "rename not supported by current backend".to_string(),
657 ))
658 }
659
660 /// 返回文件的元信息。
661 fn stat(&mut self, path: &str) -> Result<FileStat, SandboxError> {
662 let _ = path;
663 Err(SandboxError::UnsupportedOperation(
664 "stat not supported by current backend".to_string(),
665 ))
666 }
667
668 /// Exports a snapshot of the current sandbox state.
669 ///
670 /// # Examples
671 ///
672 /// ```rust,ignore
673 /// use mimobox_core::Sandbox;
674 /// use mimobox_vm::MicrovmSandbox;
675 /// use mimobox_vm::MicrovmConfig;
676 ///
677 /// let mut sandbox = MicrovmSandbox::new(MicrovmConfig::default())?;
678 /// let snapshot = sandbox.snapshot()?;
679 /// assert!(!snapshot.to_bytes()?.is_empty());
680 /// # Ok::<(), Box<dyn std::error::Error>>(())
681 /// ```
682 fn snapshot(&mut self) -> Result<SandboxSnapshot, SandboxError> {
683 Err(SandboxError::UnsupportedOperation(
684 "snapshot not supported by current backend".to_string(),
685 ))
686 }
687
688 /// Forks an independent copy from the current sandbox.
689 ///
690 /// Returns `UnsupportedOperation` by default; only the microVM backend supports this operation.
691 fn fork(&mut self) -> Result<Self, SandboxError>
692 where
693 Self: Sized,
694 {
695 Err(SandboxError::UnsupportedOperation(
696 "fork not supported by current backend".to_string(),
697 ))
698 }
699
700 /// Destroys the sandbox and releases underlying resources.
701 fn destroy(self) -> Result<(), SandboxError>;
702}
703
704#[cfg(test)]
705mod tests {
706 use std::fs;
707 use std::time::{SystemTime, UNIX_EPOCH};
708
709 use super::{PtySize, SandboxError, SandboxSnapshot};
710
711 #[test]
712 fn pty_size_default_is_80x24() {
713 assert_eq!(PtySize::default(), PtySize { cols: 80, rows: 24 });
714 }
715
716 #[test]
717 fn sandbox_snapshot_round_trip_preserves_bytes() {
718 let original = vec![0x4d, 0x4d, 0x42, 0x58, 0x01, 0x02];
719
720 let snapshot =
721 SandboxSnapshot::from_owned_bytes(original.clone()).expect("快照创建必须成功");
722
723 assert_eq!(
724 snapshot.as_bytes().expect("内存快照必须可读取字节"),
725 original.as_slice()
726 );
727 assert_eq!(
728 snapshot.to_bytes().expect("内存快照必须可复制字节"),
729 original
730 );
731 assert_eq!(snapshot.size(), 6);
732 }
733
734 #[test]
735 fn sandbox_snapshot_rejects_empty_bytes() {
736 let error = SandboxSnapshot::from_bytes(&[]).expect_err("空快照必须被拒绝");
737
738 assert!(error.to_string().contains("must not be empty"));
739 }
740
741 #[test]
742 fn sandbox_snapshot_file_mode_exposes_metadata_only() {
743 let unique = SystemTime::now()
744 .duration_since(UNIX_EPOCH)
745 .expect("系统时间必须晚于 UNIX_EPOCH")
746 .as_nanos();
747 let path = std::env::temp_dir().join(format!(
748 "mimobox-sandbox-snapshot-{}-{}.bin",
749 std::process::id(),
750 unique
751 ));
752
753 fs::write(&path, b"file-backed-snapshot").expect("测试快照文件写入必须成功");
754
755 let snapshot = SandboxSnapshot::from_file(path.clone()).expect("文件快照创建必须成功");
756
757 assert_eq!(snapshot.memory_file_path(), Some(path.as_path()));
758 assert_eq!(snapshot.size(), b"file-backed-snapshot".len());
759 assert!(matches!(
760 snapshot.as_bytes().expect_err("文件快照不应暴露内存字节"),
761 SandboxError::InvalidSnapshot
762 ));
763 assert_eq!(
764 snapshot.to_bytes().expect("文件快照必须可读回字节"),
765 b"file-backed-snapshot"
766 );
767 assert_eq!(
768 snapshot
769 .clone()
770 .into_bytes()
771 .expect("文件快照必须可转移为字节"),
772 b"file-backed-snapshot"
773 );
774
775 fs::remove_file(path).expect("测试快照文件清理必须成功");
776 }
777}