- fuse.rs: Replace Command::new(appimage_path) with 256KB binary scan for runtime detection - prevents apps like Affinity from launching on tile click - fuse.rs: Read only 12 bytes for Type 2 magic check instead of entire file - security.rs: Use find_squashfs_offset_for() instead of executing AppImages with --appimage-offset flag - updater.rs: Read only first 1MB for update info instead of entire file - detail_view.rs: Click screenshots to open in lightbox dialog - detail_view.rs: Fetch favicons from Google favicon service for link rows
357 lines
11 KiB
Rust
357 lines
11 KiB
Rust
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<String>,
|
|
pub has_dev_fuse: bool,
|
|
pub install_hint: Option<String>,
|
|
}
|
|
|
|
/// 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<String> {
|
|
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<String> {
|
|
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<String> {
|
|
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");
|
|
}
|
|
}
|