sandbox_runtime/utils/
platform.rs

1//! Platform detection utilities.
2
3/// Supported platforms.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Platform {
6    MacOS,
7    Linux,
8}
9
10impl Platform {
11    /// Detect the current platform.
12    /// Note: All Linux including WSL returns Linux. Use `get_wsl_version()` to detect WSL1 (unsupported).
13    pub fn current() -> Option<Self> {
14        #[cfg(target_os = "macos")]
15        {
16            Some(Platform::MacOS)
17        }
18        #[cfg(target_os = "linux")]
19        {
20            // WSL2+ is treated as Linux (same sandboxing)
21            // WSL1 is also returned as Linux but will fail is_supported() check
22            Some(Platform::Linux)
23        }
24        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
25        {
26            None
27        }
28    }
29
30    /// Check if the current platform is supported.
31    /// Returns false for unsupported platforms and WSL1.
32    pub fn is_supported() -> bool {
33        match Self::current() {
34            Some(Platform::Linux) => {
35                // WSL1 doesn't support bubblewrap
36                get_wsl_version() != Some("1".to_string())
37            }
38            Some(Platform::MacOS) => true,
39            None => false,
40        }
41    }
42
43    /// Get the platform name as a string.
44    pub fn name(&self) -> &'static str {
45        match self {
46            Platform::MacOS => "macOS",
47            Platform::Linux => "Linux",
48        }
49    }
50}
51
52/// Get the current platform, if supported.
53pub fn current_platform() -> Option<Platform> {
54    Platform::current()
55}
56
57/// Check if running on macOS.
58#[inline]
59pub fn is_macos() -> bool {
60    cfg!(target_os = "macos")
61}
62
63/// Check if running on Linux.
64#[inline]
65pub fn is_linux() -> bool {
66    cfg!(target_os = "linux")
67}
68
69/// Get the CPU architecture.
70pub fn get_arch() -> &'static str {
71    #[cfg(target_arch = "x86_64")]
72    {
73        "x64"
74    }
75    #[cfg(target_arch = "aarch64")]
76    {
77        "arm64"
78    }
79    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
80    {
81        "unknown"
82    }
83}
84
85/// Get the WSL version (1 or 2+) if running in WSL.
86/// Returns None if not running in WSL.
87///
88/// Detection logic:
89/// 1. Read /proc/version which contains kernel info
90/// 2. Look for explicit "WSL2", "WSL3" etc. markers (case-insensitive)
91/// 3. If no explicit version but "microsoft" is present, assume WSL1
92///    (handles the original WSL1 format like "4.4.0-19041-Microsoft")
93///
94/// WSL1 is unsupported because bubblewrap requires user namespaces which WSL1 lacks.
95/// WSL2 runs a real Linux kernel and supports full sandboxing.
96pub fn get_wsl_version() -> Option<String> {
97    #[cfg(target_os = "linux")]
98    {
99        use std::fs;
100
101        let proc_version = match fs::read_to_string("/proc/version") {
102            Ok(content) => content,
103            Err(_) => return None,
104        };
105
106        parse_wsl_version_from_string(&proc_version)
107    }
108    #[cfg(not(target_os = "linux"))]
109    {
110        None
111    }
112}
113
114/// Parse WSL version from a /proc/version string.
115/// Extracted for unit testing.
116#[cfg(any(target_os = "linux", test))]
117fn parse_wsl_version_from_string(proc_version: &str) -> Option<String> {
118    // Check for explicit WSL version markers (e.g., "WSL2", "WSL3", etc.)
119    // Use a simple pattern match since we can't use regex easily here
120    let proc_lower = proc_version.to_lowercase();
121
122    // Look for "wsl" followed by a digit - use proc_lower for both finding and extraction
123    // to ensure consistent byte positions
124    if let Some(pos) = proc_lower.find("wsl") {
125        let after_wsl = &proc_lower[pos + 3..];
126        if let Some(ch) = after_wsl.chars().next() {
127            if ch.is_ascii_digit() {
128                return Some(ch.to_string());
129            }
130        }
131    }
132
133    // If no explicit WSL version but contains Microsoft, assume WSL1
134    // This handles the original WSL1 format: "4.4.0-19041-Microsoft"
135    if proc_lower.contains("microsoft") {
136        return Some("1".to_string());
137    }
138
139    None
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_get_arch() {
148        let arch = get_arch();
149        assert!(arch == "x64" || arch == "arm64" || arch == "unknown");
150    }
151
152    #[test]
153    fn test_platform_current() {
154        let platform = Platform::current();
155        #[cfg(target_os = "macos")]
156        assert_eq!(platform, Some(Platform::MacOS));
157        #[cfg(target_os = "linux")]
158        assert_eq!(platform, Some(Platform::Linux));
159    }
160
161    #[test]
162    fn test_get_wsl_version_non_linux() {
163        #[cfg(not(target_os = "linux"))]
164        {
165            assert_eq!(get_wsl_version(), None);
166        }
167    }
168
169    #[test]
170    fn test_wsl_version_parsing_wsl2() {
171        // WSL2 kernel version string (typical format)
172        let wsl2_version = "Linux version 5.15.90.1-microsoft-standard-WSL2 (oe-user@oe-host)";
173        assert_eq!(parse_wsl_version_from_string(wsl2_version), Some("2".to_string()));
174
175        // Case insensitivity
176        let wsl2_upper = "Linux version 5.15.90.1-MICROSOFT-STANDARD-WSL2";
177        assert_eq!(parse_wsl_version_from_string(wsl2_upper), Some("2".to_string()));
178    }
179
180    #[test]
181    fn test_wsl_version_parsing_wsl1() {
182        // WSL1 kernel version string (original format with just "Microsoft")
183        let wsl1_version = "Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com)";
184        assert_eq!(parse_wsl_version_from_string(wsl1_version), Some("1".to_string()));
185
186        // Case variations
187        let wsl1_lower = "linux version 4.4.0-19041-microsoft";
188        assert_eq!(parse_wsl_version_from_string(wsl1_lower), Some("1".to_string()));
189    }
190
191    #[test]
192    fn test_wsl_version_parsing_native_linux() {
193        // Native Linux (no WSL markers)
194        let native = "Linux version 6.2.0-26-generic (buildd@ubuntu)";
195        assert_eq!(parse_wsl_version_from_string(native), None);
196
197        // Empty string
198        assert_eq!(parse_wsl_version_from_string(""), None);
199    }
200
201    #[test]
202    fn test_wsl_version_parsing_future_version() {
203        // Future WSL versions (WSL3, WSL4, etc.)
204        let wsl3 = "Linux version 6.0.0-microsoft-standard-WSL3";
205        assert_eq!(parse_wsl_version_from_string(wsl3), Some("3".to_string()));
206
207        let wsl9 = "Linux version 7.0.0-microsoft-standard-WSL9";
208        assert_eq!(parse_wsl_version_from_string(wsl9), Some("9".to_string()));
209    }
210}