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
198 lines
5.6 KiB
Rust
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())
|
|
);
|
|
}
|
|
}
|