Skip to main content

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}