diff --git a/Cargo.lock b/Cargo.lock index 95301be92..c7b441ee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -936,7 +936,7 @@ dependencies = [ [[package]] name = "ramshared-agent" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "ramshared-broker", "serde", @@ -945,14 +945,14 @@ dependencies = [ [[package]] name = "ramshared-block" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "ramshared-vram", ] [[package]] name = "ramshared-broker" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "serde", "serde_json", @@ -960,7 +960,7 @@ dependencies = [ [[package]] name = "ramshared-cli" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "libc", "ramshared-cuda", @@ -974,7 +974,7 @@ dependencies = [ [[package]] name = "ramshared-config" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "serde", "serde_path_to_error", @@ -983,7 +983,7 @@ dependencies = [ [[package]] name = "ramshared-cuda" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "cuda-async", "cuda-core", @@ -993,22 +993,22 @@ dependencies = [ [[package]] name = "ramshared-dxg" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "libc", ] [[package]] name = "ramshared-integrity" -version = "0.14.1" # x-release-please-version +version = "0.14.1" [[package]] name = "ramshared-tier" -version = "0.14.1" # x-release-please-version +version = "0.14.1" [[package]] name = "ramshared-uring" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "io-uring", "libc", @@ -1016,11 +1016,11 @@ dependencies = [ [[package]] name = "ramshared-vram" -version = "0.14.1" # x-release-please-version +version = "0.14.1" [[package]] name = "ramshared-vulkan" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "ash", "ramshared-vram", @@ -1028,7 +1028,7 @@ dependencies = [ [[package]] name = "ramshared-winbroker" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "ramshared-broker", "serde", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "ramshared-winsvc" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "base64", "ramshared-block", @@ -1059,7 +1059,7 @@ dependencies = [ [[package]] name = "ramshared-wsl2d" -version = "0.14.1" # x-release-please-version +version = "0.14.1" dependencies = [ "ramshared-block", "ramshared-broker", diff --git a/crates/ramshared-winsvc/src/host_safety.rs b/crates/ramshared-winsvc/src/host_safety.rs index aa0096ffd..209465854 100644 --- a/crates/ramshared-winsvc/src/host_safety.rs +++ b/crates/ramshared-winsvc/src/host_safety.rs @@ -103,6 +103,83 @@ impl CampaignVerdict { } } +/// Domain errors for host safety operations. +#[derive(Debug, PartialEq, Eq)] +pub enum HostSafetyError { + InsufficientMemory { available: u64, required: u64 }, + InsufficientDiskSpace { available: u64, required: u64 }, + DiskSpaceQueryFailed(String), + InvalidCpuLoad(String), + CpuLoadExceedsMaximum { current: String, maximum: String }, +} + +/// Abstract provider for system information. +pub trait SysInfoProvider { + fn available_memory(&self) -> u64; + fn available_disk_space(&self, path: &str) -> Result; + fn cpu_load_percent(&self) -> f32; +} + +/// Validates that available system memory meets the required threshold. +pub fn memory_threshold_guard( + sysinfo: &impl SysInfoProvider, + required_bytes: u64, +) -> Result<(), HostSafetyError> { + let available = sysinfo.available_memory(); + if available < required_bytes { + return Err(HostSafetyError::InsufficientMemory { + available, + required: required_bytes, + }); + } + Ok(()) +} + +/// Validates that available disk space on the specified path meets the required threshold. +pub fn disk_space_guard( + sysinfo: &impl SysInfoProvider, + path: &str, + required_bytes: u64, +) -> Result<(), HostSafetyError> { + let available = sysinfo + .available_disk_space(path) + .map_err(HostSafetyError::DiskSpaceQueryFailed)?; + if available < required_bytes { + return Err(HostSafetyError::InsufficientDiskSpace { + available, + required: required_bytes, + }); + } + Ok(()) +} + +/// Validates that the current CPU load does not exceed the maximum allowed percentage. +pub fn cpu_load_guard( + sysinfo: &impl SysInfoProvider, + max_load_percent: f32, +) -> Result<(), HostSafetyError> { + let current_load = sysinfo.cpu_load_percent(); + if current_load.is_nan() || current_load < 0.0 || current_load > 100.0 { + return Err(HostSafetyError::InvalidCpuLoad(format!( + "invalid cpu load: {}", + current_load + ))); + } + if max_load_percent.is_nan() || max_load_percent < 0.0 || max_load_percent > 100.0 { + return Err(HostSafetyError::InvalidCpuLoad(format!( + "invalid max cpu load: {}", + max_load_percent + ))); + } + if current_load > max_load_percent { + return Err(HostSafetyError::CpuLoadExceedsMaximum { + current: current_load.to_string(), + maximum: max_load_percent.to_string(), + }); + } + Ok(()) +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] @@ -208,4 +285,115 @@ mod tests { slow.teardown_ms = Some(30_001); assert!(!slow.is_pass(Duration::from_secs(30))); } + + struct MockSysInfo { + mem: u64, + disk: Result, + cpu: f32, + } + + impl MockSysInfo { + fn new(mem: u64, disk: Result, cpu: f32) -> Self { + Self { mem, disk, cpu } + } + } + + impl SysInfoProvider for MockSysInfo { + fn available_memory(&self) -> u64 { + self.mem + } + + fn available_disk_space(&self, _path: &str) -> Result { + self.disk.clone() + } + + fn cpu_load_percent(&self) -> f32 { + self.cpu + } + } + + #[test] + fn test_host_safety_memory_threshold_guard_success() { + let sysinfo = MockSysInfo::new(1024, Ok(0), 0.0); + assert!(memory_threshold_guard(&sysinfo, 512).is_ok()); + assert!(memory_threshold_guard(&sysinfo, 1024).is_ok()); + } + + #[test] + fn test_host_safety_memory_threshold_guard_failure() { + let sysinfo = MockSysInfo::new(512, Ok(0), 0.0); + assert!(memory_threshold_guard(&sysinfo, 1024).is_err()); + } + + #[test] + fn test_host_safety_memory_threshold_guard_zero_and_max() { + let sysinfo_zero = MockSysInfo::new(0, Ok(0), 0.0); + assert!(memory_threshold_guard(&sysinfo_zero, 0).is_ok()); + assert!(memory_threshold_guard(&sysinfo_zero, 1).is_err()); + + let sysinfo_max = MockSysInfo::new(u64::MAX, Ok(0), 0.0); + assert!(memory_threshold_guard(&sysinfo_max, u64::MAX).is_ok()); + } + + #[test] + fn test_host_safety_disk_space_guard_success() { + let sysinfo = MockSysInfo::new(0, Ok(2048), 0.0); + assert!(disk_space_guard(&sysinfo, "C:\\\\\\\\", 1024).is_ok()); + assert!(disk_space_guard(&sysinfo, "C:\\\\\\\\", 2048).is_ok()); + } + + #[test] + fn test_host_safety_disk_space_guard_failure() { + let sysinfo = MockSysInfo::new(0, Ok(1024), 0.0); + assert!(disk_space_guard(&sysinfo, "C:\\\\\\\\", 2048).is_err()); + } + + #[test] + fn test_host_safety_disk_space_guard_sysinfo_error() { + let sysinfo = MockSysInfo::new(0, Err(String::from("access denied")), 0.0); + assert!(disk_space_guard(&sysinfo, "C:\\\\\\\\", 1024).is_err()); + } + + #[test] + fn test_host_safety_disk_space_guard_zero_and_max() { + let sysinfo_zero = MockSysInfo::new(0, Ok(0), 0.0); + assert!(disk_space_guard(&sysinfo_zero, "C:\\\\\\\\", 0).is_ok()); + assert!(disk_space_guard(&sysinfo_zero, "C:\\\\\\\\", 1).is_err()); + + let sysinfo_max = MockSysInfo::new(0, Ok(u64::MAX), 0.0); + assert!(disk_space_guard(&sysinfo_max, "C:\\\\\\\\", u64::MAX).is_ok()); + } + + #[test] + fn test_host_safety_cpu_load_guard_success() { + let sysinfo = MockSysInfo::new(0, Ok(0), 50.0); + assert!(cpu_load_guard(&sysinfo, 80.0).is_ok()); + assert!(cpu_load_guard(&sysinfo, 50.0).is_ok()); + } + + #[test] + fn test_host_safety_cpu_load_guard_failure() { + let sysinfo = MockSysInfo::new(0, Ok(0), 90.0); + assert!(cpu_load_guard(&sysinfo, 80.0).is_err()); + } + + #[test] + fn test_host_safety_cpu_load_guard_invalid_current_load() { + let sysinfo_neg = MockSysInfo::new(0, Ok(0), -1.0); + assert!(cpu_load_guard(&sysinfo_neg, 80.0).is_err()); + + let sysinfo_over = MockSysInfo::new(0, Ok(0), 101.0); + assert!(cpu_load_guard(&sysinfo_over, 80.0).is_err()); + + let sysinfo_nan = MockSysInfo::new(0, Ok(0), f32::NAN); + assert!(cpu_load_guard(&sysinfo_nan, 80.0).is_err()); + } + + #[test] + fn test_host_safety_cpu_load_guard_invalid_max_load() { + let sysinfo = MockSysInfo::new(0, Ok(0), 50.0); + assert!(cpu_load_guard(&sysinfo, -1.0).is_err()); + assert!(cpu_load_guard(&sysinfo, 101.0).is_err()); + assert!(cpu_load_guard(&sysinfo, f32::NAN).is_err()); + } }