use std::fs; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use super::database::Database; /// Information about an AppImage's runtime binary. #[derive(Debug, Clone)] pub struct RuntimeInfo { pub runtime_size: u64, pub payload_offset: u64, pub runtime_type: RuntimeType, pub runtime_version: Option, } /// The type of AppImage runtime. #[derive(Debug, Clone, PartialEq)] pub enum RuntimeType { OldFuse2, NewMulti, Static, Unknown, } impl RuntimeType { pub fn as_str(&self) -> &str { match self { Self::OldFuse2 => "old-fuse2", Self::NewMulti => "new-multi", Self::Static => "static", Self::Unknown => "unknown", } } pub fn label(&self) -> &str { match self { Self::OldFuse2 => "Legacy FUSE 2 only", Self::NewMulti => "Multi-runtime (FUSE 2/3 + static)", Self::Static => "Static (no FUSE needed)", Self::Unknown => "Unknown runtime", } } } /// Result of a runtime replacement operation. #[derive(Debug)] pub struct RepackageResult { pub original_path: PathBuf, pub backup_path: PathBuf, pub old_runtime_type: RuntimeType, pub new_runtime_type: String, pub old_size: u64, pub new_size: u64, pub success: bool, } /// Detect the runtime type and payload offset of an AppImage. /// Type 2 AppImages store the SquashFS offset in the ELF section header. pub fn detect_runtime(appimage_path: &Path) -> Result { let mut file = fs::File::open(appimage_path) .map_err(|e| RepackageError::Io(e.to_string()))?; // Read ELF header to find section headers let mut header = [0u8; 64]; file.read_exact(&mut header) .map_err(|e| RepackageError::Io(e.to_string()))?; // Verify ELF magic if &header[0..4] != b"\x7fELF" { return Err(RepackageError::NotAppImage("Not an ELF file".to_string())); } // Find the SquashFS payload by searching for the magic bytes let payload_offset = find_squashfs_offset(appimage_path)?; let runtime_size = payload_offset; // Classify the runtime type based on size and content let runtime_type = classify_runtime(appimage_path, runtime_size)?; Ok(RuntimeInfo { runtime_size, payload_offset, runtime_type, runtime_version: None, }) } /// Find the offset where the SquashFS payload starts. /// SquashFS magic is 'hsqs' (0x73717368) at the start of the payload. fn find_squashfs_offset(appimage_path: &Path) -> Result { let mut file = fs::File::open(appimage_path) .map_err(|e| RepackageError::Io(e.to_string()))?; let file_size = file.metadata() .map(|m| m.len()) .map_err(|e| RepackageError::Io(e.to_string()))?; // SquashFS magic: 'hsqs' = [0x68, 0x73, 0x71, 0x73] let magic = b"hsqs"; // Search in chunks starting from reasonable offsets (runtime is typically 100-300KB) let mut buf = [0u8; 65536]; let search_start = 4096u64; // Skip the ELF header let search_end = std::cmp::min(file_size, 1_048_576); // Don't search beyond 1MB let mut offset = search_start; use std::io::Seek; file.seek(std::io::SeekFrom::Start(offset)) .map_err(|e| RepackageError::Io(e.to_string()))?; while offset < search_end { let n = file.read(&mut buf) .map_err(|e| RepackageError::Io(e.to_string()))?; if n == 0 { break; } // Search for magic in this chunk for i in 0..n.saturating_sub(3) { if &buf[i..i + 4] == magic { return Ok(offset + i as u64); } } offset += n as u64 - 3; // Overlap by 3 to catch magic spanning chunks file.seek(std::io::SeekFrom::Start(offset)) .map_err(|e| RepackageError::Io(e.to_string()))?; } Err(RepackageError::NotAppImage("SquashFS payload not found".to_string())) } /// Classify the runtime type based on its binary content. fn classify_runtime(appimage_path: &Path, runtime_size: u64) -> Result { let mut file = fs::File::open(appimage_path) .map_err(|e| RepackageError::Io(e.to_string()))?; let read_size = std::cmp::min(runtime_size, 65536) as usize; let mut buf = vec![0u8; read_size]; file.read_exact(&mut buf) .map_err(|e| RepackageError::Io(e.to_string()))?; let content = String::from_utf8_lossy(&buf); // Check for known strings in the runtime binary if content.contains("libfuse3") || content.contains("fuse3") { Ok(RuntimeType::NewMulti) } else if content.contains("static-runtime") || content.contains("no-fuse") { Ok(RuntimeType::Static) } else if content.contains("libfuse") || content.contains("fuse2") { Ok(RuntimeType::OldFuse2) } else if runtime_size < 4096 { // Suspiciously small runtime - probably not a valid AppImage runtime Ok(RuntimeType::Unknown) } else { // Default: older runtimes are typically fuse2-only Ok(RuntimeType::OldFuse2) } } /// Replace the runtime of an AppImage with a new one. /// Creates a backup of the original file before modifying. pub fn replace_runtime( appimage_path: &Path, new_runtime_path: &Path, keep_backup: bool, ) -> Result { if !appimage_path.exists() { return Err(RepackageError::NotAppImage("File not found".to_string())); } if !new_runtime_path.exists() { return Err(RepackageError::Io("New runtime file not found".to_string())); } let info = detect_runtime(appimage_path)?; let old_size = fs::metadata(appimage_path) .map(|m| m.len()) .map_err(|e| RepackageError::Io(e.to_string()))?; // Create backup let backup_path = appimage_path.with_extension("bak"); fs::copy(appimage_path, &backup_path) .map_err(|e| RepackageError::Io(format!("Backup failed: {}", e)))?; // Read new runtime let new_runtime = fs::read(new_runtime_path) .map_err(|e| RepackageError::Io(format!("Failed to read new runtime: {}", e)))?; // Read the SquashFS payload from the original file let mut original = fs::File::open(appimage_path) .map_err(|e| RepackageError::Io(e.to_string()))?; use std::io::Seek; original.seek(std::io::SeekFrom::Start(info.payload_offset)) .map_err(|e| RepackageError::Io(e.to_string()))?; let mut payload = Vec::new(); original.read_to_end(&mut payload) .map_err(|e| RepackageError::Io(e.to_string()))?; drop(original); // Write new AppImage: new_runtime + payload let mut output = fs::File::create(appimage_path) .map_err(|e| RepackageError::Io(e.to_string()))?; output.write_all(&new_runtime) .map_err(|e| RepackageError::Io(e.to_string()))?; output.write_all(&payload) .map_err(|e| RepackageError::Io(e.to_string()))?; // Set executable permission #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = fs::Permissions::from_mode(0o755); fs::set_permissions(appimage_path, perms).ok(); } let new_size = fs::metadata(appimage_path) .map(|m| m.len()) .unwrap_or(0); // Verify the new file is a valid AppImage let success = verify_appimage(appimage_path); if !success { // Rollback from backup log::error!("Verification failed, rolling back from backup"); fs::copy(&backup_path, appimage_path).ok(); if !keep_backup { fs::remove_file(&backup_path).ok(); } return Err(RepackageError::VerificationFailed); } if !keep_backup { fs::remove_file(&backup_path).ok(); } Ok(RepackageResult { original_path: appimage_path.to_path_buf(), backup_path, old_runtime_type: info.runtime_type, new_runtime_type: "new".to_string(), old_size, new_size, success: true, }) } /// Batch-replace runtimes for all AppImages in the database that use the old runtime. pub fn batch_replace_runtimes( db: &Database, new_runtime_path: &Path, dry_run: bool, ) -> Vec { let records = db.get_all_appimages().unwrap_or_default(); let mut results = Vec::new(); for record in &records { let path = Path::new(&record.path); if !path.exists() { continue; } let info = match detect_runtime(path) { Ok(i) => i, Err(e) => { log::warn!("Skipping {}: {}", record.filename, e); continue; } }; // Only repackage old fuse2 runtimes if info.runtime_type != RuntimeType::OldFuse2 { continue; } if dry_run { results.push(RepackageResult { original_path: path.to_path_buf(), backup_path: path.with_extension("bak"), old_runtime_type: info.runtime_type, new_runtime_type: "new".to_string(), old_size: fs::metadata(path).map(|m| m.len()).unwrap_or(0), new_size: 0, success: true, }); continue; } match replace_runtime(path, new_runtime_path, true) { Ok(result) => { // Record in database db.record_runtime_update( record.id, Some(info.runtime_type.as_str()), Some("new"), result.backup_path.to_str(), true, ).ok(); results.push(result); } Err(e) => { log::error!("Failed to repackage {}: {}", record.filename, e); db.record_runtime_update( record.id, Some(info.runtime_type.as_str()), Some("new"), None, false, ).ok(); } } } results } /// Download the latest AppImage runtime binary. pub fn download_latest_runtime() -> Result { let url = "https://github.com/AppImage/type2-runtime/releases/latest/download/runtime-x86_64"; let dest = dirs::cache_dir() .unwrap_or_else(|| PathBuf::from("/tmp")) .join("driftwood") .join("runtime-x86_64"); fs::create_dir_all(dest.parent().unwrap()).ok(); let response = ureq::get(url) .call() .map_err(|e| RepackageError::Network(e.to_string()))?; let mut file = fs::File::create(&dest) .map_err(|e| RepackageError::Io(e.to_string()))?; let mut reader = response.into_body().into_reader(); let mut buf = [0u8; 65536]; loop { let n = reader.read(&mut buf) .map_err(|e| RepackageError::Network(e.to_string()))?; if n == 0 { break; } file.write_all(&buf[..n]) .map_err(|e| RepackageError::Io(e.to_string()))?; } #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&dest, fs::Permissions::from_mode(0o755)).ok(); } Ok(dest) } /// Basic verification that a file is still a valid AppImage. fn verify_appimage(path: &Path) -> bool { // Check ELF magic let mut file = match fs::File::open(path) { Ok(f) => f, Err(_) => return false, }; let mut magic = [0u8; 4]; if file.read_exact(&mut magic).is_err() { return false; } if &magic != b"\x7fELF" { return false; } // Check that SquashFS payload exists find_squashfs_offset(path).is_ok() } // --- Error types --- #[derive(Debug)] pub enum RepackageError { NotAppImage(String), Io(String), Network(String), VerificationFailed, } impl std::fmt::Display for RepackageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NotAppImage(e) => write!(f, "Not a valid AppImage: {}", e), Self::Io(e) => write!(f, "I/O error: {}", e), Self::Network(e) => write!(f, "Network error: {}", e), Self::VerificationFailed => write!(f, "Verification failed after repackaging"), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_runtime_type_as_str() { assert_eq!(RuntimeType::OldFuse2.as_str(), "old-fuse2"); assert_eq!(RuntimeType::NewMulti.as_str(), "new-multi"); assert_eq!(RuntimeType::Static.as_str(), "static"); assert_eq!(RuntimeType::Unknown.as_str(), "unknown"); } #[test] fn test_runtime_type_label() { assert!(RuntimeType::OldFuse2.label().contains("Legacy")); assert!(RuntimeType::NewMulti.label().contains("Multi")); assert!(RuntimeType::Static.label().contains("no FUSE")); } #[test] fn test_repackage_error_display() { let err = RepackageError::NotAppImage("bad magic".to_string()); assert!(format!("{}", err).contains("bad magic")); let err = RepackageError::VerificationFailed; assert!(format!("{}", err).contains("Verification failed")); } #[test] fn test_detect_runtime_nonexistent() { let result = detect_runtime(Path::new("/nonexistent.AppImage")); assert!(result.is_err()); } #[test] fn test_detect_runtime_not_elf() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("not-an-elf"); fs::write(&path, "This is not an ELF file").unwrap(); let result = detect_runtime(&path); assert!(result.is_err()); } #[test] fn test_verify_appimage_nonexistent() { assert!(!verify_appimage(Path::new("/nonexistent"))); } #[test] fn test_verify_appimage_not_elf() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("not-elf"); fs::write(&path, "hello").unwrap(); assert!(!verify_appimage(&path)); } }