use std::path::Path; use std::process::Command; #[derive(Debug, Clone, PartialEq)] pub enum FuseStatus { /// libfuse2 available, fusermount present, /dev/fuse exists - fully working FullyFunctional, /// Only libfuse3 installed - most AppImages won't mount natively Fuse3Only, /// fusermount binary not found NoFusermount, /// /dev/fuse device not present (container or WSL) NoDevFuse, /// libfuse2 not installed MissingLibfuse2, } impl FuseStatus { pub fn as_str(&self) -> &'static str { match self { Self::FullyFunctional => "fully_functional", Self::Fuse3Only => "fuse3_only", Self::NoFusermount => "no_fusermount", Self::NoDevFuse => "no_dev_fuse", Self::MissingLibfuse2 => "missing_libfuse2", } } pub fn from_str(s: &str) -> Self { match s { "fully_functional" => Self::FullyFunctional, "fuse3_only" => Self::Fuse3Only, "no_fusermount" => Self::NoFusermount, "no_dev_fuse" => Self::NoDevFuse, _ => Self::MissingLibfuse2, } } pub fn label(&self) -> &'static str { match self { Self::FullyFunctional => "OK", Self::Fuse3Only => "FUSE3 only", Self::NoFusermount => "No fusermount", Self::NoDevFuse => "No /dev/fuse", Self::MissingLibfuse2 => "No libfuse2", } } pub fn badge_class(&self) -> &'static str { match self { Self::FullyFunctional => "success", Self::Fuse3Only => "warning", Self::NoFusermount | Self::NoDevFuse | Self::MissingLibfuse2 => "error", } } pub fn is_functional(&self) -> bool { matches!(self, Self::FullyFunctional) } } #[derive(Debug, Clone)] pub struct FuseSystemInfo { pub status: FuseStatus, pub has_libfuse2: bool, pub has_libfuse3: bool, pub has_fusermount: bool, pub fusermount_path: Option, pub has_dev_fuse: bool, pub install_hint: Option, } /// Detect the system FUSE status by checking for libraries, binaries, and device nodes. pub fn detect_system_fuse() -> FuseSystemInfo { let has_libfuse2 = check_library("libfuse.so.2"); let has_libfuse3 = check_library("libfuse3.so.3"); let fusermount_path = find_fusermount(); let has_fusermount = fusermount_path.is_some(); let has_dev_fuse = Path::new("/dev/fuse").exists(); let status = if has_libfuse2 && has_fusermount && has_dev_fuse { FuseStatus::FullyFunctional } else if !has_dev_fuse { FuseStatus::NoDevFuse } else if !has_fusermount { FuseStatus::NoFusermount } else if has_libfuse3 && !has_libfuse2 { FuseStatus::Fuse3Only } else { FuseStatus::MissingLibfuse2 }; let install_hint = if status.is_functional() { None } else { Some(get_install_hint()) }; FuseSystemInfo { status, has_libfuse2, has_libfuse3, has_fusermount, fusermount_path, has_dev_fuse, install_hint, } } /// Per-AppImage FUSE launch status #[derive(Debug, Clone, PartialEq)] pub enum AppImageFuseStatus { /// Will mount natively via FUSE NativeFuse, /// Uses new type2-runtime with static FUSE StaticRuntime, /// Will use extract-and-run fallback (slower startup) ExtractAndRun, /// Cannot launch at all CannotLaunch, } impl AppImageFuseStatus { pub fn as_str(&self) -> &'static str { match self { Self::NativeFuse => "native_fuse", Self::StaticRuntime => "static_runtime", Self::ExtractAndRun => "extract_and_run", Self::CannotLaunch => "cannot_launch", } } pub fn label(&self) -> &'static str { match self { Self::NativeFuse => "Native FUSE", Self::StaticRuntime => "Static runtime", Self::ExtractAndRun => "Extract & Run", Self::CannotLaunch => "Cannot launch", } } pub fn badge_class(&self) -> &'static str { match self { Self::NativeFuse | Self::StaticRuntime => "success", Self::ExtractAndRun => "warning", Self::CannotLaunch => "error", } } } /// Determine launch status for a specific AppImage given system FUSE state. pub fn determine_app_fuse_status( system: &FuseSystemInfo, appimage_path: &Path, ) -> AppImageFuseStatus { // Check if the AppImage uses the new static runtime if has_static_runtime(appimage_path) { return AppImageFuseStatus::StaticRuntime; } if system.status.is_functional() { return AppImageFuseStatus::NativeFuse; } // FUSE not fully functional - check if extract-and-run works if supports_extract_and_run(appimage_path) { AppImageFuseStatus::ExtractAndRun } else { AppImageFuseStatus::CannotLaunch } } /// Check if the AppImage uses the new type2-runtime with statically linked FUSE. /// Scans the first 256KB of the binary for runtime signatures instead of executing /// the AppImage (which can hang for apps with custom AppRun scripts like Affinity). fn has_static_runtime(appimage_path: &Path) -> bool { use std::io::Read; let mut file = match std::fs::File::open(appimage_path) { Ok(f) => f, Err(_) => return false, }; // The runtime signature is in the ELF binary header area, well within first 256KB let mut buf = vec![0u8; 256 * 1024]; let n = match file.read(&mut buf) { Ok(n) => n, Err(_) => return false, }; let data = &buf[..n]; let haystack = String::from_utf8_lossy(data).to_lowercase(); haystack.contains("type2-runtime") || haystack.contains("libfuse3") } /// Check if --appimage-extract-and-run is supported. fn supports_extract_and_run(appimage_path: &Path) -> bool { // Check for AppImage Type 2 magic at offset 8 - only need to read 12 bytes use std::io::Read; let mut file = match std::fs::File::open(appimage_path) { Ok(f) => f, Err(_) => return false, }; let mut header = [0u8; 12]; if file.read_exact(&mut header).is_ok() { return header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02; } false } /// Check if a shared library is available on the system via ldconfig. fn check_library(soname: &str) -> bool { let output = Command::new("ldconfig") .arg("-p") .output(); if let Ok(output) = output { let stdout = String::from_utf8_lossy(&output.stdout); return stdout.contains(soname); } false } /// Find fusermount or fusermount3 binary. fn find_fusermount() -> Option { for name in &["fusermount", "fusermount3"] { let output = Command::new("which") .arg(name) .output(); if let Ok(output) = output { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !path.is_empty() { return Some(path); } } } } None } /// Detect distro and return the appropriate libfuse2 install command. fn get_install_hint() -> String { if let Ok(content) = std::fs::read_to_string("/etc/os-release") { let id = extract_os_field(&content, "ID"); let version_id = extract_os_field(&content, "VERSION_ID"); let id_like = extract_os_field(&content, "ID_LIKE"); return match id.as_deref() { Some("ubuntu") => { let ver: f64 = version_id .as_deref() .and_then(|v| v.parse().ok()) .unwrap_or(0.0); if ver >= 24.04 { "sudo apt install libfuse2t64".to_string() } else { "sudo apt install libfuse2".to_string() } } Some("debian") => "sudo apt install libfuse2".to_string(), Some("fedora") => "sudo dnf install fuse-libs".to_string(), Some("arch") | Some("manjaro") | Some("endeavouros") => { "sudo pacman -S fuse2".to_string() } Some("opensuse-tumbleweed") | Some("opensuse-leap") => { "sudo zypper install libfuse2".to_string() } _ => { // Check ID_LIKE for derivatives if let Some(like) = id_like.as_deref() { if like.contains("ubuntu") || like.contains("debian") { return "sudo apt install libfuse2".to_string(); } if like.contains("fedora") { return "sudo dnf install fuse-libs".to_string(); } if like.contains("arch") { return "sudo pacman -S fuse2".to_string(); } if like.contains("suse") { return "sudo zypper install libfuse2".to_string(); } } "Install libfuse2 using your distribution's package manager".to_string() } }; } "Install libfuse2 using your distribution's package manager".to_string() } fn extract_os_field(content: &str, key: &str) -> Option { for line in content.lines() { if let Some(rest) = line.strip_prefix(&format!("{}=", key)) { return Some(rest.trim_matches('"').to_string()); } } None } /// Check if AppImageLauncher is installed (known conflicts with new runtime). pub fn detect_appimagelauncher() -> Option { let output = Command::new("dpkg") .args(["-s", "appimagelauncher"]) .output(); if let Ok(output) = output { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { if let Some(ver) = line.strip_prefix("Version: ") { return Some(ver.trim().to_string()); } } return Some("unknown".to_string()); } } None } #[cfg(test)] mod tests { use super::*; #[test] fn test_fuse_status_roundtrip() { let statuses = [ FuseStatus::FullyFunctional, FuseStatus::Fuse3Only, FuseStatus::NoFusermount, FuseStatus::NoDevFuse, FuseStatus::MissingLibfuse2, ]; for status in &statuses { assert_eq!(&FuseStatus::from_str(status.as_str()), status); } } #[test] fn test_extract_os_field() { let content = r#"NAME="Ubuntu" VERSION_ID="24.04" ID=ubuntu ID_LIKE=debian "#; assert_eq!(extract_os_field(content, "ID"), Some("ubuntu".to_string())); assert_eq!(extract_os_field(content, "VERSION_ID"), Some("24.04".to_string())); assert_eq!(extract_os_field(content, "ID_LIKE"), Some("debian".to_string())); assert_eq!(extract_os_field(content, "MISSING"), None); } #[test] fn test_fuse_status_badges() { assert_eq!(FuseStatus::FullyFunctional.badge_class(), "success"); assert_eq!(FuseStatus::Fuse3Only.badge_class(), "warning"); assert_eq!(FuseStatus::MissingLibfuse2.badge_class(), "error"); } }