Fix icon extraction, drop overlay styling, and card padding
- Fix upsert_appimage returning 0 for existing records by falling back to a SELECT query when last_insert_rowid is 0 - Replace --appimage-offset execution with binary squashfs magic scan to avoid hanging on AppImages with custom AppRun scripts - Add 5-second timeout fallback if binary scan fails - Extract desktop files from usr/share/applications/ for reverse-DNS named entries that root-level *.desktop glob misses - Add root-level png/svg fallback in icon search - Add CSS for drop overlay scrim, drop zone card, and drop zone icon - Add card padding (24px 20px) so content does not touch card edges - Always scan on startup to discover new AppImages
This commit is contained in:
@@ -703,7 +703,17 @@ impl Database {
|
||||
last_scanned = datetime('now')",
|
||||
params![path, filename, appimage_type, size_bytes, is_executable, file_modified],
|
||||
)?;
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
// last_insert_rowid() returns 0 for ON CONFLICT UPDATE, so query the actual id
|
||||
let id = self.conn.last_insert_rowid();
|
||||
if id != 0 {
|
||||
return Ok(id);
|
||||
}
|
||||
let id: i64 = self.conn.query_row(
|
||||
"SELECT id FROM appimages WHERE path = ?1",
|
||||
params![path],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn update_metadata(
|
||||
|
||||
@@ -74,13 +74,70 @@ fn has_unsquashfs() -> bool {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Find the squashfs offset by scanning for a valid superblock in the binary.
|
||||
/// This avoids executing the AppImage, which can hang for apps with custom AppRun scripts.
|
||||
fn find_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
|
||||
let data = fs::read(path)?;
|
||||
let magic = b"hsqs";
|
||||
// Search for squashfs magic after the ELF header (skip first 4KB to avoid false matches)
|
||||
let start = 4096.min(data.len());
|
||||
for i in start..data.len().saturating_sub(96) {
|
||||
if &data[i..i + 4] == magic {
|
||||
// Validate: check squashfs superblock version at offset +28 (major) and +30 (minor)
|
||||
let major = u16::from_le_bytes([data[i + 28], data[i + 29]]);
|
||||
let minor = u16::from_le_bytes([data[i + 30], data[i + 31]]);
|
||||
// Valid squashfs 4.0
|
||||
if major == 4 && minor == 0 {
|
||||
// Also check block_size at offset +12 is a power of 2 and reasonable (4KB-1MB)
|
||||
let block_size = u32::from_le_bytes([data[i + 12], data[i + 13], data[i + 14], data[i + 15]]);
|
||||
if block_size.is_power_of_two() && block_size >= 4096 && block_size <= 1_048_576 {
|
||||
return Ok(i as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(InspectorError::NoOffset)
|
||||
}
|
||||
|
||||
/// Get the squashfs offset from the AppImage by running it with --appimage-offset.
|
||||
/// Falls back to binary scanning if execution times out or fails.
|
||||
fn get_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
|
||||
let output = Command::new(path)
|
||||
// First try the fast binary scan approach (no execution needed)
|
||||
if let Ok(offset) = find_squashfs_offset(path) {
|
||||
return Ok(offset);
|
||||
}
|
||||
|
||||
// Fallback: run the AppImage with a timeout
|
||||
let child = Command::new(path)
|
||||
.arg("--appimage-offset")
|
||||
.env("APPIMAGE_EXTRACT_AND_RUN", "0")
|
||||
.output()?;
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn();
|
||||
|
||||
let mut child = match child {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(InspectorError::IoError(e)),
|
||||
};
|
||||
|
||||
// Wait up to 5 seconds
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) => {
|
||||
if start.elapsed() > std::time::Duration::from_secs(5) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return Err(InspectorError::NoOffset);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Err(e) => return Err(InspectorError::IoError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
let output = child.wait_with_output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout
|
||||
.trim()
|
||||
@@ -103,7 +160,10 @@ fn extract_metadata_files(
|
||||
.arg(dest)
|
||||
.arg(appimage_path)
|
||||
.arg("*.desktop")
|
||||
.arg("usr/share/applications/*.desktop")
|
||||
.arg(".DirIcon")
|
||||
.arg("*.png")
|
||||
.arg("*.svg")
|
||||
.arg("usr/share/icons/*")
|
||||
.arg("usr/share/metainfo/*.xml")
|
||||
.arg("usr/share/appdata/*.xml")
|
||||
@@ -135,7 +195,10 @@ fn extract_metadata_files_direct(
|
||||
.arg(dest)
|
||||
.arg(appimage_path)
|
||||
.arg("*.desktop")
|
||||
.arg("usr/share/applications/*.desktop")
|
||||
.arg(".DirIcon")
|
||||
.arg("*.png")
|
||||
.arg("*.svg")
|
||||
.arg("usr/share/icons/*")
|
||||
.arg("usr/share/metainfo/*.xml")
|
||||
.arg("usr/share/appdata/*.xml")
|
||||
@@ -152,8 +215,10 @@ fn extract_metadata_files_direct(
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the first .desktop file in a directory.
|
||||
/// Find the first .desktop file in the extract directory.
|
||||
/// Checks root level first, then usr/share/applications/.
|
||||
fn find_desktop_file(dir: &Path) -> Option<PathBuf> {
|
||||
// Check root of extract dir
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
@@ -162,6 +227,16 @@ fn find_desktop_file(dir: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check usr/share/applications/
|
||||
let apps_dir = dir.join("usr/share/applications");
|
||||
if let Ok(entries) = fs::read_dir(&apps_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("desktop") {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -266,9 +341,9 @@ fn detect_architecture(path: &Path) -> Option<String> {
|
||||
|
||||
/// Find an icon file in the extracted squashfs directory.
|
||||
fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
|
||||
// First try .DirIcon
|
||||
// First try .DirIcon (skip if it's a broken symlink)
|
||||
let dir_icon = extract_dir.join(".DirIcon");
|
||||
if dir_icon.exists() {
|
||||
if dir_icon.exists() && dir_icon.metadata().is_ok() {
|
||||
return Some(dir_icon);
|
||||
}
|
||||
|
||||
@@ -282,7 +357,7 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check usr/share/icons recursively
|
||||
// Check usr/share/icons recursively (prefer largest resolution)
|
||||
let icons_dir = extract_dir.join("usr/share/icons");
|
||||
if icons_dir.exists() {
|
||||
if let Some(found) = find_icon_recursive(&icons_dir, name) {
|
||||
@@ -291,6 +366,19 @@ fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: grab any .png or .svg at the root level
|
||||
if let Ok(entries) = fs::read_dir(extract_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
if ext == "png" || ext == "svg" {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user