mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-11 13:40:19 +02:00
feat: linux support preview
This commit is contained in:
+705
-28
@@ -34,18 +34,146 @@ impl Extractor {
|
||||
};
|
||||
let _ = app_handle.emit("download-progress", &progress);
|
||||
|
||||
let extension = archive_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("");
|
||||
// Try to detect the actual file type by reading the file header
|
||||
let actual_format = self.detect_file_format(archive_path)?;
|
||||
|
||||
match extension {
|
||||
"dmg" => self.extract_dmg(archive_path, dest_dir).await,
|
||||
match actual_format.as_str() {
|
||||
"dmg" => {
|
||||
#[cfg(target_os = "macos")]
|
||||
return self.extract_dmg(archive_path, dest_dir).await;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
return Err("DMG extraction is only supported on macOS".into());
|
||||
}
|
||||
"zip" => self.extract_zip(archive_path, dest_dir).await,
|
||||
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
||||
"tar.xz" => self.extract_tar_xz(archive_path, dest_dir).await,
|
||||
"tar.bz2" => self.extract_tar_bz2(archive_path, dest_dir).await,
|
||||
"tar.gz" => self.extract_tar_gz(archive_path, dest_dir).await,
|
||||
"exe" => {
|
||||
// For Windows EXE files, some may be self-extracting archives, others are installers
|
||||
// For browsers like Firefox, TOR, they're typically installers that don't need extraction
|
||||
self
|
||||
.handle_exe_file(archive_path, dest_dir, browser_type)
|
||||
.await
|
||||
}
|
||||
"deb" => {
|
||||
#[cfg(target_os = "linux")]
|
||||
return self.extract_deb(archive_path, dest_dir).await;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Err("DEB extraction is only supported on Linux".into());
|
||||
}
|
||||
"appimage" => {
|
||||
#[cfg(target_os = "linux")]
|
||||
return self.handle_appimage(archive_path, dest_dir).await;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Err("AppImage is only supported on Linux".into());
|
||||
}
|
||||
_ => {
|
||||
Err(format!(
|
||||
"Unsupported archive format: {} (detected: {}). The downloaded file might be corrupted or in an unexpected format.",
|
||||
archive_path.extension().and_then(|ext| ext.to_str()).unwrap_or("unknown"),
|
||||
actual_format
|
||||
).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the actual file format by reading file headers
|
||||
fn detect_file_format(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
let mut file = File::open(file_path)?;
|
||||
let mut buffer = [0u8; 12]; // Read first 12 bytes for magic number detection
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
// Check magic numbers for different file types
|
||||
match &buffer[0..4] {
|
||||
[0x50, 0x4B, 0x03, 0x04] | [0x50, 0x4B, 0x05, 0x06] | [0x50, 0x4B, 0x07, 0x08] => {
|
||||
return Ok("zip".to_string())
|
||||
}
|
||||
[0x7F, 0x45, 0x4C, 0x46] => return Ok("appimage".to_string()), // ELF header (AppImage)
|
||||
[0x4D, 0x5A, _, _] => return Ok("exe".to_string()), // PE header (Windows EXE)
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Check for XZ compressed files
|
||||
if buffer[0..6] == [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00] {
|
||||
return Ok("tar.xz".to_string());
|
||||
}
|
||||
|
||||
// Check for Bzip2 compressed files
|
||||
if buffer[0..3] == [0x42, 0x5A, 0x68] {
|
||||
return Ok("tar.bz2".to_string());
|
||||
}
|
||||
|
||||
// Check for Gzip compressed files
|
||||
if buffer[0..3] == [0x1F, 0x8B, 0x08] {
|
||||
return Ok("tar.gz".to_string());
|
||||
}
|
||||
|
||||
// Check for DEB files
|
||||
if buffer[0..8] == [0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A] {
|
||||
return Ok("deb".to_string());
|
||||
}
|
||||
|
||||
// Fallback to file extension
|
||||
if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
|
||||
match ext.to_lowercase().as_str() {
|
||||
"dmg" => Ok("dmg".to_string()),
|
||||
"zip" => Ok("zip".to_string()),
|
||||
"xz" => {
|
||||
if file_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.ends_with(".tar.xz")
|
||||
{
|
||||
Ok("tar.xz".to_string())
|
||||
} else {
|
||||
Ok("xz".to_string())
|
||||
}
|
||||
}
|
||||
"bz2" => {
|
||||
if file_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.ends_with(".tar.bz2")
|
||||
{
|
||||
Ok("tar.bz2".to_string())
|
||||
} else {
|
||||
Ok("bz2".to_string())
|
||||
}
|
||||
}
|
||||
"gz" => {
|
||||
if file_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.ends_with(".tar.gz")
|
||||
{
|
||||
Ok("tar.gz".to_string())
|
||||
} else {
|
||||
Ok("gz".to_string())
|
||||
}
|
||||
}
|
||||
"exe" => Ok("exe".to_string()),
|
||||
"deb" => Ok("deb".to_string()),
|
||||
"appimage" => Ok("appimage".to_string()),
|
||||
_ => Ok("unknown".to_string()),
|
||||
}
|
||||
} else {
|
||||
Ok("unknown".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn extract_dmg(
|
||||
&self,
|
||||
dmg_path: &Path,
|
||||
@@ -154,7 +282,56 @@ impl Extractor {
|
||||
zip_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Use unzip command to extract
|
||||
// Platform-specific ZIP extraction
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.extract_zip_windows(zip_path, dest_dir).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
self.extract_zip_unix(zip_path, dest_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn extract_zip_windows(
|
||||
&self,
|
||||
zip_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Use PowerShell's Expand-Archive on Windows
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!(
|
||||
"Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
|
||||
zip_path.display(),
|
||||
dest_dir.display()
|
||||
),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to extract zip with PowerShell: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
self.find_extracted_executable(dest_dir).await
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
async fn extract_zip_unix(
|
||||
&self,
|
||||
zip_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Use unzip command on Unix-like systems
|
||||
let output = Command::new("unzip")
|
||||
.args([
|
||||
"-q", // quiet
|
||||
@@ -174,16 +351,269 @@ impl Extractor {
|
||||
);
|
||||
}
|
||||
|
||||
// Find the extracted .app directory or Chromium.app specifically
|
||||
let mut app_path: Option<PathBuf> = None;
|
||||
self.find_extracted_executable(dest_dir).await
|
||||
}
|
||||
|
||||
pub async fn extract_tar_xz(
|
||||
&self,
|
||||
tar_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
create_dir_all(dest_dir)?;
|
||||
|
||||
// Use tar command for more reliable extraction
|
||||
let output = Command::new("tar")
|
||||
.args([
|
||||
"-xf",
|
||||
tar_path.to_str().unwrap(),
|
||||
"-C",
|
||||
dest_dir.to_str().unwrap(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to extract tar.xz: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Find the extracted executable and set proper permissions
|
||||
let executable_path = self.find_extracted_executable(dest_dir).await?;
|
||||
|
||||
// Ensure executable permissions are set correctly for Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
self.set_executable_permissions(&executable_path).await?;
|
||||
}
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
pub async fn extract_tar_bz2(
|
||||
&self,
|
||||
tar_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
create_dir_all(dest_dir)?;
|
||||
|
||||
// Use tar command for more reliable extraction
|
||||
let output = Command::new("tar")
|
||||
.args([
|
||||
"-xjf",
|
||||
tar_path.to_str().unwrap(),
|
||||
"-C",
|
||||
dest_dir.to_str().unwrap(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to extract tar.bz2: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Find the extracted executable and set proper permissions
|
||||
let executable_path = self.find_extracted_executable(dest_dir).await?;
|
||||
|
||||
// Ensure executable permissions are set correctly for Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
self.set_executable_permissions(&executable_path).await?;
|
||||
}
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
pub async fn extract_tar_gz(
|
||||
&self,
|
||||
tar_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
create_dir_all(dest_dir)?;
|
||||
|
||||
// Use tar command for more reliable extraction
|
||||
let output = Command::new("tar")
|
||||
.args([
|
||||
"-xzf",
|
||||
tar_path.to_str().unwrap(),
|
||||
"-C",
|
||||
dest_dir.to_str().unwrap(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to extract tar.gz: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Find the extracted executable and set proper permissions
|
||||
let executable_path = self.find_extracted_executable(dest_dir).await?;
|
||||
|
||||
// Ensure executable permissions are set correctly for Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
self.set_executable_permissions(&executable_path).await?;
|
||||
}
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn extract_deb(
|
||||
&self,
|
||||
deb_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
create_dir_all(dest_dir)?;
|
||||
|
||||
// Extract DEB package using dpkg-deb
|
||||
let output = Command::new("dpkg-deb")
|
||||
.args(["-x", deb_path.to_str().unwrap(), dest_dir.to_str().unwrap()])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to extract DEB: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Find the extracted executable and set proper permissions
|
||||
let executable_path = self.find_extracted_executable(dest_dir).await?;
|
||||
|
||||
// Ensure executable permissions are set correctly
|
||||
self.set_executable_permissions(&executable_path).await?;
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn handle_appimage(
|
||||
&self,
|
||||
appimage_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
create_dir_all(dest_dir)?;
|
||||
|
||||
// For AppImages, we typically just copy them and make sure they're executable
|
||||
let dest_file = dest_dir.join(
|
||||
appimage_path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new("app.AppImage")),
|
||||
);
|
||||
|
||||
// Copy the AppImage to destination
|
||||
fs::copy(appimage_path, &dest_file)?;
|
||||
|
||||
// Set executable permissions
|
||||
self.set_executable_permissions(&dest_file).await?;
|
||||
|
||||
Ok(dest_file)
|
||||
}
|
||||
|
||||
pub async fn handle_exe_file(
|
||||
&self,
|
||||
exe_path: &Path,
|
||||
dest_dir: &Path,
|
||||
browser_type: BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match browser_type {
|
||||
BrowserType::Zen => {
|
||||
// Zen installer EXE needs to be run to install
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.install_zen_windows(exe_path, dest_dir).await
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Err("Zen EXE installation is only supported on Windows".into())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For other browsers (Firefox, TOR, etc.), the EXE is typically just copied
|
||||
let exe_name = exe_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("browser.exe");
|
||||
|
||||
let dest_path = dest_dir.join(exe_name);
|
||||
fs::copy(exe_path, &dest_path)?;
|
||||
Ok(dest_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn install_zen_windows(
|
||||
&self,
|
||||
installer_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// For Zen installer, we need to run it silently
|
||||
// This is a simplified approach - in practice, you might need more sophisticated installer handling
|
||||
let output = Command::new(installer_path)
|
||||
.args(["/S", &format!("/D={}", dest_dir.display())])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to install Zen: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Find the installed executable
|
||||
self.find_extracted_executable(dest_dir).await
|
||||
}
|
||||
|
||||
async fn find_extracted_executable(
|
||||
&self,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Platform-specific executable finding logic
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.find_macos_app(dest_dir).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.find_windows_executable(dest_dir).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.find_linux_executable(dest_dir).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn find_macos_app(
|
||||
&self,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// First, try to find any .app file in the destination directory
|
||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "app") {
|
||||
app_path = Some(path);
|
||||
break;
|
||||
return Ok(path);
|
||||
}
|
||||
// For Chromium, check subdirectories (chrome-mac folder)
|
||||
if path.is_dir() {
|
||||
@@ -194,33 +624,280 @@ impl Extractor {
|
||||
// Move the app to the root destination directory
|
||||
let target_path = dest_dir.join(sub_path.file_name().unwrap());
|
||||
fs::rename(&sub_path, &target_path)?;
|
||||
app_path = Some(target_path);
|
||||
|
||||
// Clean up the now-empty subdirectory
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
break;
|
||||
return Ok(target_path);
|
||||
}
|
||||
}
|
||||
if app_path.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app_path = app_path.ok_or("No .app found after extraction")?;
|
||||
Err("No .app found after extraction".into())
|
||||
}
|
||||
|
||||
// Remove quarantine attributes
|
||||
let _ = Command::new("xattr")
|
||||
.args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
|
||||
.output();
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn find_windows_executable(
|
||||
&self,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Look for .exe files, preferring main browser executables
|
||||
let exe_names = [
|
||||
"chrome.exe",
|
||||
"firefox.exe",
|
||||
"zen.exe",
|
||||
"brave.exe",
|
||||
"tor.exe",
|
||||
];
|
||||
|
||||
let _ = Command::new("xattr")
|
||||
.args(["-cr", app_path.to_str().unwrap()])
|
||||
.output();
|
||||
for exe_name in &exe_names {
|
||||
let exe_path = dest_dir.join(exe_name);
|
||||
if exe_path.exists() {
|
||||
return Ok(exe_path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(app_path)
|
||||
// If no specific executable found, look for any .exe file
|
||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
// Check subdirectories
|
||||
if path.is_dir() {
|
||||
if let Ok(sub_result) = self.find_windows_executable(&path).await {
|
||||
return Ok(sub_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("No executable found after extraction".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn find_linux_executable(
|
||||
&self,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Enhanced list of common browser executable names with better pattern matching
|
||||
let exe_names = [
|
||||
// Firefox variants
|
||||
"firefox",
|
||||
"firefox-bin",
|
||||
"firefox-esr",
|
||||
"firefox-trunk",
|
||||
// Chrome/Chromium variants
|
||||
"chrome",
|
||||
"google-chrome",
|
||||
"google-chrome-stable",
|
||||
"google-chrome-beta",
|
||||
"google-chrome-unstable",
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"chromium-bin",
|
||||
// Zen Browser
|
||||
"zen",
|
||||
"zen-browser",
|
||||
"zen-bin",
|
||||
// Brave variants
|
||||
"brave",
|
||||
"brave-browser",
|
||||
"brave-browser-stable",
|
||||
"brave-browser-beta",
|
||||
"brave-browser-dev",
|
||||
"brave-bin",
|
||||
// Tor Browser variants
|
||||
"tor-browser",
|
||||
"torbrowser-launcher",
|
||||
"tor-browser_en-US",
|
||||
"start-tor-browser",
|
||||
"Browser/start-tor-browser",
|
||||
// Mullvad Browser
|
||||
"mullvad-browser",
|
||||
"mullvad-browser-bin",
|
||||
// AppImage pattern (will be handled specially)
|
||||
"*.AppImage",
|
||||
];
|
||||
|
||||
// First, try direct lookup in the main directory
|
||||
for exe_name in &exe_names {
|
||||
if exe_name.contains('*') {
|
||||
// Handle glob patterns like *.AppImage
|
||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if file_name.ends_with(".AppImage") && self.is_executable(&path) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let exe_path = dest_dir.join(exe_name);
|
||||
if exe_path.exists() && self.is_executable(&exe_path) {
|
||||
return Ok(exe_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced list of common Linux subdirectories to search
|
||||
let subdirs = [
|
||||
// Standard Unix directories
|
||||
"bin",
|
||||
"usr/bin",
|
||||
"usr/local/bin",
|
||||
"opt",
|
||||
"sbin",
|
||||
"usr/sbin",
|
||||
// Browser-specific directories
|
||||
"firefox",
|
||||
"chrome",
|
||||
"chromium",
|
||||
"brave",
|
||||
"zen",
|
||||
"tor-browser",
|
||||
"mullvad-browser",
|
||||
// Common extraction patterns
|
||||
".",
|
||||
"./",
|
||||
// Package-specific extraction patterns
|
||||
"firefox",
|
||||
"mullvad-browser",
|
||||
"tor-browser_en-US",
|
||||
"Browser",
|
||||
"browser",
|
||||
// Nested patterns for different distro packaging
|
||||
"opt/google/chrome",
|
||||
"opt/brave.com/brave",
|
||||
"opt/mullvad-browser",
|
||||
"usr/lib/firefox",
|
||||
"usr/lib/chromium",
|
||||
"usr/share/applications",
|
||||
// AppImage mount patterns
|
||||
"usr/bin",
|
||||
"AppRun",
|
||||
];
|
||||
|
||||
// Search in subdirectories with better depth handling
|
||||
for subdir in &subdirs {
|
||||
let subdir_path = dest_dir.join(subdir);
|
||||
if subdir_path.exists() && subdir_path.is_dir() {
|
||||
for exe_name in &exe_names {
|
||||
if exe_name.contains('*') {
|
||||
// Handle glob patterns for AppImages
|
||||
if let Ok(entries) = fs::read_dir(&subdir_path) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if file_name.ends_with(".AppImage") && self.is_executable(&path) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let exe_path = subdir_path.join(exe_name);
|
||||
if exe_path.exists() && self.is_executable(&exe_path) {
|
||||
return Ok(exe_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: enhanced recursive search for any executable file
|
||||
self.find_any_executable_recursive(dest_dir, 0).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_executable(&self, path: &Path) -> bool {
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
return metadata.permissions().mode() & 0o111 != 0;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Set executable permissions on Linux for extracted binaries
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn set_executable_permissions(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
if path.exists() {
|
||||
let mut permissions = path.metadata()?.permissions();
|
||||
// Set executable permissions for owner, group, and others if they have read permission
|
||||
let current_mode = permissions.mode();
|
||||
let new_mode = current_mode | 0o111; // Add execute permission
|
||||
permissions.set_mode(new_mode);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
async fn set_executable_permissions(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn find_any_executable_recursive(
|
||||
&self,
|
||||
dir: &Path,
|
||||
depth: usize,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Limit recursion depth to avoid infinite loops
|
||||
if depth > 5 {
|
||||
return Err("Maximum search depth reached".into());
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
let mut directories = Vec::new();
|
||||
|
||||
// First pass: look for executable files
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && self.is_executable(&path) {
|
||||
// Prefer files with browser-like names
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
let name_lower = file_name.to_lowercase();
|
||||
if name_lower.contains("firefox")
|
||||
|| name_lower.contains("chrome")
|
||||
|| name_lower.contains("brave")
|
||||
|| name_lower.contains("zen")
|
||||
|| name_lower.contains("tor")
|
||||
|| name_lower.contains("mullvad")
|
||||
|| file_name.ends_with(".AppImage")
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
directories.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: recursively search directories
|
||||
for dir_path in directories {
|
||||
if let Ok(result) = Box::pin(self.find_any_executable_recursive(&dir_path, depth + 1)).await
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("No executable found".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,13 +909,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_extractor_creation() {
|
||||
let _extractor = Extractor::new();
|
||||
let _ = Extractor::new();
|
||||
// Just verify we can create an extractor instance
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_archive_format() {
|
||||
let _extractor = Extractor::new();
|
||||
let _ = Extractor::new();
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let fake_archive = temp_dir.path().join("test.rar");
|
||||
File::create(&fake_archive).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user