microsandbox_core/vm/
vm.rs

1use std::{ffi::CString, net::Ipv4Addr, path::PathBuf, ptr};
2
3use getset::Getters;
4use ipnetwork::Ipv4Network;
5use microsandbox_utils::SupportedPathType;
6use typed_path::Utf8UnixPathBuf;
7
8use crate::{
9    config::{EnvPair, NetworkScope, PathPair, PortPair},
10    utils, InvalidMicroVMConfigError, MicrosandboxError, MicrosandboxResult,
11};
12
13use super::{ffi, LinuxRlimit, MicroVmBuilder, MicroVmConfigBuilder};
14
15//--------------------------------------------------------------------------------------------------
16// Constants
17//--------------------------------------------------------------------------------------------------
18
19/// The prefix used for virtio-fs tags when mounting shared directories
20pub const VIRTIOFS_TAG_PREFIX: &str = "virtiofs";
21
22//--------------------------------------------------------------------------------------------------
23// Types
24//--------------------------------------------------------------------------------------------------
25
26/// A lightweight Linux virtual machine.
27///
28/// MicroVm provides a secure, isolated environment for running applications with their own
29/// filesystem, network, and resource constraints.
30///
31/// ## Examples
32///
33/// ```no_run
34/// use microsandbox_core::vm::{MicroVm, Rootfs};
35/// use tempfile::TempDir;
36///
37/// # fn main() -> anyhow::Result<()> {
38/// let temp_dir = TempDir::new()?;
39/// let vm = MicroVm::builder()
40///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
41///     .ram_mib(1024)
42///     .exec_path("/bin/echo")
43///     .args(["Hello, World!"])
44///     .build()?;
45///
46/// // Start the MicroVm
47/// vm.start()?;  // This would actually run the VM
48/// # Ok(())
49/// # }
50/// ```
51#[derive(Debug, Getters)]
52pub struct MicroVm {
53    /// The context ID for the MicroVm configuration.
54    ctx_id: u32,
55
56    /// The configuration for the MicroVm.
57    #[get = "pub with_prefix"]
58    config: MicroVmConfig,
59}
60
61/// The type of rootfs to use for the MicroVm.
62///
63/// This enum represents the different types of rootfss that can be used for the MicroVm.
64///
65/// ## Variants
66///
67/// * `Native(PathBuf)` - A native rootfs using a single path.
68/// * `Overlayfs(Vec<PathBuf>)` - An overlayfs rootfs using a list of paths.
69///
70/// ## Examples
71///
72/// ```rust
73/// use microsandbox_core::vm::Rootfs;
74/// use std::path::PathBuf;
75///
76/// let native_root = Rootfs::Native(PathBuf::from("/path/to/root"));
77/// let overlayfs_root = Rootfs::Overlayfs(vec![PathBuf::from("/path/to/root1"), PathBuf::from("/path/to/root2")]);
78/// ```
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Rootfs {
81    /// A rootfs using underlying native filesystem.
82    Native(PathBuf),
83
84    /// An overlayfs rootfs using a list of paths.
85    Overlayfs(Vec<PathBuf>),
86}
87
88/// Configuration for a MicroVm instance.
89///
90/// This struct holds all the settings needed to create and run a MicroVm,
91/// including system resources, filesystem configuration, networking, and
92/// process execution details.
93///
94/// Rather than creating this directly, use `MicroVmConfigBuilder` or
95/// `MicroVmBuilder` for a more ergonomic interface.
96///
97/// ## Examples
98///
99/// ```rust
100/// use microsandbox_core::vm::{MicroVm, MicroVmConfig, Rootfs};
101/// use tempfile::TempDir;
102///
103/// # fn main() -> anyhow::Result<()> {
104/// let temp_dir = TempDir::new()?;
105/// let config = MicroVmConfig::builder()
106///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
107///     .memory_mib(1024)
108///     .exec_path("/bin/echo")
109///     .build();
110///
111/// let vm = MicroVm::from_config(config)?;
112/// # Ok(())
113/// # }
114/// ```
115#[derive(Debug)]
116pub struct MicroVmConfig {
117    /// The log level to use for the MicroVm.
118    pub log_level: LogLevel,
119
120    /// The rootfs for the MicroVm.
121    pub rootfs: Rootfs,
122
123    /// The number of vCPUs to use for the MicroVm.
124    pub num_vcpus: u8,
125
126    /// The amount of memory in MiB to use for the MicroVm.
127    pub memory_mib: u32,
128
129    /// The directories to mount in the MicroVm using virtio-fs.
130    /// Each PathPair represents a host:guest path mapping.
131    pub mapped_dirs: Vec<PathPair>,
132
133    /// The port map to use for the MicroVm.
134    pub port_map: Vec<PortPair>,
135
136    /// The network scope to use for the MicroVm.
137    pub scope: NetworkScope,
138
139    /// The IP address to use for the MicroVm.
140    pub ip: Option<Ipv4Addr>,
141
142    /// The subnet to use for the MicroVm.
143    pub subnet: Option<Ipv4Network>,
144
145    /// The resource limits to use for the MicroVm.
146    pub rlimits: Vec<LinuxRlimit>,
147
148    /// The working directory path to use for the MicroVm.
149    pub workdir_path: Option<Utf8UnixPathBuf>,
150
151    /// The executable path to use for the MicroVm.
152    pub exec_path: Utf8UnixPathBuf,
153
154    /// The arguments to pass to the executable.
155    pub args: Vec<String>,
156
157    /// The environment variables to set for the executable.
158    pub env: Vec<EnvPair>,
159
160    /// The console output path to use for the MicroVm.
161    pub console_output: Option<Utf8UnixPathBuf>,
162}
163
164/// The log level to use for the MicroVm.
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166#[repr(u8)]
167pub enum LogLevel {
168    /// No logging.
169    #[default]
170    Off = 0,
171
172    /// Error messages.
173    Error = 1,
174
175    /// Warning messages.
176    Warn = 2,
177
178    /// Informational messages.
179    Info = 3,
180
181    /// Debug messages.
182    Debug = 4,
183
184    /// Trace messages.
185    Trace = 5,
186}
187
188//--------------------------------------------------------------------------------------------------
189// Methods
190//--------------------------------------------------------------------------------------------------
191
192impl MicroVm {
193    /// Creates a new MicroVm from the given configuration.
194    ///
195    /// This is a low-level constructor - prefer using `MicroVm::builder()`
196    /// for a more ergonomic interface.
197    ///
198    /// ## Errors
199    /// Returns an error if:
200    /// - The configuration is invalid
201    /// - Required resources cannot be allocated
202    /// - The system lacks required capabilities
203    pub fn from_config(config: MicroVmConfig) -> MicrosandboxResult<Self> {
204        let ctx_id = Self::create_ctx();
205
206        config.validate()?;
207
208        Self::apply_config(ctx_id, &config);
209
210        Ok(Self { ctx_id, config })
211    }
212
213    /// Creates a builder for configuring a new MicroVm instance.
214    ///
215    /// This is the recommended way to create a new MicroVm.
216    ///
217    /// ## Examples
218    ///
219    /// ```rust
220    /// use microsandbox_core::vm::{MicroVm, Rootfs};
221    /// use tempfile::TempDir;
222    ///
223    /// # fn main() -> anyhow::Result<()> {
224    /// let temp_dir = TempDir::new()?;
225    /// let vm = MicroVm::builder()
226    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
227    ///     .ram_mib(1024)
228    ///     .exec_path("/bin/echo")
229    ///     .build()?;
230    /// # Ok(())
231    /// # }
232    /// ```
233    pub fn builder() -> MicroVmBuilder<(), ()> {
234        MicroVmBuilder::default()
235    }
236
237    /// Starts the MicroVm and waits for it to complete.
238    ///
239    /// This function will block until the MicroVm exits. The exit status
240    /// of the guest process is returned.
241    ///
242    /// ## Examples
243    ///
244    /// ```rust,no_run
245    /// use microsandbox_core::vm::{MicroVm, Rootfs};
246    /// use tempfile::TempDir;
247    ///
248    /// # fn main() -> anyhow::Result<()> {
249    /// let temp_dir = TempDir::new()?;
250    /// let vm = MicroVm::builder()
251    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
252    ///     .ram_mib(1024)
253    ///     .exec_path("/usr/bin/python3")
254    ///     .args(["-c", "print('Hello from MicroVm!')"])
255    ///     .build()?;
256    ///
257    /// // let status = vm.start()?;
258    /// // assert_eq!(status, 0);  // Process exited successfully
259    /// # Ok(())
260    /// # }
261    /// ```
262    ///
263    /// ## Notes
264    /// - This function takes control of stdin/stdout
265    /// - The MicroVm is automatically cleaned up when this returns
266    /// - A non-zero status indicates the guest process failed
267    pub fn start(&self) -> MicrosandboxResult<i32> {
268        let ctx_id = self.ctx_id;
269        let status = unsafe { ffi::krun_start_enter(ctx_id) };
270        if status < 0 {
271            tracing::error!("failed to start microvm: {}", status);
272            return Err(MicrosandboxError::StartVmFailed(status));
273        }
274        tracing::info!("microvm exited with status: {}", status);
275        Ok(status)
276    }
277
278    /// Creates a new MicroVm context.
279    fn create_ctx() -> u32 {
280        let ctx_id = unsafe { ffi::krun_create_ctx() };
281        assert!(ctx_id >= 0, "failed to create microvm context: {}", ctx_id);
282        ctx_id as u32
283    }
284
285    /// Applies the configuration to the MicroVm context.
286    ///
287    /// This method configures all aspects of the MicroVm including:
288    /// - Basic VM settings (vCPUs, RAM)
289    /// - Root filesystem
290    /// - Directory mappings via virtio-fs
291    /// - Port mappings
292    /// - Resource limits
293    /// - Working directory
294    /// - Executable and arguments
295    /// - Environment variables
296    /// - Console output
297    /// - Network settings
298    ///
299    /// ## Arguments
300    /// * `ctx_id` - The MicroVm context ID to configure
301    /// * `config` - The configuration to apply
302    ///
303    /// ## Panics
304    /// Panics if:
305    /// - Any libkrun API call fails
306    /// - Cannot update the rootfs fstab file
307    fn apply_config(ctx_id: u32, config: &MicroVmConfig) {
308        // Set log level
309        unsafe {
310            let status = ffi::krun_set_log_level(config.log_level as u32);
311            assert!(status >= 0, "failed to set log level: {}", status);
312        }
313
314        // Set basic VM configuration
315        unsafe {
316            let status = ffi::krun_set_vm_config(ctx_id, config.num_vcpus, config.memory_mib);
317            assert!(status >= 0, "failed to set VM config: {}", status);
318        }
319
320        // Set rootfs.
321        match &config.rootfs {
322            Rootfs::Native(path) => {
323                let c_path = CString::new(path.to_str().unwrap().as_bytes()).unwrap();
324                unsafe {
325                    let status = ffi::krun_set_root(ctx_id, c_path.as_ptr());
326                    assert!(status >= 0, "failed to set rootfs: {}", status);
327                }
328            }
329            Rootfs::Overlayfs(paths) => {
330                tracing::debug!("setting overlayfs rootfs: {:?}", paths);
331                let c_paths: Vec<_> = paths
332                    .iter()
333                    .map(|p| CString::new(p.to_str().unwrap().as_bytes()).unwrap())
334                    .collect();
335                let c_paths_ptrs = utils::to_null_terminated_c_array(&c_paths);
336                unsafe {
337                    let status = ffi::krun_set_overlayfs_root(ctx_id, c_paths_ptrs.as_ptr());
338                    assert!(status >= 0, "failed to set rootfs: {}", status);
339                }
340            }
341        }
342
343        tracing::debug!("applying config: {:#?}", config);
344
345        // Add mapped directories using virtio-fs
346        let mapped_dirs = &config.mapped_dirs;
347        for (idx, dir) in mapped_dirs.iter().enumerate() {
348            let tag = CString::new(format!("{}_{}", VIRTIOFS_TAG_PREFIX, idx)).unwrap();
349            tracing::debug!("adding virtiofs mount for {}", tag.to_string_lossy());
350
351            // Canonicalize the host path
352            let host_path_buf = PathBuf::from(dir.get_host().as_str());
353            let canonical_host_path = match host_path_buf.canonicalize() {
354                Ok(path) => path,
355                Err(e) => {
356                    tracing::error!("failed to canonicalize host path: {}", e);
357                    panic!("failed to canonicalize host path: {}", e);
358                }
359            };
360
361            let host_path = CString::new(canonical_host_path.to_string_lossy().as_bytes()).unwrap();
362            tracing::debug!("canonical host path: {}", host_path.to_string_lossy());
363
364            unsafe {
365                let status = ffi::krun_add_virtiofs(ctx_id, tag.as_ptr(), host_path.as_ptr());
366                assert!(status >= 0, "failed to add mapped directory: {}", status);
367            }
368        }
369
370        // Set port map
371        let c_port_map: Vec<_> = config
372            .port_map
373            .iter()
374            .map(|p| CString::new(p.to_string()).unwrap())
375            .collect();
376        let c_port_map_ptrs = utils::to_null_terminated_c_array(&c_port_map);
377
378        unsafe {
379            let status = ffi::krun_set_port_map(ctx_id, c_port_map_ptrs.as_ptr());
380            assert!(status >= 0, "failed to set port map: {}", status);
381        }
382
383        // Set network scope
384        unsafe {
385            let status =
386                ffi::krun_set_tsi_scope(ctx_id, ptr::null(), ptr::null(), config.scope as u8);
387            assert!(status >= 0, "failed to set network scope: {}", status);
388        }
389
390        // Set resource limits
391        if !config.rlimits.is_empty() {
392            let c_rlimits: Vec<_> = config
393                .rlimits
394                .iter()
395                .map(|s| CString::new(s.to_string()).unwrap())
396                .collect();
397            let c_rlimits_ptrs = utils::to_null_terminated_c_array(&c_rlimits);
398            unsafe {
399                let status = ffi::krun_set_rlimits(ctx_id, c_rlimits_ptrs.as_ptr());
400                assert!(status >= 0, "failed to set resource limits: {}", status);
401            }
402        }
403
404        // Set working directory
405        if let Some(workdir) = &config.workdir_path {
406            let c_workdir = CString::new(workdir.to_string().as_bytes()).unwrap();
407            unsafe {
408                let status = ffi::krun_set_workdir(ctx_id, c_workdir.as_ptr());
409                assert!(status >= 0, "Failed to set working directory: {}", status);
410            }
411        }
412
413        // Set executable path, arguments, and environment variables
414        let c_exec_path = CString::new(config.exec_path.to_string().as_bytes()).unwrap();
415
416        let c_argv: Vec<_> = config
417            .args
418            .iter()
419            .map(|s| CString::new(s.as_str()).unwrap())
420            .collect();
421        let c_argv_ptrs = utils::to_null_terminated_c_array(&c_argv);
422
423        let c_env: Vec<_> = config
424            .env
425            .iter()
426            .map(|s| CString::new(s.to_string()).unwrap())
427            .collect();
428        let c_env_ptrs = utils::to_null_terminated_c_array(&c_env);
429
430        unsafe {
431            let status = ffi::krun_set_exec(
432                ctx_id,
433                c_exec_path.as_ptr(),
434                c_argv_ptrs.as_ptr(),
435                c_env_ptrs.as_ptr(),
436            );
437            assert!(
438                status >= 0,
439                "Failed to set executable configuration: {}",
440                status
441            );
442        }
443
444        // Set console output
445        if let Some(console_output) = &config.console_output {
446            let c_console_output = CString::new(console_output.to_string().as_bytes()).unwrap();
447            unsafe {
448                let status = ffi::krun_set_console_output(ctx_id, c_console_output.as_ptr());
449                assert!(status >= 0, "Failed to set console output: {}", status);
450            }
451        }
452    }
453}
454
455impl MicroVmConfig {
456    /// Creates a builder for configuring a new MicroVm configuration.
457    ///
458    /// This is the recommended way to create a new MicroVmConfig instance. The builder pattern
459    /// provides a more ergonomic interface and ensures all required fields are set.
460    ///
461    /// ## Examples
462    ///
463    /// ```rust
464    /// use microsandbox_core::vm::{MicroVmConfig, Rootfs};
465    /// use tempfile::TempDir;
466    ///
467    /// # fn main() -> anyhow::Result<()> {
468    /// let temp_dir = TempDir::new()?;
469    /// let config = MicroVmConfig::builder()
470    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
471    ///     .memory_mib(1024)
472    ///     .exec_path("/bin/echo")
473    ///     .build();
474    /// # Ok(())
475    /// # }
476    /// ```
477    pub fn builder() -> MicroVmConfigBuilder<(), ()> {
478        MicroVmConfigBuilder::default()
479    }
480
481    /// Validates that guest paths are not subsets of each other.
482    ///
483    /// For example, these paths would conflict:
484    /// - /app and /app/data
485    /// - /var/log and /var
486    /// - /data and /data
487    ///
488    /// ## Arguments
489    /// * `mapped_dirs` - The mapped directories to validate
490    ///
491    /// ## Returns
492    /// - Ok(()) if no paths are subsets of each other
493    /// - Err with details about conflicting paths
494    fn validate_guest_paths(mapped_dirs: &[PathPair]) -> MicrosandboxResult<()> {
495        // Early return if we have 0 or 1 paths - no conflicts possible
496        if mapped_dirs.len() <= 1 {
497            return Ok(());
498        }
499
500        // Pre-normalize all paths once to avoid repeated normalization
501        let normalized_paths: Vec<_> = mapped_dirs
502            .iter()
503            .map(|dir| {
504                microsandbox_utils::normalize_path(
505                    dir.get_guest().as_str(),
506                    SupportedPathType::Absolute,
507                )
508                .map_err(Into::into)
509            })
510            .collect::<MicrosandboxResult<Vec<_>>>()?;
511
512        // Compare each path with every other path only once
513        // Using windows of size 2 would miss some comparisons since we need to check both directions
514        for i in 0..normalized_paths.len() {
515            let path1 = &normalized_paths[i];
516
517            // Only need to check paths after this one since previous comparisons were already done
518            for path2 in &normalized_paths[i + 1..] {
519                // Check both directions for prefix relationship
520                if utils::paths_overlap(path1, path2) {
521                    return Err(MicrosandboxError::InvalidMicroVMConfig(
522                        InvalidMicroVMConfigError::ConflictingGuestPaths(
523                            path1.to_string(),
524                            path2.to_string(),
525                        ),
526                    ));
527                }
528            }
529        }
530
531        Ok(())
532    }
533
534    /// Validates the MicroVm configuration.
535    ///
536    /// Performs a series of checks to ensure the configuration is valid:
537    /// - Verifies the root path exists and is accessible
538    /// - Verifies all host paths in mapped_dirs exist and are accessible
539    /// - Ensures number of vCPUs is non-zero
540    /// - Ensures memory allocation is non-zero
541    /// - Validates executable path and arguments contain only printable ASCII characters
542    /// - Validates guest paths don't overlap or conflict with each other
543    ///
544    /// ## Returns
545    /// - `Ok(())` if the configuration is valid
546    /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` with details about what failed
547    ///
548    /// ## Examples
549    /// ```rust
550    /// use microsandbox_core::vm::{MicroVmConfig, Rootfs};
551    /// use tempfile::TempDir;
552    ///
553    /// # fn main() -> anyhow::Result<()> {
554    /// let temp_dir = TempDir::new()?;
555    /// let config = MicroVmConfig::builder()
556    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
557    ///     .memory_mib(1024)
558    ///     .exec_path("/bin/echo")
559    ///     .build();
560    ///
561    /// assert!(config.validate().is_ok());
562    /// # Ok(())
563    /// # }
564    /// ```
565    pub fn validate(&self) -> MicrosandboxResult<()> {
566        // Check that paths specified in rootfs exist
567        match &self.rootfs {
568            Rootfs::Native(path) => {
569                if !path.exists() {
570                    return Err(MicrosandboxError::InvalidMicroVMConfig(
571                        InvalidMicroVMConfigError::RootPathDoesNotExist(
572                            path.to_str().unwrap().into(),
573                        ),
574                    ));
575                }
576            }
577            Rootfs::Overlayfs(paths) => {
578                for path in paths {
579                    if !path.exists() {
580                        return Err(MicrosandboxError::InvalidMicroVMConfig(
581                            InvalidMicroVMConfigError::RootPathDoesNotExist(
582                                path.to_str().unwrap().into(),
583                            ),
584                        ));
585                    }
586                }
587            }
588        }
589
590        // Check all host paths in mapped_dirs exist
591        for dir in &self.mapped_dirs {
592            let host_path = PathBuf::from(dir.get_host().as_str());
593            if !host_path.exists() {
594                return Err(MicrosandboxError::InvalidMicroVMConfig(
595                    InvalidMicroVMConfigError::HostPathDoesNotExist(
596                        host_path.to_str().unwrap().into(),
597                    ),
598                ));
599            }
600        }
601
602        if self.num_vcpus == 0 {
603            return Err(MicrosandboxError::InvalidMicroVMConfig(
604                InvalidMicroVMConfigError::NumVCPUsIsZero,
605            ));
606        }
607
608        // Validate memory_mib is not zero
609        if self.memory_mib == 0 {
610            return Err(MicrosandboxError::InvalidMicroVMConfig(
611                InvalidMicroVMConfigError::MemoryIsZero,
612            ));
613        }
614
615        Self::validate_command_line(self.exec_path.as_ref())?;
616
617        for arg in &self.args {
618            Self::validate_command_line(arg)?;
619        }
620
621        // Validate guest paths are not subsets of each other
622        Self::validate_guest_paths(&self.mapped_dirs)?;
623
624        Ok(())
625    }
626
627    /// Validates that a command line string contains only allowed characters.
628    ///
629    /// Command line strings (executable paths and arguments) must contain only printable ASCII
630    /// characters in the range from space (0x20) to tilde (0x7E). This excludes:
631    /// - Control characters (newlines, tabs, etc.)
632    /// - Non-ASCII Unicode characters
633    /// - Null bytes
634    ///
635    /// ## Arguments
636    /// * `s` - The string to validate
637    ///
638    /// ## Returns
639    /// - `Ok(())` if the string contains only valid characters
640    /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` if invalid characters are found
641    ///
642    /// ## Examples
643    /// ```rust
644    /// use microsandbox_core::vm::MicroVmConfig;
645    ///
646    /// // Valid strings
647    /// assert!(MicroVmConfig::validate_command_line("/bin/echo").is_ok());
648    /// assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok());
649    ///
650    /// // Invalid strings
651    /// assert!(MicroVmConfig::validate_command_line("/bin/echo\n").is_err());  // newline
652    /// assert!(MicroVmConfig::validate_command_line("hello🌎").is_err());      // emoji
653    /// ```
654    pub fn validate_command_line(s: &str) -> MicrosandboxResult<()> {
655        fn valid_char(c: char) -> bool {
656            matches!(c, ' '..='~')
657        }
658
659        if s.chars().all(valid_char) {
660            Ok(())
661        } else {
662            Err(MicrosandboxError::InvalidMicroVMConfig(
663                InvalidMicroVMConfigError::InvalidCommandLineString(s.to_string()),
664            ))
665        }
666    }
667}
668
669//--------------------------------------------------------------------------------------------------
670// Trait Implementations
671//--------------------------------------------------------------------------------------------------
672
673impl Drop for MicroVm {
674    fn drop(&mut self) {
675        unsafe { ffi::krun_free_ctx(self.ctx_id) };
676    }
677}
678
679impl TryFrom<u8> for LogLevel {
680    type Error = MicrosandboxError;
681
682    fn try_from(value: u8) -> Result<Self, MicrosandboxError> {
683        match value {
684            0 => Ok(LogLevel::Off),
685            1 => Ok(LogLevel::Error),
686            2 => Ok(LogLevel::Warn),
687            3 => Ok(LogLevel::Info),
688            4 => Ok(LogLevel::Debug),
689            5 => Ok(LogLevel::Trace),
690            _ => Err(MicrosandboxError::InvalidLogLevel(value)),
691        }
692    }
693}
694
695//--------------------------------------------------------------------------------------------------
696// Tests
697//--------------------------------------------------------------------------------------------------
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use microsandbox_utils::DEFAULT_NUM_VCPUS;
703    use std::path::PathBuf;
704    use tempfile::TempDir;
705
706    #[test]
707    fn test_microvm_config_builder() {
708        let config = MicroVmConfig::builder()
709            .log_level(LogLevel::Info)
710            .rootfs(Rootfs::Native(PathBuf::from("/tmp")))
711            .memory_mib(512)
712            .exec_path("/bin/echo")
713            .build();
714
715        assert!(config.log_level == LogLevel::Info);
716        assert_eq!(config.rootfs, Rootfs::Native(PathBuf::from("/tmp")));
717        assert_eq!(config.memory_mib, 512);
718        assert_eq!(config.num_vcpus, DEFAULT_NUM_VCPUS);
719    }
720
721    #[test]
722    fn test_microvm_config_validation_success() {
723        let temp_dir = TempDir::new().unwrap();
724        let config = MicroVmConfig::builder()
725            .log_level(LogLevel::Info)
726            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
727            .exec_path("/bin/echo")
728            .build();
729
730        assert!(config.validate().is_ok());
731    }
732
733    #[test]
734    fn test_microvm_config_validation_failure_root_path() {
735        let config = MicroVmConfig::builder()
736            .log_level(LogLevel::Info)
737            .rootfs(Rootfs::Native(PathBuf::from("/non/existent/path")))
738            .memory_mib(512)
739            .exec_path("/bin/echo")
740            .build();
741
742        assert!(matches!(
743            config.validate(),
744            Err(MicrosandboxError::InvalidMicroVMConfig(
745                InvalidMicroVMConfigError::RootPathDoesNotExist(_)
746            ))
747        ));
748    }
749
750    #[test]
751    fn test_microvm_config_validation_failure_zero_ram() {
752        let temp_dir = TempDir::new().unwrap();
753        let config = MicroVmConfig::builder()
754            .log_level(LogLevel::Info)
755            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
756            .memory_mib(0)
757            .exec_path("/bin/echo")
758            .build();
759
760        assert!(matches!(
761            config.validate(),
762            Err(MicrosandboxError::InvalidMicroVMConfig(
763                InvalidMicroVMConfigError::MemoryIsZero
764            ))
765        ));
766    }
767
768    #[test]
769    fn test_validate_command_line_valid_strings() {
770        // Test basic ASCII strings
771        assert!(MicroVmConfig::validate_command_line("hello").is_ok());
772        assert!(MicroVmConfig::validate_command_line("hello world").is_ok());
773        assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok());
774
775        // Test edge cases of valid range (space to tilde)
776        assert!(MicroVmConfig::validate_command_line(" ").is_ok()); // space (0x20)
777        assert!(MicroVmConfig::validate_command_line("~").is_ok()); // tilde (0x7E)
778
779        // Test special characters within valid range
780        assert!(MicroVmConfig::validate_command_line("!@#$%^&*()").is_ok());
781        assert!(MicroVmConfig::validate_command_line("path/to/file").is_ok());
782        assert!(MicroVmConfig::validate_command_line("user-name_123").is_ok());
783    }
784
785    #[test]
786    fn test_validate_command_line_invalid_strings() {
787        // Test control characters
788        assert!(MicroVmConfig::validate_command_line("\n").is_err()); // newline
789        assert!(MicroVmConfig::validate_command_line("\t").is_err()); // tab
790        assert!(MicroVmConfig::validate_command_line("\r").is_err()); // carriage return
791        assert!(MicroVmConfig::validate_command_line("\x1B").is_err()); // escape
792
793        // Test non-ASCII Unicode characters
794        assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); // emoji
795        assert!(MicroVmConfig::validate_command_line("über").is_err()); // umlaut
796        assert!(MicroVmConfig::validate_command_line("café").is_err()); // accent
797        assert!(MicroVmConfig::validate_command_line("你好").is_err()); // Chinese characters
798
799        // Test strings mixing valid and invalid characters
800        assert!(MicroVmConfig::validate_command_line("hello\nworld").is_err());
801        assert!(MicroVmConfig::validate_command_line("path/to/file\0").is_err()); // null byte
802        assert!(MicroVmConfig::validate_command_line("hello\x7F").is_err()); // DEL character
803    }
804
805    #[test]
806    fn test_validate_command_line_in_config() {
807        let temp_dir = TempDir::new().unwrap();
808
809        // Test invalid executable path
810        let config = MicroVmConfig::builder()
811            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
812            .memory_mib(512)
813            .exec_path("/bin/hello\nworld")
814            .build();
815        assert!(matches!(
816            config.validate(),
817            Err(MicrosandboxError::InvalidMicroVMConfig(
818                InvalidMicroVMConfigError::InvalidCommandLineString(_)
819            ))
820        ));
821
822        // Test invalid argument
823        let config = MicroVmConfig::builder()
824            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
825            .memory_mib(512)
826            .exec_path("/bin/echo")
827            .args(["hello\tworld"])
828            .build();
829        assert!(matches!(
830            config.validate(),
831            Err(MicrosandboxError::InvalidMicroVMConfig(
832                InvalidMicroVMConfigError::InvalidCommandLineString(_)
833            ))
834        ));
835    }
836
837    #[test]
838    fn test_validate_guest_paths() -> anyhow::Result<()> {
839        // Test valid paths (no conflicts)
840        let valid_paths = vec![
841            "/app".parse::<PathPair>()?,
842            "/data".parse()?,
843            "/var/log".parse()?,
844            "/etc/config".parse()?,
845        ];
846        assert!(MicroVmConfig::validate_guest_paths(&valid_paths).is_ok());
847
848        // Test conflicting paths (direct match)
849        let conflicting_paths = vec![
850            "/app".parse()?,
851            "/data".parse()?,
852            "/app".parse()?, // Duplicate
853        ];
854        assert!(MicroVmConfig::validate_guest_paths(&conflicting_paths).is_err());
855
856        // Test conflicting paths (subset)
857        let subset_paths = vec![
858            "/app".parse()?,
859            "/app/data".parse()?, // Subset of /app
860            "/var/log".parse()?,
861        ];
862        assert!(MicroVmConfig::validate_guest_paths(&subset_paths).is_err());
863
864        // Test conflicting paths (parent)
865        let parent_paths = vec![
866            "/var/log".parse()?,
867            "/var".parse()?, // Parent of /var/log
868            "/etc".parse()?,
869        ];
870        assert!(MicroVmConfig::validate_guest_paths(&parent_paths).is_err());
871
872        // Test paths needing normalization
873        let unnormalized_paths = vec![
874            "/app/./data".parse()?,
875            "/var/log".parse()?,
876            "/etc//config".parse()?,
877        ];
878        assert!(MicroVmConfig::validate_guest_paths(&unnormalized_paths).is_ok());
879
880        // Test paths with normalization conflicts
881        let normalized_conflicts = vec![
882            "/app/./data".parse()?,
883            "/app/data/".parse()?, // Same as first path after normalization
884            "/var/log".parse()?,
885        ];
886        assert!(MicroVmConfig::validate_guest_paths(&normalized_conflicts).is_err());
887
888        Ok(())
889    }
890
891    #[test]
892    fn test_microvm_config_validation_with_guest_paths() -> anyhow::Result<()> {
893        use tempfile::TempDir;
894
895        let temp_dir = TempDir::new()?;
896        let host_dir1 = temp_dir.path().join("dir1");
897        let host_dir2 = temp_dir.path().join("dir2");
898        std::fs::create_dir_all(&host_dir1)?;
899        std::fs::create_dir_all(&host_dir2)?;
900
901        // Test valid configuration
902        let valid_config = MicroVmConfig::builder()
903            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
904            .memory_mib(1024)
905            .exec_path("/bin/echo")
906            .mapped_dirs([
907                format!("{}:/app", host_dir1.display()).parse()?,
908                format!("{}:/data", host_dir2.display()).parse()?,
909            ])
910            .build();
911
912        assert!(valid_config.validate().is_ok());
913
914        // Test configuration with conflicting guest paths
915        let invalid_config = MicroVmConfig::builder()
916            .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
917            .memory_mib(1024)
918            .exec_path("/bin/echo")
919            .mapped_dirs([
920                format!("{}:/app/data", host_dir1.display()).parse()?,
921                format!("{}:/app", host_dir2.display()).parse()?,
922            ])
923            .build();
924
925        assert!(matches!(
926            invalid_config.validate(),
927            Err(MicrosandboxError::InvalidMicroVMConfig(
928                InvalidMicroVMConfigError::ConflictingGuestPaths(_, _)
929            ))
930        ));
931
932        Ok(())
933    }
934}