diff --git a/src/core/fuse.rs b/src/core/fuse.rs index b9a6830..e66a762 100644 --- a/src/core/fuse.rs +++ b/src/core/fuse.rs @@ -171,36 +171,37 @@ pub fn determine_app_fuse_status( } /// Check if the AppImage uses the new type2-runtime with statically linked FUSE. -/// The new runtime embeds FUSE support and doesn't need system libfuse. +/// 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 { - // The new type2-runtime responds to --appimage-version with a version string - // containing "type2-runtime" or a recent date - let output = Command::new(appimage_path) - .arg("--appimage-version") - .env("APPIMAGE_EXTRACT_AND_RUN", "1") - .output(); - - if let Ok(output) = output { - let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase(); - let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase(); - let combined = format!("{}{}", stdout, stderr); - // New runtime identifies itself - return combined.contains("type2-runtime") - || combined.contains("static") - || combined.contains("libfuse3"); - } - false + 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 { - // Virtually all Type 2 AppImages support this flag - // We check by looking at the appimage type (offset 8 in the file) - if let Ok(data) = std::fs::read(appimage_path) { - if data.len() > 11 { - // Check for AppImage Type 2 magic at offset 8 - return data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x02; - } + // 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 } diff --git a/src/core/security.rs b/src/core/security.rs index d0499e0..876d758 100644 --- a/src/core/security.rs +++ b/src/core/security.rs @@ -155,17 +155,11 @@ fn version_from_soname(soname: &str) -> Option { /// Extract the list of shared libraries bundled inside an AppImage. pub fn inventory_bundled_libraries(appimage_path: &Path) -> Vec { - // Get squashfs offset - let offset_output = Command::new(appimage_path) - .arg("--appimage-offset") - .env("APPIMAGE_EXTRACT_AND_RUN", "1") - .output(); - - let offset = match offset_output { - Ok(out) if out.status.success() => { - String::from_utf8_lossy(&out.stdout).trim().to_string() - } - _ => return Vec::new(), + // Get squashfs offset via binary scan (never execute the AppImage - + // some apps like Affinity have custom AppRun scripts that ignore flags) + let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) { + Some(o) => o.to_string(), + None => return Vec::new(), }; // Use unsquashfs to list all files with details @@ -250,18 +244,9 @@ pub fn detect_version_from_binary( appimage_path: &Path, lib_file_path: &str, ) -> Option { - // Get squashfs offset - let offset_output = Command::new(appimage_path) - .arg("--appimage-offset") - .env("APPIMAGE_EXTRACT_AND_RUN", "1") - .output() - .ok()?; - - if !offset_output.status.success() { - return None; - } - - let offset = String::from_utf8_lossy(&offset_output.stdout).trim().to_string(); + // Get squashfs offset via binary scan (never execute the AppImage) + let offset = crate::core::inspector::find_squashfs_offset_for(appimage_path)? + .to_string(); // Extract the specific library to a temp file let temp_dir = tempfile::tempdir().ok()?; diff --git a/src/core/updater.rs b/src/core/updater.rs index 3424c19..b86b30d 100644 --- a/src/core/updater.rs +++ b/src/core/updater.rs @@ -119,7 +119,14 @@ pub fn parse_update_info(raw: &str) -> Option { /// named ".upd_info" or ".updinfo". It can also be found at a fixed offset /// in the AppImage runtime (bytes 0x414..0x614 in the ELF header area). pub fn read_update_info(path: &Path) -> Option { - let data = fs::read(path).ok()?; + // Only read the first 1MB - update info is always in the ELF header area, + // never deep in the squashfs payload. Avoids loading 1.5GB+ files into memory. + let mut file = fs::File::open(path).ok()?; + let file_len = file.metadata().ok()?.len() as usize; + let read_len = file_len.min(1024 * 1024); + let mut data = vec![0u8; read_len]; + use std::io::Read; + file.read_exact(&mut data).ok()?; // Method 1: Try to read from fixed offset range in AppImage Type 2 runtime. // The update info is typically at offset 0xC48 (3144) in the ELF, but the diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index 75ce326..20cae58 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -367,7 +367,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { } // ----------------------------------------------------------------------- - // Screenshots section - async image loading from URLs + // Screenshots section - async image loading from URLs, click to lightbox // ----------------------------------------------------------------------- if let Some(ref urls_str) = record.screenshot_urls { let urls: Vec<&str> = urls_str.lines().filter(|u| !u.is_empty()).collect(); @@ -387,11 +387,18 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .carousel(&carousel) .build(); - for url in &urls { + // Store textures for lightbox access + let textures: Rc>>> = + Rc::new(std::cell::RefCell::new(vec![None; urls.len()])); + + for (idx, url) in urls.iter().enumerate() { let picture = gtk::Picture::builder() .content_fit(gtk::ContentFit::Contain) .height_request(300) .build(); + if let Some(cursor) = gtk::gdk::Cursor::from_name("pointer", None) { + picture.set_cursor(Some(&cursor)); + } picture.set_can_shrink(true); // Placeholder spinner while loading @@ -404,12 +411,30 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { .build(); overlay.add_overlay(&spinner); + // Click handler for lightbox + let textures_click = textures.clone(); + let click = gtk::GestureClick::new(); + click.connect_released(move |gesture, _, _, _| { + let textures_ref = textures_click.borrow(); + if let Some(Some(ref texture)) = textures_ref.get(idx) { + if let Some(widget) = gesture.widget() { + if let Some(root) = gtk::prelude::WidgetExt::root(&widget) { + if let Ok(window) = root.downcast::() { + show_screenshot_lightbox(&window, texture); + } + } + } + } + }); + overlay.add_controller(click); + carousel.append(&overlay); // Load image asynchronously let url_owned = url.to_string(); let picture_ref = picture.clone(); let spinner_ref = spinner.clone(); + let textures_load = textures.clone(); glib::spawn_future_local(async move { let result = gio::spawn_blocking(move || { let mut response = ureq::get(&url_owned) @@ -432,6 +457,9 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { ) { let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf); picture_ref.set_paintable(Some(&texture)); + if let Some(slot) = textures_load.borrow_mut().get_mut(idx) { + *slot = Some(texture); + } } } }); @@ -487,9 +515,12 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { icon.set_valign(gtk::Align::Center); row.add_suffix(&icon); + // Start with the fallback icon, then try to load favicon let prefix_icon = gtk::Image::from_icon_name(*icon_name); prefix_icon.set_valign(gtk::Align::Center); + prefix_icon.set_pixel_size(16); row.add_prefix(&prefix_icon); + fetch_favicon_async(url, &prefix_icon); let url_clone = url.clone(); row.connect_activated(move |row| { @@ -1558,3 +1589,78 @@ fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> { _ => None, } } + +/// Show a screenshot in a fullscreen-ish lightbox dialog. +fn show_screenshot_lightbox(parent: >k::Window, texture: >k::gdk::Texture) { + let picture = gtk::Picture::builder() + .paintable(texture) + .content_fit(gtk::ContentFit::Contain) + .hexpand(true) + .vexpand(true) + .build(); + picture.set_can_shrink(true); + + let dialog = adw::Dialog::builder() + .title("Screenshot") + .content_width(900) + .content_height(600) + .build(); + + let toolbar = adw::ToolbarView::new(); + let header = adw::HeaderBar::new(); + toolbar.add_top_bar(&header); + toolbar.set_content(Some(&picture)); + dialog.set_child(Some(&toolbar)); + + dialog.present(Some(parent)); +} + +/// Fetch a favicon for a URL and set it on an image widget. +fn fetch_favicon_async(url: &str, image: >k::Image) { + // Extract domain from URL for favicon service + let domain = url + .trim_start_matches("https://") + .trim_start_matches("http://") + .split('/') + .next() + .unwrap_or("") + .to_string(); + + if domain.is_empty() { + return; + } + + let image_ref = image.clone(); + glib::spawn_future_local(async move { + let favicon_url = format!( + "https://www.google.com/s2/favicons?domain={}&sz=32", + domain + ); + let result = gio::spawn_blocking(move || { + let mut response = ureq::get(&favicon_url) + .header("User-Agent", "Driftwood-AppImage-Manager/0.1") + .call() + .ok()?; + let mut buf = Vec::new(); + response.body_mut().as_reader().read_to_end(&mut buf).ok()?; + if buf.len() > 100 { + Some(buf) + } else { + None + } + }) + .await; + + if let Ok(Some(data)) = result { + let gbytes = glib::Bytes::from(&data); + let stream = gio::MemoryInputStream::from_bytes(&gbytes); + if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream( + &stream, + None::<&gio::Cancellable>, + ) { + let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf); + image_ref.set_paintable(Some(&texture)); + } + } + }); +}