envs/
lib.rs

1use std::collections::HashSet;
2use std::env;
3use std::fs;
4use std::path::Path;
5use std::fmt;
6
7/// Trait to abstract filesystem operations for easier testing.
8pub trait FileSystem {
9    fn file_exists(&self, path: &Path) -> bool;
10    fn read_to_string(&self, path: &Path) -> std::io::Result<String>;
11}
12
13/// Real filesystem implementation of the `FileSystem` trait.
14pub struct RealFileSystem;
15
16impl FileSystem for RealFileSystem {
17    fn file_exists(&self, path: &Path) -> bool {
18        path.exists()
19    }
20
21    fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
22        fs::read_to_string(path)
23    }
24}
25
26/// Enum representing supported operating systems.
27#[derive(Debug, PartialEq, Eq)]
28pub enum OperatingSystem {
29    Windows,
30    Linux,
31    MacOS,
32    Unknown(String),
33}
34
35impl fmt::Display for OperatingSystem {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            OperatingSystem::Windows => write!(f, "Windows"),
39            OperatingSystem::Linux => write!(f, "Linux"),
40            OperatingSystem::MacOS => write!(f, "macOS"),
41            OperatingSystem::Unknown(name) => write!(f, "Unknown ({})", name),
42        }
43    }
44}
45
46/// Enum representing different container environments.
47#[derive(Debug, PartialEq, Eq)]
48pub enum ContainerEnvironment {
49    Docker,
50    Kubernetes,
51    Podman,
52    None,
53}
54
55impl fmt::Display for ContainerEnvironment {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            ContainerEnvironment::Docker => write!(f, "Docker"),
59            ContainerEnvironment::Kubernetes => write!(f, "Kubernetes"),
60            ContainerEnvironment::Podman => write!(f, "Podman"),
61            ContainerEnvironment::None => write!(f, "None"),
62        }
63    }
64}
65
66/// Enum representing virtualization platforms.
67#[derive(Debug, PartialEq, Eq)]
68pub enum VirtualizationPlatform {
69    VMware,
70    VirtualBox,
71    HyperV,
72    KVM,
73    Other(String),
74    None,
75}
76
77impl fmt::Display for VirtualizationPlatform {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            VirtualizationPlatform::VMware => write!(f, "VMware"),
81            VirtualizationPlatform::VirtualBox => write!(f, "VirtualBox"),
82            VirtualizationPlatform::HyperV => write!(f, "Hyper-V"),
83            VirtualizationPlatform::KVM => write!(f, "KVM"),
84            VirtualizationPlatform::Other(name) => write!(f, "Other ({})", name),
85            VirtualizationPlatform::None => write!(f, "None"),
86        }
87    }
88}
89
90/// Enum representing specific capabilities.
91#[allow(non_camel_case_types)]
92#[derive(Debug, PartialEq, Eq, Hash, Clone)]
93pub enum Capability {
94    CAP_CHOWN,
95    CAP_DAC_OVERRIDE,
96    CAP_DAC_READ_SEARCH,
97    CAP_FOWNER,
98    CAP_FSETID,
99    CAP_KILL,
100    CAP_SETGID,
101    CAP_SETUID,
102    CAP_SETPCAP,
103    CAP_LINUX_IMMUTABLE,
104    CAP_NET_BIND_SERVICE,
105    CAP_NET_BROADCAST,
106    CAP_NET_ADMIN,
107    CAP_NET_RAW,
108    CAP_IPC_LOCK,
109    CAP_IPC_OWNER,
110    CAP_SYS_MODULE,
111    CAP_SYS_RAWIO,
112    CAP_SYS_CHROOT,
113    CAP_SYS_PTRACE,
114    CAP_SYS_PACCT,
115    CAP_SYS_ADMIN,
116    CAP_SYS_BOOT,
117    CAP_SYS_NICE,
118    CAP_SYS_RESOURCE,
119    CAP_SYS_TIME,
120    CAP_SYS_TTY_CONFIG,
121    CAP_MKNOD,
122    CAP_LEASE,
123    CAP_AUDIT_WRITE,
124    CAP_AUDIT_CONTROL,
125    CAP_SETFCAP,
126    Unknown(String),
127}
128
129impl fmt::Display for Capability {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Capability::CAP_CHOWN => write!(f, "CAP_CHOWN"),
133            Capability::CAP_DAC_OVERRIDE => write!(f, "CAP_DAC_OVERRIDE"),
134            Capability::CAP_DAC_READ_SEARCH => write!(f, "CAP_DAC_READ_SEARCH"),
135            Capability::CAP_FOWNER => write!(f, "CAP_FOWNER"),
136            Capability::CAP_FSETID => write!(f, "CAP_FSETID"),
137            Capability::CAP_KILL => write!(f, "CAP_KILL"),
138            Capability::CAP_SETGID => write!(f, "CAP_SETGID"),
139            Capability::CAP_SETUID => write!(f, "CAP_SETUID"),
140            Capability::CAP_SETPCAP => write!(f, "CAP_SETPCAP"),
141            Capability::CAP_LINUX_IMMUTABLE => write!(f, "CAP_LINUX_IMMUTABLE"),
142            Capability::CAP_NET_BIND_SERVICE => write!(f, "CAP_NET_BIND_SERVICE"),
143            Capability::CAP_NET_BROADCAST => write!(f, "CAP_NET_BROADCAST"),
144            Capability::CAP_NET_ADMIN => write!(f, "CAP_NET_ADMIN"),
145            Capability::CAP_NET_RAW => write!(f, "CAP_NET_RAW"),
146            Capability::CAP_IPC_LOCK => write!(f, "CAP_IPC_LOCK"),
147            Capability::CAP_IPC_OWNER => write!(f, "CAP_IPC_OWNER"),
148            Capability::CAP_SYS_MODULE => write!(f, "CAP_SYS_MODULE"),
149            Capability::CAP_SYS_RAWIO => write!(f, "CAP_SYS_RAWIO"),
150            Capability::CAP_SYS_CHROOT => write!(f, "CAP_SYS_CHROOT"),
151            Capability::CAP_SYS_PTRACE => write!(f, "CAP_SYS_PTRACE"),
152            Capability::CAP_SYS_PACCT => write!(f, "CAP_SYS_PACCT"),
153            Capability::CAP_SYS_ADMIN => write!(f, "CAP_SYS_ADMIN"),
154            Capability::CAP_SYS_BOOT => write!(f, "CAP_SYS_BOOT"),
155            Capability::CAP_SYS_NICE => write!(f, "CAP_SYS_NICE"),
156            Capability::CAP_SYS_RESOURCE => write!(f, "CAP_SYS_RESOURCE"),
157            Capability::CAP_SYS_TIME => write!(f, "CAP_SYS_TIME"),
158            Capability::CAP_SYS_TTY_CONFIG => write!(f, "CAP_SYS_TTY_CONFIG"),
159            Capability::CAP_MKNOD => write!(f, "CAP_MKNOD"),
160            Capability::CAP_LEASE => write!(f, "CAP_LEASE"),
161            Capability::CAP_AUDIT_WRITE => write!(f, "CAP_AUDIT_WRITE"),
162            Capability::CAP_AUDIT_CONTROL => write!(f, "CAP_AUDIT_CONTROL"),
163            Capability::CAP_SETFCAP => write!(f, "CAP_SETFCAP"),
164            Capability::Unknown(name) => write!(f, "Unknown ({})", name),
165        }
166    }
167}
168
169/// Struct holding the detected capabilities.
170#[derive(Debug, PartialEq, Eq)]
171pub struct Capabilities {
172    pub effective: HashSet<Capability>,
173}
174
175/// Struct holding the environment information.
176#[derive(Debug, PartialEq, Eq)]
177pub struct EnvironmentInfo {
178    pub os: OperatingSystem,
179    pub container: ContainerEnvironment,
180    pub virtualization: VirtualizationPlatform,
181    pub capabilities: Option<Capabilities>,
182}
183
184/// Retrieves the current operating system.
185pub fn get_os() -> OperatingSystem {
186    match env::consts::OS {
187        "windows" => OperatingSystem::Windows,
188        "linux" => OperatingSystem::Linux,
189        "macos" => OperatingSystem::MacOS,
190        other => OperatingSystem::Unknown(other.to_string()),
191    }
192}
193
194/// Checks if the environment is Kubernetes by verifying specific environment variables.
195fn is_kubernetes() -> bool {
196    env::var("KUBERNETES_SERVICE_HOST").is_ok()
197}
198
199/// Attempts to determine the container runtime by inspecting specific environment variables.
200/// This function can be expanded to include more sophisticated detection mechanisms.
201fn get_container_runtime() -> Option<String> {
202    // Example: Check for container runtime environment variable.
203    env::var("CONTAINER_RUNTIME").ok()
204}
205
206/// Detects the current container environment using the provided `FileSystem`.
207pub fn detect_container(fs: &dyn FileSystem) -> ContainerEnvironment {
208    // Check for Kubernetes environment
209    if is_kubernetes() {
210        return ContainerEnvironment::Kubernetes;
211    }
212
213    // Check for /.dockerenv file
214    if fs.file_exists(Path::new("/.dockerenv")) {
215        // Further distinguish between Docker and Podman if possible.
216        if let Some(container_runtime) = get_container_runtime() {
217            return match container_runtime.as_str() {
218                "docker" => ContainerEnvironment::Docker,
219                "podman" => ContainerEnvironment::Podman,
220                _ => ContainerEnvironment::None,
221            };
222        }
223        return ContainerEnvironment::Docker;
224    }
225
226    // Check /proc/1/cgroup for Docker or Podman identifiers
227    if let Ok(cgroup) = fs.read_to_string(Path::new("/proc/1/cgroup")) {
228        if cgroup.contains("docker") {
229            return ContainerEnvironment::Docker;
230        } else if cgroup.contains("podman") {
231            return ContainerEnvironment::Podman;
232        }
233    }
234
235    ContainerEnvironment::None
236}
237
238/// Capability bit position mapping
239const CAPABILITY_BIT_MAP: [Option<Capability>; 32] = [
240    Some(Capability::CAP_CHOWN),             // 0
241    Some(Capability::CAP_DAC_OVERRIDE),      // 1
242    Some(Capability::CAP_DAC_READ_SEARCH),   // 2
243    Some(Capability::CAP_FOWNER),            // 3
244    Some(Capability::CAP_FSETID),            // 4
245    Some(Capability::CAP_KILL),              // 5
246    Some(Capability::CAP_SETGID),            // 6
247    Some(Capability::CAP_SETUID),            // 7
248    Some(Capability::CAP_SETPCAP),           // 8
249    Some(Capability::CAP_LINUX_IMMUTABLE),   // 9
250    Some(Capability::CAP_NET_BIND_SERVICE),  // 10
251    Some(Capability::CAP_NET_BROADCAST),     // 11
252    Some(Capability::CAP_NET_ADMIN),         // 12
253    Some(Capability::CAP_NET_RAW),           // 13
254    Some(Capability::CAP_IPC_LOCK),          // 14
255    Some(Capability::CAP_IPC_OWNER),         // 15
256    Some(Capability::CAP_SYS_MODULE),        // 16
257    Some(Capability::CAP_SYS_RAWIO),         // 17
258    Some(Capability::CAP_SYS_CHROOT),        // 18
259    Some(Capability::CAP_SYS_PTRACE),        // 19
260    Some(Capability::CAP_SYS_PACCT),         // 20
261    Some(Capability::CAP_SYS_ADMIN),         // 21
262    Some(Capability::CAP_SYS_BOOT),          // 22
263    Some(Capability::CAP_SYS_NICE),          // 23
264    Some(Capability::CAP_SYS_RESOURCE),      // 24
265    Some(Capability::CAP_SYS_TIME),          // 25
266    Some(Capability::CAP_SYS_TTY_CONFIG),    // 26
267    Some(Capability::CAP_MKNOD),             // 27
268    Some(Capability::CAP_LEASE),             // 28
269    Some(Capability::CAP_AUDIT_WRITE),       // 29
270    Some(Capability::CAP_AUDIT_CONTROL),     // 30
271    Some(Capability::CAP_SETFCAP),           // 31
272];
273
274/// Parses the CapEff value and returns a list of enabled capabilities.
275fn parse_capabilities(cap_eff: u64) -> Vec<Capability> {
276    let mut capabilities = Vec::new();
277    for bit in 0..32 {
278        if cap_eff & (1 << bit) != 0 {
279            if let Some(cap) = CAPABILITY_BIT_MAP[bit as usize].clone() {
280                capabilities.push(cap);
281            } else {
282                capabilities.push(Capability::Unknown(format!("Bit {}", bit)));
283            }
284        }
285    }
286    capabilities
287}
288
289/// Reads and parses the CapEff field from /proc/self/status to determine effective capabilities.
290fn get_capabilities(fs: &dyn FileSystem) -> Option<Capabilities> {
291    let status_path = Path::new("/proc/self/status");
292    if !fs.file_exists(status_path) {
293        return None;
294    }
295
296    let status_content = match fs.read_to_string(status_path) {
297        Ok(content) => content,
298        Err(_) => return None,
299    };
300
301    // Find the CapEff line
302    let cap_eff_line = status_content
303        .lines()
304        .find(|line| line.starts_with("CapEff:"))
305        .map(|line| line.trim_start_matches("CapEff:").trim());
306
307    let cap_eff_str = match cap_eff_line {
308        Some(s) => s,
309        None => return None,
310    };
311
312    // Parse the hexadecimal CapEff value
313    let cap_eff = match u64::from_str_radix(cap_eff_str, 16) {
314        Ok(val) => val,
315        Err(_) => return None,
316    };
317
318    // Parse capabilities using the provided parse_capabilities function
319    let capabilities = parse_capabilities(cap_eff);
320
321    Some(Capabilities {
322        effective: capabilities.into_iter().collect(),
323    })
324}
325
326/// Detects the current virtualization platform using the provided `FileSystem`.
327pub fn detect_virtualization(fs: &dyn FileSystem) -> VirtualizationPlatform {
328    // If running inside a container, skip virtualization detection
329    if detect_container(fs) != ContainerEnvironment::None {
330        return VirtualizationPlatform::None;
331    }
332
333    let mut is_hypervisor = false;
334
335    // On Linux, check for hypervisor or kvm flag in /proc/cpuinfo
336    if let Ok(cpuinfo) = fs.read_to_string(Path::new("/proc/cpuinfo")) {
337        let cpuinfo_lower = cpuinfo.to_lowercase();
338        if cpuinfo_lower.contains("hypervisor") || cpuinfo_lower.contains("kvm") {
339            is_hypervisor = true;
340        }
341    }
342
343    // Additionally, check for /dev/kvm
344    if fs.file_exists(Path::new("/dev/kvm")) {
345        is_hypervisor = true;
346    }
347
348    if is_hypervisor {
349        // Further distinguish virtualization platforms by checking DMI info
350        for path in [
351            Path::new("/sys/class/dmi/id/product_name"),
352            Path::new("/sys/class/dmi/id/sys_vendor"),
353        ]
354            .iter()
355        {
356            if let Ok(content) = fs.read_to_string(path) {
357                let content_lower = content.to_lowercase();
358                if content_lower.contains("virtualbox") {
359                    return VirtualizationPlatform::VirtualBox;
360                } else if content_lower.contains("vmware") {
361                    return VirtualizationPlatform::VMware;
362                } else if content_lower.contains("hyper-v") || content_lower.contains("microsoft corporation") {
363                    return VirtualizationPlatform::HyperV;
364                } else if content_lower.contains("kvm") || content_lower.contains("q35") || content_lower.contains("standard pc") {
365                    return VirtualizationPlatform::KVM;
366                } else if !content.trim().is_empty() {
367                    return VirtualizationPlatform::Other(content.trim().to_string());
368                }
369            }
370        }
371
372        // If no DMI info matches, but hypervisor flag is set, assume KVM
373        return VirtualizationPlatform::KVM;
374    }
375
376    VirtualizationPlatform::None
377}
378
379/// Retrieves the complete environment information using the real filesystem.
380pub fn get_environment_info() -> EnvironmentInfo {
381    let fs = RealFileSystem;
382    let container = detect_container(&fs);
383    let capabilities = if container == ContainerEnvironment::Docker {
384        get_capabilities(&fs)
385    } else {
386        None
387    };
388    EnvironmentInfo {
389        os: get_os(),
390        container,
391        virtualization: detect_virtualization(&fs),
392        capabilities,
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use std::collections::HashMap;
400    use std::env;
401    use std::path::PathBuf;
402
403    /// Mock filesystem for testing purposes.
404    struct MockFileSystem {
405        existing_files: Vec<PathBuf>,
406        file_contents: HashMap<PathBuf, String>,
407    }
408
409    impl MockFileSystem {
410        fn new() -> Self {
411            Self {
412                existing_files: Vec::new(),
413                file_contents: HashMap::new(),
414            }
415        }
416
417        /// Adds a file to the mock filesystem with specified contents.
418        fn add_file(&mut self, path: PathBuf, contents: String) {
419            self.existing_files.push(path.clone());
420            self.file_contents.insert(path, contents);
421        }
422    }
423
424    impl FileSystem for MockFileSystem {
425        fn file_exists(&self, path: &Path) -> bool {
426            self.existing_files.iter().any(|p| p == path)
427        }
428
429        fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
430            if let Some(content) = self.file_contents.get(path) {
431                Ok(content.clone())
432            } else {
433                Err(std::io::Error::new(
434                    std::io::ErrorKind::NotFound,
435                    "File not found",
436                ))
437            }
438        }
439    }
440
441    #[test]
442    fn test_get_os() {
443        let os = get_os();
444        #[cfg(target_os = "windows")]
445        assert_eq!(os, OperatingSystem::Windows);
446        #[cfg(target_os = "linux")]
447        assert_eq!(os, OperatingSystem::Linux);
448        #[cfg(target_os = "macos")]
449        assert_eq!(os, OperatingSystem::MacOS);
450        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
451        match os {
452            OperatingSystem::Unknown(ref s) => println!("Unknown OS: {}", s),
453            _ => panic!("Expected Unknown OS variant"),
454        }
455    }
456
457    #[test]
458    fn test_is_kubernetes() {
459        // Simulate Kubernetes environment variable
460        env::set_var("KUBERNETES_SERVICE_HOST", "localhost");
461        assert!(is_kubernetes());
462
463        // Remove the environment variable
464        env::remove_var("KUBERNETES_SERVICE_HOST");
465        assert!(!is_kubernetes());
466    }
467
468    #[cfg(target_os = "linux")]
469    #[test]
470    fn test_detect_container_docker() {
471        let mut mock_fs = MockFileSystem::new();
472        mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
473        // Simulate container runtime
474        env::set_var("CONTAINER_RUNTIME", "docker");
475
476        let container = detect_container(&mock_fs);
477        assert_eq!(container, ContainerEnvironment::Docker);
478
479        env::remove_var("CONTAINER_RUNTIME");
480    }
481
482    #[cfg(target_os = "linux")]
483    #[test]
484    fn test_detect_container_podman() {
485        let mut mock_fs = MockFileSystem::new();
486        mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
487        env::set_var("CONTAINER_RUNTIME", "podman");
488
489        let container = detect_container(&mock_fs);
490        assert_eq!(container, ContainerEnvironment::Podman);
491
492        env::remove_var("CONTAINER_RUNTIME");
493    }
494
495    #[cfg(target_os = "linux")]
496    #[test]
497    fn test_detect_container_docker_via_cgroup() {
498        let mut mock_fs = MockFileSystem::new();
499        // Simulate /proc/1/cgroup containing "docker"
500        mock_fs.add_file(PathBuf::from("/proc/1/cgroup"), "docker".to_string());
501
502        let container = detect_container(&mock_fs);
503        assert_eq!(container, ContainerEnvironment::Docker);
504    }
505
506    #[cfg(target_os = "linux")]
507    #[test]
508    fn test_detect_container_podman_via_cgroup() {
509        let mut mock_fs = MockFileSystem::new();
510        // Simulate /proc/1/cgroup containing "podman"
511        mock_fs.add_file(PathBuf::from("/proc/1/cgroup"), "podman".to_string());
512
513        let container = detect_container(&mock_fs);
514        assert_eq!(container, ContainerEnvironment::Podman);
515    }
516
517    #[cfg(target_os = "linux")]
518    #[test]
519    fn test_detect_container_none() {
520        let mock_fs = MockFileSystem::new();
521        let container = detect_container(&mock_fs);
522        assert_eq!(container, ContainerEnvironment::None);
523    }
524
525    #[cfg(target_os = "linux")]
526    #[test]
527    fn test_detect_virtualization_virtualbox() {
528        let mut mock_fs = MockFileSystem::new();
529        // Ensure not running in container
530        // Simulate /proc/cpuinfo containing "hypervisor"
531        mock_fs.add_file(
532            PathBuf::from("/proc/cpuinfo"),
533            "flags : hypervisor".to_string(),
534        );
535        // Simulate /sys/class/dmi/id/product_name containing "VirtualBox"
536        mock_fs.add_file(
537            PathBuf::from("/sys/class/dmi/id/product_name"),
538            "VirtualBox".to_string(),
539        );
540
541        let virtualization = detect_virtualization(&mock_fs);
542        assert_eq!(virtualization, VirtualizationPlatform::VirtualBox);
543    }
544
545    #[cfg(target_os = "linux")]
546    #[test]
547    fn test_detect_virtualization_vmware() {
548        let mut mock_fs = MockFileSystem::new();
549        // Ensure not running in container
550        // Simulate /proc/cpuinfo containing "hypervisor"
551        mock_fs.add_file(
552            PathBuf::from("/proc/cpuinfo"),
553            "flags : hypervisor".to_string(),
554        );
555        // Simulate /sys/class/dmi/id/sys_vendor containing "VMware"
556        mock_fs.add_file(
557            PathBuf::from("/sys/class/dmi/id/sys_vendor"),
558            "VMware, Inc.".to_string(),
559        );
560
561        let virtualization = detect_virtualization(&mock_fs);
562        assert_eq!(virtualization, VirtualizationPlatform::VMware);
563    }
564
565    #[cfg(target_os = "linux")]
566    #[test]
567    fn test_detect_virtualization_hyperv() {
568        let mut mock_fs = MockFileSystem::new();
569        // Ensure not running in container
570        // Simulate /proc/cpuinfo containing "hypervisor"
571        mock_fs.add_file(
572            PathBuf::from("/proc/cpuinfo"),
573            "flags : hypervisor".to_string(),
574        );
575        // Simulate /sys/class/dmi/id/sys_vendor containing "Microsoft Corporation" (Hyper-V)
576        mock_fs.add_file(
577            PathBuf::from("/sys/class/dmi/id/sys_vendor"),
578            "Microsoft Corporation".to_string(),
579        );
580
581        let virtualization = detect_virtualization(&mock_fs);
582        assert_eq!(virtualization, VirtualizationPlatform::HyperV);
583    }
584
585    #[cfg(target_os = "linux")]
586    #[test]
587    fn test_detect_virtualization_kvm() {
588        let mut mock_fs = MockFileSystem::new();
589        // Ensure not running in container
590        // Simulate /proc/cpuinfo containing "hypervisor" and "kvm"
591        mock_fs.add_file(
592            PathBuf::from("/proc/cpuinfo"),
593            "flags : hypervisor kvm".to_string(),
594        );
595        // Simulate /dev/kvm exists
596        mock_fs.add_file(PathBuf::from("/dev/kvm"), "".to_string());
597
598        let virtualization = detect_virtualization(&mock_fs);
599        assert_eq!(virtualization, VirtualizationPlatform::KVM);
600    }
601
602    #[cfg(target_os = "linux")]
603    #[test]
604    fn test_get_environment_info_virtualization() {
605        let mut mock_fs = MockFileSystem::new();
606        // Simulate /proc/cpuinfo containing "hypervisor"
607        mock_fs.add_file(
608            PathBuf::from("/proc/cpuinfo"),
609            "flags : hypervisor".to_string(),
610        );
611        // Simulate /sys/class/dmi/id/product_name containing "VMware"
612        mock_fs.add_file(
613            PathBuf::from("/sys/class/dmi/id/product_name"),
614            "VMware".to_string(),
615        );
616
617        let info = get_environment_info_with_fs(&mock_fs);
618        assert_eq!(info.os, OperatingSystem::Linux);
619        assert_eq!(info.container, ContainerEnvironment::None);
620        assert_eq!(info.virtualization, VirtualizationPlatform::VMware);
621        assert_eq!(info.capabilities, None);
622    }
623
624    #[cfg(target_os = "linux")]
625    #[test]
626    fn test_detect_virtualization_none() {
627        let mock_fs = MockFileSystem::new();
628        let virtualization = detect_virtualization(&mock_fs);
629        assert_eq!(virtualization, VirtualizationPlatform::None);
630    }
631
632    #[test]
633    fn test_get_environment_info() {
634        // Simulate Kubernetes environment
635        env::set_var("KUBERNETES_SERVICE_HOST", "localhost");
636        let info = get_environment_info_with_fs(&RealFileSystem);
637        #[cfg(target_os = "windows")]
638        assert_eq!(info.os, OperatingSystem::Windows);
639        #[cfg(target_os = "linux")]
640        assert_eq!(info.os, OperatingSystem::Linux);
641        #[cfg(target_os = "macos")]
642        assert_eq!(info.os, OperatingSystem::MacOS);
643        assert_eq!(info.container, ContainerEnvironment::Kubernetes);
644        assert_eq!(info.virtualization, VirtualizationPlatform::None);
645        assert_eq!(info.capabilities, None);
646        env::remove_var("KUBERNETES_SERVICE_HOST");
647    }
648
649    #[cfg(target_os = "linux")]
650    #[test]
651    fn test_get_environment_info_capabilities() {
652        let mut mock_fs = MockFileSystem::new();
653        // Simulate Docker environment
654        mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
655        // Simulate CapEff in /proc/self/status (e.g., CapEff=00000000002c0000)
656        mock_fs.add_file(
657            PathBuf::from("/proc/self/status"),
658            "CapEff: 00000000002c0000\n".to_string(),
659        );
660
661        let capabilities = get_capabilities(&mock_fs).unwrap();
662        let expected: HashSet<Capability> = vec![
663            Capability::CAP_SYS_ADMIN,
664            Capability::CAP_SYS_PTRACE,
665            Capability::CAP_SYS_CHROOT,
666        ]
667            .into_iter()
668            .collect();
669        assert_eq!(capabilities.effective, expected);
670    }
671
672    /// Helper function to get environment info using a specific `FileSystem` implementation.
673    fn get_environment_info_with_fs(fs: &dyn FileSystem) -> EnvironmentInfo {
674        EnvironmentInfo {
675            os: get_os(),
676            container: detect_container(fs),
677            virtualization: detect_virtualization(fs),
678            capabilities: if detect_container(fs) == ContainerEnvironment::Docker {
679                get_capabilities(fs)
680            } else {
681                None
682            },
683        }
684    }
685}