Fix performance, add screenshots, make banner scrollable
- Make detail view banner scroll with content instead of staying fixed, preventing tall banners from eating screen space - Optimize squashfs offset scanning with buffered 256KB chunk reading instead of loading entire file into memory (critical for 1.5GB+ files) - Add screenshot URL parsing from AppStream XML and async image display with carousel in the overview tab - Fix infinite re-analysis bug: has_appstream check caused every app without AppStream data to be re-analyzed on every startup. Now handled via one-time migration reset in v10 - Database migration v10: add screenshot_urls column, reset analysis status for one-time re-scan with new parser
This commit is contained in:
@@ -60,6 +60,7 @@ pub struct AppImageMetadata {
|
||||
pub releases: Vec<crate::core::appstream::ReleaseInfo>,
|
||||
pub desktop_actions: Vec<String>,
|
||||
pub has_signature: bool,
|
||||
pub screenshot_urls: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -105,26 +106,73 @@ pub fn find_squashfs_offset_for(path: &Path) -> Option<u64> {
|
||||
|
||||
/// 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.
|
||||
/// Uses buffered chunk-based reading to avoid loading entire files into memory
|
||||
/// (critical for large AppImages like Affinity at 1.5GB+).
|
||||
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);
|
||||
use std::io::{BufReader, Seek, SeekFrom};
|
||||
|
||||
let file = fs::File::open(path)?;
|
||||
let file_len = file.metadata()?.len();
|
||||
let mut reader = BufReader::with_capacity(256 * 1024, file);
|
||||
|
||||
// Skip first 4KB to avoid false matches in ELF header
|
||||
let start: u64 = 4096.min(file_len);
|
||||
reader.seek(SeekFrom::Start(start))?;
|
||||
|
||||
// Read in 256KB chunks with 96-byte overlap to catch magic spanning boundaries
|
||||
let chunk_size: usize = 256 * 1024;
|
||||
let overlap: usize = 96;
|
||||
let mut buf = vec![0u8; chunk_size];
|
||||
let mut file_pos = start;
|
||||
|
||||
loop {
|
||||
if file_pos >= file_len {
|
||||
break;
|
||||
}
|
||||
|
||||
let to_read = chunk_size.min((file_len - file_pos) as usize);
|
||||
let mut total_read = 0;
|
||||
while total_read < to_read {
|
||||
let n = Read::read(&mut reader, &mut buf[total_read..to_read])?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
total_read += n;
|
||||
}
|
||||
if total_read < 32 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Scan this chunk for squashfs magic
|
||||
let scan_end = total_read.saturating_sub(31);
|
||||
for i in 0..scan_end {
|
||||
if buf[i..i + 4] == *b"hsqs" {
|
||||
let major = u16::from_le_bytes([buf[i + 28], buf[i + 29]]);
|
||||
let minor = u16::from_le_bytes([buf[i + 30], buf[i + 31]]);
|
||||
if major == 4 && minor == 0 {
|
||||
let block_size = u32::from_le_bytes([
|
||||
buf[i + 12], buf[i + 13], buf[i + 14], buf[i + 15],
|
||||
]);
|
||||
if block_size.is_power_of_two()
|
||||
&& block_size >= 4096
|
||||
&& block_size <= 1_048_576
|
||||
{
|
||||
return Ok(file_pos + i as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance, keeping overlap to catch magic spanning chunks
|
||||
let advance = if total_read > overlap {
|
||||
total_read - overlap
|
||||
} else {
|
||||
total_read
|
||||
};
|
||||
file_pos += advance as u64;
|
||||
reader.seek(SeekFrom::Start(file_pos))?;
|
||||
}
|
||||
|
||||
Err(InspectorError::NoOffset)
|
||||
}
|
||||
|
||||
@@ -677,6 +725,10 @@ pub fn inspect_appimage(
|
||||
.unwrap_or_default(),
|
||||
desktop_actions: fields.actions,
|
||||
has_signature: has_sig,
|
||||
screenshot_urls: appstream
|
||||
.as_ref()
|
||||
.map(|a| a.screenshot_urls.clone())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user