Files
driftwood/src/core/orphan.rs
lashman 830c3cad9d Fix second audit findings and restore crash detection dialog
Address 29 issues found in comprehensive API/spec audit:
- Fix .desktop Exec key path escaping per Desktop Entry spec
- Fix update dialog double-dispatch with connect_response
- Fix version comparison total ordering with lexicographic fallback
- Use RETURNING id for reliable upsert in database
- Replace tilde-based path fallbacks with proper XDG helpers
- Fix backup create/restore path asymmetry for non-home paths
- HTML-escape severity class in security reports
- Use AppStream <custom> element instead of <metadata>
- Fix has_appimage_update_tool to check .is_ok() not .success()
- Use ListBoxRow instead of ActionRow::set_child in ExpanderRow
- Add ELF magic validation to architecture detection
- Add timeout to extract_update_info_runtime
- Skip symlinks in dir_size calculation
- Use Condvar instead of busy-wait in analysis thread pool
- Restore crash detection to single blocking call architecture
2026-02-27 22:48:43 +02:00

198 lines
5.6 KiB
Rust

use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct OrphanedDesktopEntry {
pub desktop_file_path: PathBuf,
pub original_appimage_path: String,
pub app_name: Option<String>,
}
pub struct CleanupSummary {
pub entries_removed: usize,
pub icons_removed: usize,
}
fn applications_dir() -> PathBuf {
crate::config::data_dir_fallback()
.join("applications")
}
fn icons_dir() -> PathBuf {
crate::config::data_dir_fallback()
.join("icons/hicolor")
}
/// Parse key-value pairs from a .desktop file's [Desktop Entry] section.
fn parse_desktop_key(content: &str, key: &str) -> Option<String> {
let mut in_section = false;
for line in content.lines() {
let line = line.trim();
if line == "[Desktop Entry]" {
in_section = true;
continue;
}
if line.starts_with('[') {
in_section = false;
continue;
}
if !in_section {
continue;
}
if let Some(value) = line.strip_prefix(key).and_then(|rest| rest.strip_prefix('=')) {
return Some(value.trim().to_string());
}
}
None
}
/// Scan for orphaned desktop entries managed by Driftwood.
pub fn detect_orphans() -> Vec<OrphanedDesktopEntry> {
let mut orphans = Vec::new();
let apps_dir = applications_dir();
let entries = match fs::read_dir(&apps_dir) {
Ok(entries) => entries,
Err(_) => return orphans,
};
for entry in entries.flatten() {
let path = entry.path();
// Only check driftwood-*.desktop files
let _filename = match path.file_name().and_then(|n| n.to_str()) {
Some(name) if name.starts_with("driftwood-") && name.ends_with(".desktop") => name,
_ => continue,
};
// Read and check if managed by Driftwood
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let managed = parse_desktop_key(&content, "X-AppImage-Managed-By");
if managed.as_deref() != Some("Driftwood") {
continue;
}
// Check if the referenced AppImage still exists
let appimage_path = match parse_desktop_key(&content, "X-AppImage-Path") {
Some(p) => p,
None => continue,
};
if !Path::new(&appimage_path).exists() {
let app_name = parse_desktop_key(&content, "Name");
orphans.push(OrphanedDesktopEntry {
desktop_file_path: path,
original_appimage_path: appimage_path,
app_name,
});
}
}
orphans
}
/// Clean up a specific orphaned desktop entry.
pub fn clean_orphan(entry: &OrphanedDesktopEntry) -> Result<(bool, usize), std::io::Error> {
let mut icons_removed = 0;
// Remove the .desktop file
let entry_removed = if entry.desktop_file_path.exists() {
fs::remove_file(&entry.desktop_file_path)?;
true
} else {
false
};
// Try to determine the icon ID and remove associated icon files
if let Some(filename) = entry.desktop_file_path.file_stem().and_then(|n| n.to_str()) {
// filename is like "driftwood-firefox" - the icon ID is the same
let icon_id = filename;
let base = icons_dir();
let candidates = [
base.join(format!("256x256/apps/{}.png", icon_id)),
base.join(format!("scalable/apps/{}.svg", icon_id)),
base.join(format!("128x128/apps/{}.png", icon_id)),
base.join(format!("48x48/apps/{}.png", icon_id)),
];
for path in &candidates {
if path.exists() {
fs::remove_file(path)?;
icons_removed += 1;
}
}
}
Ok((entry_removed, icons_removed))
}
/// Clean all detected orphans.
pub fn clean_all_orphans() -> Result<CleanupSummary, std::io::Error> {
let orphans = detect_orphans();
let mut summary = CleanupSummary {
entries_removed: 0,
icons_removed: 0,
};
for entry in &orphans {
match clean_orphan(entry) {
Ok((removed, icons)) => {
if removed {
summary.entries_removed += 1;
}
summary.icons_removed += icons;
}
Err(e) => {
log::warn!(
"Failed to clean orphan {}: {}",
entry.desktop_file_path.display(),
e
);
}
}
}
Ok(summary)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_desktop_key() {
let content = "[Desktop Entry]\n\
Name=Test App\n\
X-AppImage-Path=/home/user/test.AppImage\n\
X-AppImage-Managed-By=Driftwood\n";
assert_eq!(
parse_desktop_key(content, "Name"),
Some("Test App".to_string())
);
assert_eq!(
parse_desktop_key(content, "X-AppImage-Path"),
Some("/home/user/test.AppImage".to_string())
);
assert_eq!(
parse_desktop_key(content, "X-AppImage-Managed-By"),
Some("Driftwood".to_string())
);
assert_eq!(parse_desktop_key(content, "Missing"), None);
}
#[test]
fn test_parse_desktop_key_ignores_other_sections() {
let content = "[Desktop Entry]\n\
Name=App\n\
[Desktop Action New]\n\
Name=Other\n";
assert_eq!(
parse_desktop_key(content, "Name"),
Some("App".to_string())
);
}
}