#![allow(dead_code, unused_imports, unused_variables, clippy::ptr_arg)]
use super::{UpdateInfo, UpgradeConfig, UpgradeError, UpgradeResult};
use base64::Engine;
use ring::signature::{self, UnparsedPublicKey};
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use sysinfo::{DiskExt, ProcessExt, System, SystemExt};
use tracing::{debug, info, warn};
const INFERNO_PUBLIC_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
pub struct SafetyChecker {
config: UpgradeConfig,
system: System,
}
#[derive(Debug, Clone)]
pub struct CompatibilityReport {
pub os_compatible: bool,
pub arch_compatible: bool,
pub version_compatible: bool,
pub dependencies_satisfied: bool,
pub issues: Vec<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ResourceReport {
pub disk_space_sufficient: bool,
pub memory_sufficient: bool,
pub cpu_load_acceptable: bool,
pub network_available: bool,
pub available_disk_mb: u64,
pub available_memory_mb: u64,
pub cpu_usage_percent: f32,
pub issues: Vec<String>,
}
impl SafetyChecker {
pub fn new(config: &UpgradeConfig) -> Self {
let mut system = System::new_all();
system.refresh_all();
Self {
config: config.clone(),
system,
}
}
pub async fn check_pre_installation(&mut self, update_info: &UpdateInfo) -> UpgradeResult<()> {
info!("Running pre-installation safety checks");
self.system.refresh_all();
if self.config.safety_checks.check_compatibility {
self.check_system_compatibility(update_info).await?;
}
if self.config.safety_checks.check_disk_space {
self.check_disk_space(update_info).await?;
}
if self.config.safety_checks.check_network {
self.check_network_connectivity().await?;
}
if self.config.safety_checks.check_running_processes {
self.check_running_processes().await?;
}
if self.config.safety_checks.check_dependencies {
self.check_system_dependencies().await?;
}
info!("All pre-installation safety checks passed");
Ok(())
}
pub async fn verify_package(
&self,
package_path: &PathBuf,
update_info: &UpdateInfo,
) -> UpgradeResult<()> {
info!("Verifying package integrity");
if !package_path.exists() {
return Err(UpgradeError::InvalidPackage(
"Package file not found".to_string(),
));
}
let file_size = std::fs::metadata(package_path)
.map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?
.len();
let platform = std::env::consts::OS;
if let Some(expected_size) = update_info.size_bytes.get(platform) {
if file_size != *expected_size {
return Err(UpgradeError::VerificationFailed(format!(
"File size mismatch: expected {} bytes, got {} bytes",
expected_size, file_size
)));
}
}
self.verify_package_format(package_path).await?;
if self.config.require_signatures {
self.verify_package_signature(package_path, update_info)
.await?;
}
if self.is_malware_scanning_available() {
self.scan_for_malware(package_path).await?;
}
info!("Package verification completed successfully");
Ok(())
}
async fn check_system_compatibility(&self, update_info: &UpdateInfo) -> UpgradeResult<()> {
debug!("Checking system compatibility");
let current_os = std::env::consts::OS;
if !update_info.download_urls.contains_key(current_os) {
return Err(UpgradeError::PlatformNotSupported(current_os.to_string()));
}
let current_arch = std::env::consts::ARCH;
debug!("Current architecture: {}", current_arch);
if let Some(min_version) = &update_info.minimum_version {
let current_version = super::ApplicationVersion::current();
if !current_version.is_compatible_with(min_version) {
return Err(UpgradeError::VerificationFailed(format!(
"Current version {} is not compatible with minimum required version {}",
current_version.to_string(),
min_version.to_string()
)));
}
}
self.check_os_version_compatibility()?;
Ok(())
}
async fn check_disk_space(&self, update_info: &UpdateInfo) -> UpgradeResult<()> {
debug!("Checking disk space");
let platform = std::env::consts::OS;
let package_size = update_info.size_bytes.get(platform).copied().unwrap_or(0);
let required_space =
package_size * 3 + (self.config.safety_checks.min_free_space_mb * 1024 * 1024);
let available_space = self.get_available_disk_space(&self.config.download_dir)?;
if available_space < required_space {
return Err(UpgradeError::InsufficientDiskSpace {
required: required_space / 1024 / 1024,
available: available_space / 1024 / 1024,
});
}
debug!(
"Disk space check passed: {} MB available, {} MB required",
available_space / 1024 / 1024,
required_space / 1024 / 1024
);
Ok(())
}
async fn check_network_connectivity(&self) -> UpgradeResult<()> {
debug!("Checking network connectivity");
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
tokio::net::lookup_host("api.github.com:443"),
)
.await;
match result {
Ok(Ok(_)) => {
debug!("Network connectivity check passed");
Ok(())
}
Ok(Err(e)) => Err(UpgradeError::NetworkError(format!(
"DNS resolution failed: {}",
e
))),
Err(_) => Err(UpgradeError::NetworkError(
"Network connectivity timeout".to_string(),
)),
}
}
async fn check_running_processes(&self) -> UpgradeResult<()> {
debug!("Checking running processes");
let dangerous_processes = vec!["antivirus", "scanner", "backup", "sync", "cloud"];
for process in self.system.processes().values() {
let process_name = process.name().to_lowercase();
for dangerous in &dangerous_processes {
if process_name.contains(dangerous) {
warn!(
"Potentially interfering process detected: {}",
process.name()
);
}
}
}
let current_exe =
std::env::current_exe().map_err(|e| UpgradeError::Internal(e.to_string()))?;
let current_name = current_exe
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("inferno");
let mut running_instances = 0;
for (pid, process) in self.system.processes() {
if process.name().to_lowercase().contains("inferno")
&& *pid != sysinfo::get_current_pid().unwrap()
{
running_instances += 1;
}
}
if running_instances > 0 {
warn!(
"Found {} other running instances of the application",
running_instances
);
}
Ok(())
}
async fn check_system_dependencies(&self) -> UpgradeResult<()> {
debug!("Checking system dependencies");
#[cfg(target_os = "macos")]
{
self.check_macos_dependencies()?;
}
#[cfg(target_os = "linux")]
{
self.check_linux_dependencies()?;
}
#[cfg(target_os = "windows")]
{
self.check_windows_dependencies()?;
}
Ok(())
}
async fn verify_package_format(&self, package_path: &PathBuf) -> UpgradeResult<()> {
let extension = package_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
match extension.to_lowercase().as_str() {
"tar" | "tgz" | "tar.gz" => self.verify_tar_format(package_path),
"zip" => self.verify_zip_format(package_path),
"pkg" => self.verify_pkg_format(package_path),
"msi" | "exe" => self.verify_windows_format(package_path),
"deb" => self.verify_deb_format(package_path),
"rpm" => self.verify_rpm_format(package_path),
_ => Err(UpgradeError::InvalidPackage(format!(
"Unsupported package format: {}",
extension
))),
}
}
async fn verify_package_signature(
&self,
package_path: &PathBuf,
update_info: &UpdateInfo,
) -> UpgradeResult<()> {
debug!("Verifying package digital signature");
let platform = std::env::consts::OS;
if let Some(signature_b64) = update_info.signatures.get(platform) {
if signature_b64.is_empty() {
return Err(UpgradeError::VerificationFailed(
"No signature provided".to_string(),
));
}
let signature_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, signature_b64)
.map_err(|e| {
UpgradeError::VerificationFailed(format!("Invalid signature encoding: {}", e))
})?;
let mut file = std::fs::File::open(package_path).map_err(|e| {
UpgradeError::VerificationFailed(format!("Cannot open package: {}", e))
})?;
let mut package_bytes = Vec::new();
file.read_to_end(&mut package_bytes).map_err(|e| {
UpgradeError::VerificationFailed(format!("Cannot read package: {}", e))
})?;
let public_key = UnparsedPublicKey::new(&signature::ED25519, INFERNO_PUBLIC_KEY);
public_key
.verify(&package_bytes, &signature_bytes)
.map_err(|_| {
UpgradeError::VerificationFailed(
"Signature verification failed - package may be tampered".to_string(),
)
})?;
info!("Package signature verified successfully");
} else if self.config.require_signatures {
return Err(UpgradeError::VerificationFailed(
"Signature required but not provided".to_string(),
));
}
Ok(())
}
async fn scan_for_malware(&self, package_path: &PathBuf) -> UpgradeResult<()> {
debug!("Scanning package for malware");
#[cfg(target_os = "windows")]
{
if let Ok(output) = Command::new("powershell")
.args(&[
"-Command",
"Get-MpComputerStatus | Select-Object RealTimeProtectionEnabled",
])
.output()
{
if output.status.success() {
debug!("Windows Defender is available for scanning");
}
}
}
Ok(())
}
fn is_malware_scanning_available(&self) -> bool {
#[cfg(target_os = "windows")]
{
Command::new("powershell")
.args(&["-Command", "Get-MpComputerStatus"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
{
Command::new("clamscan")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
{
false
}
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
false
}
}
fn get_available_disk_space(&self, path: &PathBuf) -> UpgradeResult<u64> {
for disk in self.system.disks() {
if path.starts_with(disk.mount_point()) {
return Ok(disk.available_space());
}
}
if let Some(root_disk) = self.system.disks().first() {
Ok(root_disk.available_space())
} else {
Err(UpgradeError::Internal(
"Cannot determine available disk space".to_string(),
))
}
}
fn check_os_version_compatibility(&self) -> UpgradeResult<()> {
let os_version = self.system.os_version();
debug!("OS version: {:?}", os_version);
#[cfg(target_os = "macos")]
{
if let Some(version) = os_version {
if self.is_macos_version_too_old(&version) {
return Err(UpgradeError::PlatformNotSupported(format!(
"macOS version {} is too old. Minimum version required: 10.15",
version
)));
}
}
}
#[cfg(target_os = "linux")]
{
if let Some(version) = os_version {
debug!("Linux version: {}", version);
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn check_macos_dependencies(&self) -> UpgradeResult<()> {
debug!("Checking macOS dependencies");
Ok(())
}
#[cfg(target_os = "linux")]
fn check_linux_dependencies(&self) -> UpgradeResult<()> {
debug!("Checking Linux dependencies");
Ok(())
}
#[cfg(target_os = "windows")]
fn check_windows_dependencies(&self) -> UpgradeResult<()> {
debug!("Checking Windows dependencies");
Ok(())
}
fn verify_tar_format(&self, path: &PathBuf) -> UpgradeResult<()> {
Command::new("tar")
.args(["-tf", path.to_str().unwrap()])
.output()
.map_err(|e| UpgradeError::InvalidPackage(format!("Tar validation failed: {}", e)))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
Err(UpgradeError::InvalidPackage(
"Invalid tar file format".to_string(),
))
}
})
}
fn verify_zip_format(&self, path: &PathBuf) -> UpgradeResult<()> {
use std::fs::File;
let file = File::open(path).map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?;
use std::io::Read;
let mut magic = [0u8; 4];
let mut reader = file;
reader
.read_exact(&mut magic)
.map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?;
if &magic == b"PK\x03\x04" || &magic == b"PK\x05\x06" || &magic == b"PK\x07\x08" {
Ok(())
} else {
Err(UpgradeError::InvalidPackage(
"Invalid ZIP file format".to_string(),
))
}
}
fn verify_pkg_format(&self, _path: &PathBuf) -> UpgradeResult<()> {
Ok(())
}
fn verify_windows_format(&self, _path: &PathBuf) -> UpgradeResult<()> {
Ok(())
}
fn verify_deb_format(&self, path: &PathBuf) -> UpgradeResult<()> {
Command::new("dpkg")
.args(["--info", path.to_str().unwrap()])
.output()
.map_err(|e| UpgradeError::InvalidPackage(format!("DEB validation failed: {}", e)))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
Err(UpgradeError::InvalidPackage(
"Invalid DEB package format".to_string(),
))
}
})
}
fn verify_rpm_format(&self, path: &PathBuf) -> UpgradeResult<()> {
Command::new("rpm")
.args(["-qp", path.to_str().unwrap()])
.output()
.map_err(|e| UpgradeError::InvalidPackage(format!("RPM validation failed: {}", e)))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
Err(UpgradeError::InvalidPackage(
"Invalid RPM package format".to_string(),
))
}
})
}
#[cfg(target_os = "macos")]
fn is_macos_version_too_old(&self, version: &str) -> bool {
let major_version = version
.split('.')
.next()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
major_version < 10 || (major_version == 10 && self.get_macos_minor_version(version) < 15)
}
#[cfg(target_os = "macos")]
fn get_macos_minor_version(&self, version: &str) -> u32 {
version
.split('.')
.nth(1)
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_config() -> UpgradeConfig {
let temp_dir = TempDir::new().unwrap();
UpgradeConfig {
download_dir: temp_dir.path().to_path_buf(),
backup_dir: temp_dir.path().to_path_buf(),
..Default::default()
}
}
#[tokio::test]
async fn test_safety_checker_creation() {
let config = create_test_config();
let checker = SafetyChecker::new(&config);
assert!(checker.system.disks().len() > 0);
}
#[tokio::test]
async fn test_network_connectivity() {
let config = create_test_config();
let checker = SafetyChecker::new(&config);
let result = checker.check_network_connectivity().await;
if result.is_err() {
println!(
"Network connectivity test failed (expected in offline environments): {:?}",
result
);
}
}
#[test]
fn test_disk_space_calculation() {
let config = create_test_config();
let checker = SafetyChecker::new(&config);
let space = checker.get_available_disk_space(&config.download_dir);
assert!(space.is_ok());
assert!(space.unwrap() > 0);
}
}