adaptive_pipeline_bootstrap/platform/
unix.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Unix Platform Implementation
9//!
10//! POSIX-compliant implementation for Linux and macOS.
11//!
12//! ## Platform APIs Used
13//!
14//! - **System Info**: `libc::sysconf` for page size and CPU count
15//! - **Memory Info**:
16//!   - Linux: `/proc/meminfo` parsing
17//!   - macOS: `sysctlbyname` syscalls
18//! - **Security**: `libc::geteuid` for privilege checking
19//! - **Permissions**: `std::os::unix::fs::PermissionsExt`
20//! - **File Sync**: `tokio::fs::File::sync_all`
21
22use super::{Platform, PlatformError};
23use async_trait::async_trait;
24use std::path::{Path, PathBuf};
25
26/// Unix (POSIX) platform implementation
27///
28/// Supports Linux and macOS using POSIX APIs and platform-specific syscalls.
29pub struct UnixPlatform;
30
31impl UnixPlatform {
32    /// Create a new Unix platform instance
33    pub fn new() -> Self {
34        Self
35    }
36
37    /// Get memory information on Linux by parsing /proc/meminfo
38    #[cfg(target_os = "linux")]
39    fn get_memory_info_linux() -> Result<(u64, u64), PlatformError> {
40        use std::fs;
41
42        let meminfo = fs::read_to_string("/proc/meminfo")
43            .map_err(|e| PlatformError::Other(format!("Failed to read /proc/meminfo: {}", e)))?;
44
45        let mut total = None;
46        let mut available = None;
47
48        for line in meminfo.lines() {
49            if let Some(value) = line.strip_prefix("MemTotal:") {
50                total = value
51                    .split_whitespace()
52                    .next()
53                    .and_then(|s| s.parse::<u64>().ok())
54                    .map(|kb| kb * 1024); // Convert KB to bytes
55            } else if let Some(value) = line.strip_prefix("MemAvailable:") {
56                available = value
57                    .split_whitespace()
58                    .next()
59                    .and_then(|s| s.parse::<u64>().ok())
60                    .map(|kb| kb * 1024); // Convert KB to bytes
61            }
62
63            if total.is_some() && available.is_some() {
64                break;
65            }
66        }
67
68        match (total, available) {
69            (Some(t), Some(a)) => Ok((t, a)),
70            _ => Err(PlatformError::Other("Failed to parse memory info".to_string())),
71        }
72    }
73
74    /// Get memory information on macOS using sysctl
75    #[cfg(target_os = "macos")]
76    fn get_memory_info_macos() -> Result<(u64, u64), PlatformError> {
77        use std::mem;
78
79        // SAFETY: sysctlbyname is safe when:
80        // 1. The name string is a valid C string (guaranteed by c"" literal)
81        // 2. The output buffer is properly sized (we pass size of u64)
82        // 3. The oldp pointer is valid and properly aligned (we use &mut u64)
83        // Returns non-zero on error, which we check and handle appropriately.
84        unsafe {
85            // Get total memory
86            let mut total: u64 = 0;
87            let mut size = mem::size_of::<u64>();
88            #[rustfmt::skip]
89            let name = c"hw.memsize".as_ptr();
90
91            if libc::sysctlbyname(
92                name,
93                &mut total as *mut _ as *mut libc::c_void,
94                &mut size,
95                std::ptr::null_mut(),
96                0,
97            ) != 0
98            {
99                return Err(PlatformError::Other(
100                    "Failed to get total memory via sysctl".to_string(),
101                ));
102            }
103
104            // Get available memory (approximate using free memory)
105            // Note: This is an approximation. For exact VM stats, would need mach APIs
106            let mut available: u64 = 0;
107            let mut avail_size = mem::size_of::<u64>();
108            #[rustfmt::skip]
109            let avail_name = c"vm.page_free_count".as_ptr();
110
111            if libc::sysctlbyname(
112                avail_name,
113                &mut available as *mut _ as *mut libc::c_void,
114                &mut avail_size,
115                std::ptr::null_mut(),
116                0,
117            ) != 0
118            {
119                // If can't get exact available, estimate as half of total
120                // This is very rough but prevents errors
121                available = total / 2;
122            } else {
123                // Convert pages to bytes
124                let page_size = Self::page_size_impl();
125                available *= page_size;
126            }
127
128            Ok((total, available))
129        }
130    }
131
132    /// Internal implementation of page_size
133    fn page_size_impl() -> u64 {
134        // SAFETY: sysconf(_SC_PAGESIZE) is always safe to call on Unix systems.
135        // It returns -1 on error, which we check and handle with a fallback value.
136        unsafe {
137            let size = libc::sysconf(libc::_SC_PAGESIZE);
138            if size > 0 {
139                size as u64
140            } else {
141                4096 // Default fallback
142            }
143        }
144    }
145}
146
147impl Default for UnixPlatform {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[async_trait]
154impl Platform for UnixPlatform {
155    fn page_size(&self) -> usize {
156        Self::page_size_impl() as usize
157    }
158
159    fn cpu_count(&self) -> usize {
160        // SAFETY: sysconf(_SC_NPROCESSORS_ONLN) is always safe to call on Unix systems.
161        // It returns -1 on error, which we check and handle with a fallback value.
162        unsafe {
163            let count = libc::sysconf(libc::_SC_NPROCESSORS_ONLN);
164            if count > 0 {
165                count as usize
166            } else {
167                1 // Fallback to 1 CPU
168            }
169        }
170    }
171
172    fn total_memory(&self) -> Result<u64, PlatformError> {
173        #[cfg(target_os = "linux")]
174        {
175            Self::get_memory_info_linux().map(|(total, _)| total)
176        }
177
178        #[cfg(target_os = "macos")]
179        {
180            Self::get_memory_info_macos().map(|(total, _)| total)
181        }
182
183        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
184        {
185            Err(PlatformError::NotSupported(
186                "Memory info not supported on this Unix variant".to_string(),
187            ))
188        }
189    }
190
191    fn available_memory(&self) -> Result<u64, PlatformError> {
192        #[cfg(target_os = "linux")]
193        {
194            Self::get_memory_info_linux().map(|(_, available)| available)
195        }
196
197        #[cfg(target_os = "macos")]
198        {
199            Self::get_memory_info_macos().map(|(_, available)| available)
200        }
201
202        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
203        {
204            Err(PlatformError::NotSupported(
205                "Memory info not supported on this Unix variant".to_string(),
206            ))
207        }
208    }
209
210    fn line_separator(&self) -> &'static str {
211        "\n"
212    }
213
214    fn path_separator(&self) -> char {
215        ':'
216    }
217
218    fn platform_name(&self) -> &'static str {
219        #[cfg(target_os = "linux")]
220        return "linux";
221
222        #[cfg(target_os = "macos")]
223        return "macos";
224
225        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
226        return "unix";
227    }
228
229    fn temp_dir(&self) -> PathBuf {
230        std::env::temp_dir()
231    }
232
233    fn is_elevated(&self) -> bool {
234        // SAFETY: geteuid() is always safe to call on Unix systems.
235        // It simply returns the effective user ID of the calling process.
236        unsafe { libc::geteuid() == 0 }
237    }
238
239    fn set_permissions(&self, path: &Path, mode: u32) -> Result<(), PlatformError> {
240        use std::fs;
241        use std::os::unix::fs::PermissionsExt;
242
243        let permissions = fs::Permissions::from_mode(mode);
244        fs::set_permissions(path, permissions)?;
245        Ok(())
246    }
247
248    fn is_executable(&self, path: &Path) -> bool {
249        use std::os::unix::fs::PermissionsExt;
250
251        if let Ok(metadata) = std::fs::metadata(path) {
252            let permissions = metadata.permissions();
253            let mode = permissions.mode();
254            // Check if any execute bit is set (owner, group, or other)
255            (mode & 0o111) != 0
256        } else {
257            false
258        }
259    }
260
261    async fn sync_file(&self, file: &tokio::fs::File) -> Result<(), PlatformError> {
262        file.sync_all().await?;
263        Ok(())
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_unix_platform_basics() {
273        let platform = UnixPlatform::new();
274
275        // CPU count should be at least 1
276        assert!(platform.cpu_count() >= 1);
277
278        // Page size should be reasonable
279        let page_size = platform.page_size();
280        assert!(page_size >= 512);
281        assert!(page_size <= 65536);
282    }
283
284    #[test]
285    fn test_unix_platform_constants() {
286        let platform = UnixPlatform::new();
287
288        assert_eq!(platform.line_separator(), "\n");
289        assert_eq!(platform.path_separator(), ':');
290
291        let name = platform.platform_name();
292        assert!(name == "linux" || name == "macos" || name == "unix");
293    }
294
295    #[test]
296    fn test_memory_info() {
297        let platform = UnixPlatform::new();
298
299        // Total memory should be retrievable
300        let total = platform.total_memory();
301        assert!(total.is_ok());
302        if let Ok(t) = total {
303            assert!(t > 0);
304        }
305
306        // Available memory should be retrievable
307        let available = platform.available_memory();
308        assert!(available.is_ok());
309        if let Ok(a) = available {
310            assert!(a > 0);
311        }
312    }
313
314    #[test]
315    fn test_temp_dir() {
316        let platform = UnixPlatform::new();
317        let temp = platform.temp_dir();
318        assert!(temp.exists());
319    }
320
321    #[test]
322    fn test_is_elevated() {
323        let platform = UnixPlatform::new();
324        // Just make sure it doesn't panic
325        let _ = platform.is_elevated();
326    }
327}