Stop executing AppImages during analysis, add screenshot lightbox and favicons
- 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
This commit is contained in:
@@ -171,36 +171,37 @@ pub fn determine_app_fuse_status(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the AppImage uses the new type2-runtime with statically linked FUSE.
|
/// 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 {
|
fn has_static_runtime(appimage_path: &Path) -> bool {
|
||||||
// The new type2-runtime responds to --appimage-version with a version string
|
use std::io::Read;
|
||||||
// containing "type2-runtime" or a recent date
|
let mut file = match std::fs::File::open(appimage_path) {
|
||||||
let output = Command::new(appimage_path)
|
Ok(f) => f,
|
||||||
.arg("--appimage-version")
|
Err(_) => return false,
|
||||||
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
|
};
|
||||||
.output();
|
// The runtime signature is in the ELF binary header area, well within first 256KB
|
||||||
|
let mut buf = vec![0u8; 256 * 1024];
|
||||||
if let Ok(output) = output {
|
let n = match file.read(&mut buf) {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
|
Ok(n) => n,
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
|
Err(_) => return false,
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
};
|
||||||
// New runtime identifies itself
|
let data = &buf[..n];
|
||||||
return combined.contains("type2-runtime")
|
let haystack = String::from_utf8_lossy(data).to_lowercase();
|
||||||
|| combined.contains("static")
|
haystack.contains("type2-runtime")
|
||||||
|| combined.contains("libfuse3");
|
|| haystack.contains("libfuse3")
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if --appimage-extract-and-run is supported.
|
/// Check if --appimage-extract-and-run is supported.
|
||||||
fn supports_extract_and_run(appimage_path: &Path) -> bool {
|
fn supports_extract_and_run(appimage_path: &Path) -> bool {
|
||||||
// Virtually all Type 2 AppImages support this flag
|
// Check for AppImage Type 2 magic at offset 8 - only need to read 12 bytes
|
||||||
// We check by looking at the appimage type (offset 8 in the file)
|
use std::io::Read;
|
||||||
if let Ok(data) = std::fs::read(appimage_path) {
|
let mut file = match std::fs::File::open(appimage_path) {
|
||||||
if data.len() > 11 {
|
Ok(f) => f,
|
||||||
// Check for AppImage Type 2 magic at offset 8
|
Err(_) => return false,
|
||||||
return data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x02;
|
};
|
||||||
}
|
let mut header = [0u8; 12];
|
||||||
|
if file.read_exact(&mut header).is_ok() {
|
||||||
|
return header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,17 +155,11 @@ fn version_from_soname(soname: &str) -> Option<String> {
|
|||||||
|
|
||||||
/// Extract the list of shared libraries bundled inside an AppImage.
|
/// Extract the list of shared libraries bundled inside an AppImage.
|
||||||
pub fn inventory_bundled_libraries(appimage_path: &Path) -> Vec<BundledLibrary> {
|
pub fn inventory_bundled_libraries(appimage_path: &Path) -> Vec<BundledLibrary> {
|
||||||
// Get squashfs offset
|
// Get squashfs offset via binary scan (never execute the AppImage -
|
||||||
let offset_output = Command::new(appimage_path)
|
// some apps like Affinity have custom AppRun scripts that ignore flags)
|
||||||
.arg("--appimage-offset")
|
let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) {
|
||||||
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
|
Some(o) => o.to_string(),
|
||||||
.output();
|
None => return Vec::new(),
|
||||||
|
|
||||||
let offset = match offset_output {
|
|
||||||
Ok(out) if out.status.success() => {
|
|
||||||
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
||||||
}
|
|
||||||
_ => return Vec::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use unsquashfs to list all files with details
|
// Use unsquashfs to list all files with details
|
||||||
@@ -250,18 +244,9 @@ pub fn detect_version_from_binary(
|
|||||||
appimage_path: &Path,
|
appimage_path: &Path,
|
||||||
lib_file_path: &str,
|
lib_file_path: &str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
// Get squashfs offset
|
// Get squashfs offset via binary scan (never execute the AppImage)
|
||||||
let offset_output = Command::new(appimage_path)
|
let offset = crate::core::inspector::find_squashfs_offset_for(appimage_path)?
|
||||||
.arg("--appimage-offset")
|
.to_string();
|
||||||
.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();
|
|
||||||
|
|
||||||
// Extract the specific library to a temp file
|
// Extract the specific library to a temp file
|
||||||
let temp_dir = tempfile::tempdir().ok()?;
|
let temp_dir = tempfile::tempdir().ok()?;
|
||||||
|
|||||||
@@ -119,7 +119,14 @@ pub fn parse_update_info(raw: &str) -> Option<UpdateType> {
|
|||||||
/// named ".upd_info" or ".updinfo". It can also be found at a fixed offset
|
/// 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).
|
/// in the AppImage runtime (bytes 0x414..0x614 in the ELF header area).
|
||||||
pub fn read_update_info(path: &Path) -> Option<String> {
|
pub fn read_update_info(path: &Path) -> Option<String> {
|
||||||
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.
|
// 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
|
// The update info is typically at offset 0xC48 (3144) in the ELF, but the
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> 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 {
|
if let Some(ref urls_str) = record.screenshot_urls {
|
||||||
let urls: Vec<&str> = urls_str.lines().filter(|u| !u.is_empty()).collect();
|
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<Database>) -> gtk::Box {
|
|||||||
.carousel(&carousel)
|
.carousel(&carousel)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
for url in &urls {
|
// Store textures for lightbox access
|
||||||
|
let textures: Rc<std::cell::RefCell<Vec<Option<gtk::gdk::Texture>>>> =
|
||||||
|
Rc::new(std::cell::RefCell::new(vec![None; urls.len()]));
|
||||||
|
|
||||||
|
for (idx, url) in urls.iter().enumerate() {
|
||||||
let picture = gtk::Picture::builder()
|
let picture = gtk::Picture::builder()
|
||||||
.content_fit(gtk::ContentFit::Contain)
|
.content_fit(gtk::ContentFit::Contain)
|
||||||
.height_request(300)
|
.height_request(300)
|
||||||
.build();
|
.build();
|
||||||
|
if let Some(cursor) = gtk::gdk::Cursor::from_name("pointer", None) {
|
||||||
|
picture.set_cursor(Some(&cursor));
|
||||||
|
}
|
||||||
picture.set_can_shrink(true);
|
picture.set_can_shrink(true);
|
||||||
|
|
||||||
// Placeholder spinner while loading
|
// Placeholder spinner while loading
|
||||||
@@ -404,12 +411,30 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
|||||||
.build();
|
.build();
|
||||||
overlay.add_overlay(&spinner);
|
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::<gtk::Window>() {
|
||||||
|
show_screenshot_lightbox(&window, texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
overlay.add_controller(click);
|
||||||
|
|
||||||
carousel.append(&overlay);
|
carousel.append(&overlay);
|
||||||
|
|
||||||
// Load image asynchronously
|
// Load image asynchronously
|
||||||
let url_owned = url.to_string();
|
let url_owned = url.to_string();
|
||||||
let picture_ref = picture.clone();
|
let picture_ref = picture.clone();
|
||||||
let spinner_ref = spinner.clone();
|
let spinner_ref = spinner.clone();
|
||||||
|
let textures_load = textures.clone();
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
let result = gio::spawn_blocking(move || {
|
let result = gio::spawn_blocking(move || {
|
||||||
let mut response = ureq::get(&url_owned)
|
let mut response = ureq::get(&url_owned)
|
||||||
@@ -432,6 +457,9 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
|||||||
) {
|
) {
|
||||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
picture_ref.set_paintable(Some(&texture));
|
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<Database>) -> gtk::Box {
|
|||||||
icon.set_valign(gtk::Align::Center);
|
icon.set_valign(gtk::Align::Center);
|
||||||
row.add_suffix(&icon);
|
row.add_suffix(&icon);
|
||||||
|
|
||||||
|
// Start with the fallback icon, then try to load favicon
|
||||||
let prefix_icon = gtk::Image::from_icon_name(*icon_name);
|
let prefix_icon = gtk::Image::from_icon_name(*icon_name);
|
||||||
prefix_icon.set_valign(gtk::Align::Center);
|
prefix_icon.set_valign(gtk::Align::Center);
|
||||||
|
prefix_icon.set_pixel_size(16);
|
||||||
row.add_prefix(&prefix_icon);
|
row.add_prefix(&prefix_icon);
|
||||||
|
fetch_favicon_async(url, &prefix_icon);
|
||||||
|
|
||||||
let url_clone = url.clone();
|
let url_clone = url.clone();
|
||||||
row.connect_activated(move |row| {
|
row.connect_activated(move |row| {
|
||||||
@@ -1558,3 +1589,78 @@ fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> {
|
|||||||
_ => None,
|
_ => 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user