Files
donutbrowser/src-tauri/src/extraction.rs
T
2026-03-13 12:57:01 +04:00

1752 lines
54 KiB
Rust

use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use crate::browser::BrowserType;
use crate::downloader::DownloadProgress;
use crate::events;
#[cfg(target_os = "macos")]
use std::process::Command;
#[cfg(target_os = "macos")]
use std::fs::create_dir_all;
pub struct Extractor;
impl Extractor {
fn new() -> Self {
Self
}
pub fn instance() -> &'static Extractor {
&EXTRACTOR
}
// NOTE: We intentionally do not rename or sanitize ZIP entry paths.
// We only ensure paths are enclosed within the destination using zip's enclosed_name.
/// Ensure the extracted files are in the correct directory structure expected by verification
#[cfg(target_os = "linux")]
async fn ensure_correct_directory_structure(
&self,
dest_dir: &Path,
exe_path: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Determine browser type from the destination directory path
let browser_type = if dest_dir.to_string_lossy().contains("camoufox") {
"camoufox"
} else if dest_dir.to_string_lossy().contains("wayfern") {
"wayfern"
} else {
return Ok(());
};
// For Camoufox and Wayfern on Linux, we expect the executable directly under version directory
// e.g., binaries/camoufox/<version>/camoufox, without an extra subdirectory
if browser_type == "camoufox" || browser_type == "wayfern" {
return Ok(());
}
let expected_subdir = dest_dir.join(browser_type);
// If the executable is not in the expected subdirectory, create the structure
if !exe_path.starts_with(&expected_subdir) {
log::info!("Reorganizing directory structure for {}", browser_type);
// Create the expected subdirectory
std::fs::create_dir_all(&expected_subdir)?;
// Move all files from the root to the subdirectory
if let Ok(entries) = std::fs::read_dir(dest_dir) {
for entry in entries.flatten() {
let path = entry.path();
let file_name = match path.file_name() {
Some(name) => name,
None => continue,
};
// Skip the subdirectory we just created
if path == expected_subdir {
continue;
}
let target_path = expected_subdir.join(file_name);
// Move the file/directory
if let Err(e) = std::fs::rename(&path, &target_path) {
log::info!(
"Warning: Failed to move {} to {}: {}",
path.display(),
target_path.display(),
e
);
} else {
log::info!("Moved {} to {}", path.display(), target_path.display());
}
}
}
log::info!("Directory structure reorganized for {}", browser_type);
}
Ok(())
}
pub async fn extract_browser(
&self,
_app_handle: &tauri::AppHandle,
browser_type: BrowserType,
version: &str,
archive_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Emit extraction start progress
let progress = DownloadProgress {
browser: browser_type.as_str().to_string(),
version: version.to_string(),
downloaded_bytes: 0,
total_bytes: None,
percentage: 0.0,
speed_bytes_per_sec: 0.0,
eta_seconds: None,
stage: "extracting".to_string(),
};
let _ = events::emit("download-progress", &progress);
log::info!(
"Starting extraction of {} for browser {} version {}",
archive_path.display(),
browser_type.as_str(),
version
);
// Detect the actual file type by reading the file header
let actual_format = self.detect_file_format(archive_path).map_err(|e| {
format!(
"Failed to detect file format for {}: {}",
archive_path.display(),
e
)
})?;
log::info!("Detected format: {actual_format}");
let extraction_result = match actual_format.as_str() {
"dmg" => {
#[cfg(target_os = "macos")]
{
self.extract_dmg(archive_path, dest_dir).await.map_err(|e| {
format!("DMG extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
#[cfg(not(target_os = "macos"))]
{
Err(format!("DMG extraction is only supported on macOS, but {} {} requires DMG extraction", browser_type.as_str(), version).into())
}
}
"zip" => {
self.extract_zip(archive_path, dest_dir).await.map_err(|e| {
format!("ZIP extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"tar.xz" => {
self.extract_tar_xz(archive_path, dest_dir).await.map_err(|e| {
format!("TAR.XZ extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"tar.bz2" => {
self.extract_tar_bz2(archive_path, dest_dir).await.map_err(|e| {
format!("TAR.BZ2 extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"tar.gz" => {
self.extract_tar_gz(archive_path, dest_dir).await.map_err(|e| {
format!("TAR.GZ extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"msi" => {
self.extract_msi(archive_path, dest_dir).await.map_err(|e| {
format!("MSI extraction failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"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.clone())
.await
.map_err(|e| {
format!("EXE handling failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
"appimage" => {
#[cfg(target_os = "linux")]
{
self.handle_appimage(archive_path, dest_dir).await.map_err(|e| {
format!("AppImage handling failed for {} {}: {}", browser_type.as_str(), version, e).into()
})
}
#[cfg(not(target_os = "linux"))]
{
Err(format!("AppImage is only supported on Linux, but {} {} requires AppImage handling", browser_type.as_str(), version).into())
}
}
_ => {
Err(format!(
"Unsupported archive format for {} {}: {} (detected: {}). The downloaded file might be corrupted or in an unexpected format. File: {}",
browser_type.as_str(),
version,
archive_path.extension().and_then(|ext| ext.to_str()).unwrap_or("unknown"),
actual_format,
archive_path.display()
).into())
}
};
match extraction_result {
Ok(path) => {
log::info!(
"Successfully extracted {} {} to: {}",
browser_type.as_str(),
version,
path.display()
);
Ok(path)
}
Err(e) => {
log::error!(
"Extraction failed for {} {}: {}",
browser_type.as_str(),
version,
e
);
Err(e)
}
}
}
/// 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>> {
// First check file extension for DMG files since they're common on macOS
// and can have misleading magic numbers
if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
if ext.to_lowercase() == "dmg" {
return Ok("dmg".to_string());
}
if ext.to_lowercase() == "msi" {
return Ok("msi".to_string());
}
}
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 MSI files (Microsoft Installer)
if buffer[0..8] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {
return Ok("msi".to_string());
}
// 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());
}
// 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()),
"msi" => Ok("msi".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()),
"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,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!(
"Extracting DMG: {} to {}",
dmg_path.display(),
dest_dir.display()
);
// Create a temporary mount point
let mount_point = std::env::temp_dir().join(format!(
"donut_mount_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
));
create_dir_all(&mount_point)?;
log::info!("Created mount point: {}", mount_point.display());
// Mount the DMG
let output = Command::new("hdiutil")
.args([
"attach",
"-nobrowse",
"-mountpoint",
mount_point.to_str().unwrap(),
dmg_path.to_str().unwrap(),
])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
log::info!("Failed to mount DMG. stdout: {stdout}, stderr: {stderr}");
// Clean up mount point before returning error
let _ = fs::remove_dir_all(&mount_point);
return Err(format!("Failed to mount DMG: {stderr}").into());
}
log::info!("Successfully mounted DMG");
// Find the .app directory in the mount point
let app_result = self.find_app_in_directory(&mount_point).await;
let app_entry = match app_result {
Ok(app_path) => app_path,
Err(e) => {
log::info!("Failed to find .app in mount point: {e}");
// Try to unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
let _ = fs::remove_dir_all(&mount_point);
return Err("No .app found after extraction".into());
}
};
log::info!("Found .app bundle: {}", app_entry.display());
// Copy the .app to the destination
let app_path = dest_dir.join(app_entry.file_name().unwrap());
log::info!("Copying .app to: {}", app_path.display());
let output = Command::new("cp")
.args([
"-R",
app_entry.to_str().unwrap(),
app_path.to_str().unwrap(),
])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("Failed to copy app: {stderr}");
// Unmount before returning error
let _ = Command::new("hdiutil")
.args(["detach", "-force", mount_point.to_str().unwrap()])
.output();
let _ = fs::remove_dir_all(&mount_point);
return Err(format!("Failed to copy app: {stderr}").into());
}
log::info!("Successfully copied .app bundle");
// Remove quarantine attributes
let _ = Command::new("xattr")
.args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
.output();
let _ = Command::new("xattr")
.args(["-cr", app_path.to_str().unwrap()])
.output();
log::info!("Removed quarantine attributes");
// Unmount the DMG
let output = Command::new("hdiutil")
.args(["detach", mount_point.to_str().unwrap()])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::warn!("Warning: Failed to unmount DMG: {stderr}");
// Don't fail if unmount fails - the extraction was successful
} else {
log::info!("Successfully unmounted DMG");
}
// Clean up mount point directory
let _ = fs::remove_dir_all(&mount_point);
Ok(app_path)
}
#[cfg(target_os = "macos")]
async fn find_app_in_directory(
&self,
dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
self.find_app_recursive(dir, 0).await
}
#[cfg(target_os = "macos")]
async fn find_app_recursive(
&self,
dir: &Path,
depth: usize,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Limit search depth to avoid infinite loops
if depth > 4 {
return Err("Maximum search depth reached".into());
}
if let Ok(entries) = fs::read_dir(dir) {
let mut subdirs = Vec::new();
let mut hidden_subdirs = Vec::new();
// First pass: look for .app bundles directly
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(extension) = path.extension() {
if extension == "app" {
log::info!("Found .app bundle at depth {}: {}", depth, path.display());
return Ok(path);
}
}
// Collect subdirectories for second pass
let filename = path.file_name().unwrap_or_default().to_string_lossy();
if filename.starts_with('.') {
// Hidden directories - search these with lower priority
hidden_subdirs.push(path);
} else {
// Regular directories - search these first
subdirs.push(path);
}
}
}
// Second pass: search regular subdirectories first
for subdir in subdirs {
// Skip common directories that are unlikely to contain .app files
let dirname = subdir.file_name().unwrap_or_default().to_string_lossy();
if matches!(
dirname.as_ref(),
"Documents" | "Downloads" | "Desktop" | "Library" | "System" | "tmp" | "var"
) {
continue;
}
if let Ok(result) = Box::pin(self.find_app_recursive(&subdir, depth + 1)).await {
return Ok(result);
}
}
// Third pass: search hidden directories if nothing found in regular ones
for hidden_dir in hidden_subdirs {
if let Ok(result) = Box::pin(self.find_app_recursive(&hidden_dir, depth + 1)).await {
return Ok(result);
}
}
}
Err(format!("No .app found in directory: {}", dir.display()).into())
}
pub async fn extract_zip(
&self,
zip_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Extracting ZIP archive: {}", zip_path.display());
std::fs::create_dir_all(dest_dir)?;
let file = File::open(zip_path)
.map_err(|e| format!("Failed to open ZIP file {}: {}", zip_path.display(), e))?;
let mut archive = zip::ZipArchive::new(BufReader::new(file))
.map_err(|e| format!("Failed to read ZIP archive {}: {}", zip_path.display(), e))?;
log::info!("ZIP archive contains {} files", archive.len());
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| format!("Failed to read ZIP entry at index {i}: {e}"))?;
// Use enclosed_name to prevent path traversal; do not modify names otherwise
let enclosed = entry
.enclosed_name()
.ok_or_else(|| format!("ZIP contains an invalid entry path: {}", entry.name()))?;
let outpath = dest_dir.join(enclosed);
// Handle directories and files
if entry.is_dir() {
std::fs::create_dir_all(&outpath)
.map_err(|e| format!("Failed to create directory {}: {}", outpath.display(), e))?;
} else {
if let Some(parent) = outpath.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
format!(
"Failed to create parent directory {}: {}",
parent.display(),
e
)
})?;
}
let mut outfile = File::create(&outpath)
.map_err(|e| format!("Failed to create file {}: {}", outpath.display(), e))?;
io::copy(&mut entry, &mut outfile)
.map_err(|e| format!("Failed to extract file {}: {}", outpath.display(), e))?;
// Set executable permissions on Unix-like systems based on stored mode
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = entry.unix_mode() {
let permissions = std::fs::Permissions::from_mode(mode);
std::fs::set_permissions(&outpath, permissions)
.map_err(|e| format!("Failed to set permissions for {}: {}", outpath.display(), e))?;
}
}
}
}
log::info!("ZIP extraction completed.");
self.flatten_single_directory_archive(dest_dir)?;
log::info!("Searching for executable...");
self
.find_extracted_executable(dest_dir)
.await
.map_err(|e| format!("Failed to find executable after ZIP extraction: {e}").into())
}
pub async fn extract_tar_gz(
&self,
tar_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Extracting tar.gz archive: {}", tar_path.display());
std::fs::create_dir_all(dest_dir)?;
let file = File::open(tar_path)?;
let gz_decoder = flate2::read::GzDecoder::new(BufReader::new(file));
let mut archive = tar::Archive::new(gz_decoder);
archive.unpack(dest_dir)?;
// Set executable permissions for extracted files
self.set_executable_permissions_recursive(dest_dir).await?;
log::info!("tar.gz extraction completed.");
self.flatten_single_directory_archive(dest_dir)?;
log::info!("Searching for executable...");
self.find_extracted_executable(dest_dir).await
}
pub async fn extract_tar_bz2(
&self,
tar_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Extracting tar.bz2 archive: {}", tar_path.display());
std::fs::create_dir_all(dest_dir)?;
let file = File::open(tar_path)?;
let bz2_decoder = bzip2::read::BzDecoder::new(BufReader::new(file));
let mut archive = tar::Archive::new(bz2_decoder);
archive.unpack(dest_dir)?;
// Set executable permissions for extracted files
self.set_executable_permissions_recursive(dest_dir).await?;
log::info!("tar.bz2 extraction completed.");
self.flatten_single_directory_archive(dest_dir)?;
log::info!("Searching for executable...");
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>> {
log::info!("Extracting tar.xz archive: {}", tar_path.display());
std::fs::create_dir_all(dest_dir)?;
let file = File::open(tar_path)?;
let mut buf_reader = BufReader::new(file);
// Read the entire file into memory for lzma-rs
let mut compressed_data = Vec::new();
buf_reader.read_to_end(&mut compressed_data)?;
// Decompress using lzma-rs
let mut decompressed_data = Vec::new();
lzma_rs::xz_decompress(
&mut std::io::Cursor::new(compressed_data),
&mut decompressed_data,
)?;
// Create tar archive from decompressed data
let cursor = std::io::Cursor::new(decompressed_data);
let mut archive = tar::Archive::new(cursor);
archive.unpack(dest_dir)?;
// Set executable permissions for extracted files
self.set_executable_permissions_recursive(dest_dir).await?;
log::info!("tar.xz extraction completed.");
self.flatten_single_directory_archive(dest_dir)?;
log::info!("Searching for executable...");
self.find_extracted_executable(dest_dir).await
}
pub async fn extract_msi(
&self,
msi_path: &Path,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Extracting MSI archive: {}", msi_path.display());
std::fs::create_dir_all(dest_dir)?;
// Extract MSI in a separate scope to avoid Send issues
{
let mut extractor = msi_extract::MsiExtractor::from_path(msi_path)?;
extractor.to(dest_dir);
}
log::info!("MSI extraction completed.");
self.flatten_single_directory_archive(dest_dir)?;
log::info!("Searching for executable...");
self.find_extracted_executable(dest_dir).await
}
#[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>> {
std::fs::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_recursive(&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>> {
{
let _ = browser_type;
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)
}
}
fn flatten_single_directory_archive(
&self,
dest_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let entries: Vec<_> = fs::read_dir(dest_dir)?.filter_map(|e| e.ok()).collect();
let archive_extensions = ["zip", "tar", "xz", "gz", "bz2", "dmg", "msi", "exe"];
let mut dirs = Vec::new();
let mut has_non_archive_files = false;
for entry in &entries {
let path = entry.path();
if path.is_dir() {
dirs.push(path);
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if !archive_extensions.contains(&ext.to_lowercase().as_str()) {
has_non_archive_files = true;
}
} else {
has_non_archive_files = true;
}
}
if dirs.len() == 1 && !has_non_archive_files {
let single_dir = &dirs[0];
if single_dir.extension().is_some_and(|ext| ext == "app") {
log::info!(
"Skipping flatten: {} is a macOS app bundle",
single_dir.display()
);
return Ok(());
}
log::info!(
"Flattening single-directory archive: moving contents of {} to {}",
single_dir.display(),
dest_dir.display()
);
let inner_entries: Vec<_> = fs::read_dir(single_dir)?.filter_map(|e| e.ok()).collect();
for entry in inner_entries {
let source = entry.path();
let file_name = match source.file_name() {
Some(name) => name.to_owned(),
None => continue,
};
let target = dest_dir.join(&file_name);
fs::rename(&source, &target).map_err(|e| {
format!(
"Failed to move {} to {}: {}",
source.display(),
target.display(),
e
)
})?;
}
fs::remove_dir(single_dir).map_err(|e| {
format!(
"Failed to remove empty directory {}: {}",
single_dir.display(),
e
)
})?;
log::info!("Successfully flattened archive directory structure");
}
Ok(())
}
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")]
{
let result = self.find_linux_executable(dest_dir).await;
// If we found an executable, ensure it's in the correct directory structure
// that the verification expects
if let Ok(exe_path) = &result {
self
.ensure_correct_directory_structure(dest_dir, exe_path)
.await?;
}
result
}
}
#[cfg(target_os = "macos")]
async fn find_macos_app(
&self,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Searching for .app bundle in: {}", dest_dir.display());
// Use the enhanced recursive search
match self.find_app_in_directory(dest_dir).await {
Ok(app_path) => {
// Check if the app is in a subdirectory and move it to the root if needed
let app_parent = app_path.parent().unwrap();
if app_parent != dest_dir {
log::info!(
"Found .app in subdirectory, moving to root: {} -> {}",
app_path.display(),
dest_dir.display()
);
let target_path = dest_dir.join(app_path.file_name().unwrap());
// Move the app to the root destination directory
fs::rename(&app_path, &target_path)?;
// Try to clean up the now-empty subdirectory (ignore errors)
if let Some(parent_dir) = app_path.parent() {
if parent_dir != dest_dir {
let _ = fs::remove_dir_all(parent_dir);
}
}
log::info!("Successfully moved .app to: {}", target_path.display());
Ok(target_path)
} else {
log::info!("Found .app at root level: {}", app_path.display());
Ok(app_path)
}
}
Err(e) => {
log::info!("Failed to find .app bundle: {e}");
Err("No .app found after extraction".into())
}
}
}
#[cfg(target_os = "windows")]
async fn find_windows_executable(
&self,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!(
"Searching for Windows executable in: {}",
dest_dir.display()
);
// Look for .exe files, preferring main browser executables
let priority_exe_names = [
"firefox.exe",
"chrome.exe",
"chromium.exe",
"camoufox.exe",
"wayfern.exe",
];
// First try priority executable names
for exe_name in &priority_exe_names {
let exe_path = dest_dir.join(exe_name);
if exe_path.exists() {
log::info!("Found priority executable: {}", exe_path.display());
return Ok(exe_path);
}
}
// Recursively search for executables with depth limit
match self.find_windows_executable_recursive(dest_dir, 0, 3).await {
Ok(exe_path) => {
log::info!(
"Found executable via recursive search: {}",
exe_path.display()
);
Ok(exe_path)
}
Err(_) => Err("No executable found after extraction".into()),
}
}
#[cfg(target_os = "windows")]
#[allow(clippy::type_complexity)]
fn find_windows_executable_recursive<'a>(
&'a self,
dir: &'a Path,
depth: usize,
max_depth: usize,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<PathBuf, Box<dyn std::error::Error + Send + Sync>>>
+ Send
+ 'a,
>,
> {
Box::pin(async move {
if depth > max_depth {
return Err("Maximum search depth reached".into());
}
if let Ok(entries) = fs::read_dir(dir) {
let mut dirs_to_search = Vec::new();
// First pass: look for .exe files in current directory
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
{
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
// Check if it's a browser executable
if file_name.contains("firefox")
|| file_name.contains("chrome")
|| file_name.contains("chromium")
|| file_name.contains("browser")
|| file_name.contains("camoufox")
|| file_name.contains("wayfern")
{
return Ok(path);
}
} else if path.is_dir() {
// Collect directories for later search
dirs_to_search.push(path);
}
}
// Second pass: search subdirectories
for subdir in dirs_to_search {
if let Ok(result) = self
.find_windows_executable_recursive(&subdir, depth + 1, max_depth)
.await
{
return Ok(result);
}
}
// Third pass: if no browser-specific executable found, return any .exe
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
{
return Ok(path);
}
}
}
}
Err("No executable found".into())
})
}
#[cfg(target_os = "linux")]
async fn find_linux_executable(
&self,
dest_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
log::info!("Searching for Linux executable in: {}", dest_dir.display());
// Enhanced list of common browser executable names
let exe_names = [
// Firefox variants (used by Camoufox)
"firefox",
"firefox-bin",
// Chrome/Chromium variants (used by Wayfern)
"chrome",
"chromium",
"chromium-browser",
"chromium-bin",
// Camoufox variants
"camoufox",
"camoufox-bin",
"camoufox-browser",
// Wayfern variants
"wayfern",
"wayfern-bin",
"wayfern-browser",
];
// First, try direct lookup in the main directory
for exe_name in &exe_names {
let exe_path = dest_dir.join(exe_name);
if exe_path.exists() && self.is_executable(&exe_path) {
log::info!("Found executable at root level: {}", exe_path.display());
return Ok(exe_path);
}
}
// Enhanced list of common Linux subdirectories to search
let subdirs = [
"bin",
"usr/bin",
"usr/local/bin",
"opt",
"sbin",
"usr/sbin",
"firefox",
"chrome",
"chromium",
"camoufox",
"wayfern",
".",
"./",
"Browser",
"browser",
"opt/camoufox",
"usr/lib/firefox",
"usr/lib/chromium",
"usr/lib/camoufox",
"usr/share/applications",
"usr/bin",
"AppRun",
];
// Search in subdirectories
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 {
let exe_path = subdir_path.join(exe_name);
if exe_path.exists() && self.is_executable(&exe_path) {
log::info!("Found executable in subdirectory: {}", exe_path.display());
return Ok(exe_path);
}
}
}
}
// Look for AppImage files
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) {
log::info!("Found AppImage: {}", path.display());
return Ok(path);
}
}
}
}
// Last resort: recursive search for any executable file
log::info!("Performing recursive search for executables...");
match self.find_any_executable_recursive(dest_dir, 0).await {
Ok(path) => {
log::info!("Found executable via recursive search: {}", path.display());
Ok(path)
}
Err(e) => {
// List all files in the directory for debugging
log::info!("Failed to find executable. Directory contents:");
if let Ok(entries) = fs::read_dir(dest_dir) {
for entry in entries.flatten() {
let path = entry.path();
let is_exec = if path.is_file() {
self.is_executable(&path)
} else {
false
};
log::info!(" {} (executable: {})", path.display(), is_exec);
}
}
Err(
format!(
"No executable found in {} after extraction. Original error: {}",
dest_dir.display(),
e
)
.into(),
)
}
}
}
#[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 recursively for all files in a directory
#[cfg(unix)]
async fn set_executable_permissions_recursive(
&self,
dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::os::unix::fs::PermissionsExt;
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
// Check if file looks like it should be executable
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("camoufox")
|| name_lower.contains("wayfern")
|| name_lower.ends_with(".appimage")
|| !name_lower.contains('.')
{
// Likely an executable, set permissions
let mut permissions = path.metadata()?.permissions();
let current_mode = permissions.mode();
let new_mode = current_mode | 0o755; // rwxr-xr-x
permissions.set_mode(new_mode);
std::fs::set_permissions(&path, permissions)?;
}
}
} else if path.is_dir() {
// Recursively process subdirectories
Box::pin(self.set_executable_permissions_recursive(&path)).await?;
}
}
}
Ok(())
}
#[cfg(not(unix))]
async fn set_executable_permissions_recursive(
&self,
_dir: &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();
let mut potential_executables = 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("camoufox")
|| name_lower.contains("wayfern")
|| file_name.ends_with(".AppImage")
{
log::info!(
"Found priority executable at depth {}: {}",
depth,
path.display()
);
return Ok(path);
}
// Collect other executables as potential candidates
potential_executables.push(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);
}
}
// Third pass: if no browser-specific executable found, try any executable
if !potential_executables.is_empty() {
// Sort by filename to prefer more likely candidates
potential_executables.sort_by(|a, b| {
let a_name = a
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase();
let b_name = b
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase();
// Prefer shorter names (likely main executables)
a_name.len().cmp(&b_name.len())
});
log::info!(
"Found potential executable at depth {}: {}",
depth,
potential_executables[0].display()
);
return Ok(potential_executables[0].clone());
}
}
Err(format!("No executable found in directory: {}", dir.display()).into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
#[cfg(target_os = "macos")]
use std::fs::create_dir_all;
#[test]
fn test_format_detection_zip() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
// Create a file with ZIP magic number
let mut file = File::create(&zip_path).unwrap();
file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap(); // ZIP magic
file.write_all(&[0; 8]).unwrap(); // padding
let result = extractor.detect_file_format(&zip_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "zip");
}
#[test]
fn test_format_detection_dmg_by_extension() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let dmg_path = temp_dir.path().join("test.dmg");
// Create a file (magic number won't match, but extension will)
let mut file = File::create(&dmg_path).unwrap();
file.write_all(b"fake dmg content").unwrap();
let result = extractor.detect_file_format(&dmg_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "dmg");
}
#[test]
fn test_format_detection_exe() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let exe_path = temp_dir.path().join("test.exe");
// Create a file with PE header
let mut file = File::create(&exe_path).unwrap();
file.write_all(&[0x4D, 0x5A]).unwrap(); // PE magic
file.write_all(&[0; 10]).unwrap(); // padding
let result = extractor.detect_file_format(&exe_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "exe");
}
#[test]
fn test_format_detection_tar_gz() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let tar_gz_path = temp_dir.path().join("test.tar.gz");
// Create a file with gzip magic
let mut file = File::create(&tar_gz_path).unwrap();
file.write_all(&[0x1F, 0x8B, 0x08]).unwrap(); // gzip magic
file.write_all(&[0; 9]).unwrap(); // padding
let result = extractor.detect_file_format(&tar_gz_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "tar.gz");
}
#[test]
fn test_format_detection_tar_bz2() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let tar_bz2_path = temp_dir.path().join("test.tar.bz2");
// Create a file with bzip2 magic
let mut file = File::create(&tar_bz2_path).unwrap();
file.write_all(&[0x42, 0x5A, 0x68]).unwrap(); // bzip2 magic
file.write_all(&[0; 9]).unwrap(); // padding
let result = extractor.detect_file_format(&tar_bz2_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "tar.bz2");
}
#[test]
fn test_format_detection_tar_xz() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let tar_xz_path = temp_dir.path().join("test.tar.xz");
// Create a file with xz magic
let mut file = File::create(&tar_xz_path).unwrap();
file
.write_all(&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])
.unwrap(); // xz magic
file.write_all(&[0; 6]).unwrap(); // padding
let result = extractor.detect_file_format(&tar_xz_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "tar.xz");
}
#[test]
fn test_format_detection_msi() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let msi_path = temp_dir.path().join("test.msi");
// Create a file with MSI magic
let mut file = File::create(&msi_path).unwrap();
file
.write_all(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])
.unwrap(); // MSI magic
file.write_all(&[0; 4]).unwrap(); // padding
let result = extractor.detect_file_format(&msi_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "msi");
}
#[test]
fn test_format_detection_msi_by_extension() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let msi_path = temp_dir.path().join("test.msi");
// Create a file (magic number won't match, but extension will)
let mut file = File::create(&msi_path).unwrap();
file.write_all(b"fake msi content").unwrap();
let result = extractor.detect_file_format(&msi_path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "msi");
}
#[tokio::test]
async fn test_extract_zip_with_test_archive() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let dest_dir = temp_dir.path().join("extracted");
// Create a test ZIP archive in memory
let zip_path = temp_dir.path().join("test.zip");
{
let file = std::fs::File::create(&zip_path).expect("Failed to create test zip file");
let mut zip = zip::ZipWriter::new(file);
let options =
zip::write::FileOptions::<()>::default().compression_method(zip::CompressionMethod::Stored);
zip
.start_file("test.txt", options)
.expect("Failed to start zip file");
zip
.write_all(b"Hello, World!")
.expect("Failed to write to zip");
zip.finish().expect("Failed to finish zip");
}
let result = extractor.extract_zip(&zip_path, &dest_dir).await;
// The result might fail because we're looking for executables, but the extraction should work
// Let's check if the file was extracted regardless of the result
let extracted_file = dest_dir.join("test.txt");
assert!(extracted_file.exists(), "Extracted file should exist");
let content = std::fs::read_to_string(&extracted_file).expect("Failed to read extracted file");
assert_eq!(
content.trim(),
"Hello, World!",
"Extracted content should match"
);
// If the result is an error, it should be because no executable was found, not extraction failure
if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("No executable found") || error_msg.contains("executable"),
"Error should be about missing executable, not extraction failure: {error_msg}"
);
}
}
#[tokio::test]
async fn test_extract_tar_gz_with_test_archive() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let dest_dir = temp_dir.path().join("extracted");
// Create a test tar.gz archive in memory
let tar_gz_path = temp_dir.path().join("test.tar.gz");
{
let tar_gz_file =
std::fs::File::create(&tar_gz_path).expect("Failed to create test tar.gz file");
let enc = flate2::write::GzEncoder::new(tar_gz_file, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
let mut header = tar::Header::new_gnu();
header.set_path("test.txt").expect("Failed to set tar path");
header.set_size(13); // "Hello, World!" length
header.set_cksum();
tar
.append(&header, "Hello, World!".as_bytes())
.expect("Failed to append to tar");
tar.finish().expect("Failed to finish tar");
}
let result = extractor.extract_tar_gz(&tar_gz_path, &dest_dir).await;
// Check if the file was extracted
let extracted_file = dest_dir.join("test.txt");
assert!(extracted_file.exists(), "Extracted file should exist");
let content = std::fs::read_to_string(&extracted_file).expect("Failed to read extracted file");
assert_eq!(
content.trim(),
"Hello, World!",
"Extracted content should match"
);
// If the result is an error, it should be because no executable was found, not extraction failure
if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("No executable found")
|| error_msg.contains("executable")
|| error_msg.contains("No .app found")
|| error_msg.contains("app not found"),
"Error should be about missing executable/app, not extraction failure: {error_msg}"
);
}
}
#[tokio::test]
async fn test_extract_tar_bz2_with_test_archive() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let dest_dir = temp_dir.path().join("extracted");
// Create a test tar.bz2 archive in memory
let tar_bz2_path = temp_dir.path().join("test.tar.bz2");
{
let tar_bz2_file =
std::fs::File::create(&tar_bz2_path).expect("Failed to create test tar.bz2 file");
let enc = bzip2::write::BzEncoder::new(tar_bz2_file, bzip2::Compression::default());
let mut tar = tar::Builder::new(enc);
let mut header = tar::Header::new_gnu();
header.set_path("test.txt").expect("Failed to set tar path");
header.set_size(13); // "Hello, World!" length
header.set_cksum();
tar
.append(&header, "Hello, World!".as_bytes())
.expect("Failed to append to tar");
tar.finish().expect("Failed to finish tar");
}
let result = extractor.extract_tar_bz2(&tar_bz2_path, &dest_dir).await;
// Check if the file was extracted
let extracted_file = dest_dir.join("test.txt");
assert!(extracted_file.exists(), "Extracted file should exist");
let content = std::fs::read_to_string(&extracted_file).expect("Failed to read extracted file");
assert_eq!(
content.trim(),
"Hello, World!",
"Extracted content should match"
);
// If the result is an error, it should be because no executable was found, not extraction failure
if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("No executable found")
|| error_msg.contains("executable")
|| error_msg.contains("No .app found")
|| error_msg.contains("app not found"),
"Error should be about missing executable/app, not extraction failure: {error_msg}"
);
}
}
#[tokio::test]
async fn test_extract_tar_xz_with_test_archive() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let dest_dir = temp_dir.path().join("extracted");
// Create a test tar.xz archive in memory
let tar_xz_path = temp_dir.path().join("test.tar.xz");
{
// First create a tar archive in memory
let mut tar_data = Vec::new();
{
let mut tar = tar::Builder::new(&mut tar_data);
let mut header = tar::Header::new_gnu();
header.set_path("test.txt").expect("Failed to set tar path");
header.set_size(13); // "Hello, World!" length
header.set_cksum();
tar
.append(&header, "Hello, World!".as_bytes())
.expect("Failed to append to tar");
tar.finish().expect("Failed to finish tar");
}
// Then compress with xz
let tar_xz_file =
std::fs::File::create(&tar_xz_path).expect("Failed to create test tar.xz file");
let mut compressed_data = Vec::new();
lzma_rs::xz_compress(&mut std::io::Cursor::new(tar_data), &mut compressed_data)
.expect("Failed to compress with xz");
std::io::Write::write_all(&mut std::io::BufWriter::new(tar_xz_file), &compressed_data)
.expect("Failed to write compressed data");
}
let result = extractor.extract_tar_xz(&tar_xz_path, &dest_dir).await;
// Check if the file was extracted
let extracted_file = dest_dir.join("test.txt");
assert!(extracted_file.exists(), "Extracted file should exist");
let content = std::fs::read_to_string(&extracted_file).expect("Failed to read extracted file");
assert_eq!(
content.trim(),
"Hello, World!",
"Extracted content should match"
);
// If the result is an error, it should be because no executable was found, not extraction failure
if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("No executable found")
|| error_msg.contains("executable")
|| error_msg.contains("No .app found")
|| error_msg.contains("app not found"),
"Error should be about missing executable/app, not extraction failure: {error_msg}"
);
}
}
#[test]
fn test_unsupported_archive_format() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
let fake_archive = temp_dir.path().join("test.rar");
// Create a file with invalid header
let mut file = File::create(&fake_archive).unwrap();
file.write_all(b"invalid content").unwrap();
// Test format detection
let result = extractor.detect_file_format(&fake_archive);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "unknown");
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn test_find_app_at_root_level() {
let extractor = Extractor::instance();
let temp_dir = TempDir::new().unwrap();
// Create a Firefox.app directory
let firefox_app = temp_dir.path().join("Firefox.app");
create_dir_all(&firefox_app).unwrap();
// Create the standard macOS app structure
let contents_dir = firefox_app.join("Contents");
let macos_dir = contents_dir.join("MacOS");
create_dir_all(&macos_dir).unwrap();
// Create the executable
let executable = macos_dir.join("firefox");
File::create(&executable).unwrap();
// Test finding the app
let result = extractor.find_app_in_directory(temp_dir.path()).await;
assert!(result.is_ok());
let found_app = result.unwrap();
assert_eq!(found_app.file_name().unwrap(), "Firefox.app");
assert!(found_app.exists());
}
#[test]
fn test_is_executable() {
#[allow(unused_variables)]
let extractor = Extractor::instance();
let temp_dir = TempDir::new().expect("Failed to create temp directory");
// Create a regular file
let regular_file = temp_dir.path().join("regular.txt");
File::create(&regular_file).expect("Failed to create test file");
#[cfg(target_os = "linux")]
{
// Should not be executable initially
assert!(
!extractor.is_executable(&regular_file),
"File should not be executable initially"
);
// Make it executable
use std::os::unix::fs::PermissionsExt;
let mut permissions = regular_file
.metadata()
.expect("Failed to get file metadata")
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&regular_file, permissions).expect("Failed to set permissions");
// Should now be executable
assert!(
extractor.is_executable(&regular_file),
"File should be executable after setting permissions"
);
}
#[cfg(not(target_os = "linux"))]
{
// On non-Linux systems, the is_executable method is not available
// We'll just verify the file exists since executable permissions work differently on Windows/macOS
assert!(regular_file.exists(), "Test file should exist");
// On Unix systems (but not Linux), we can still test basic permission setting
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = regular_file
.metadata()
.expect("Failed to get file metadata")
.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&regular_file, permissions).expect("Failed to set permissions");
// Verify the permissions were set
let new_permissions = regular_file
.metadata()
.expect("Failed to get updated metadata")
.permissions();
assert_eq!(
new_permissions.mode() & 0o777,
0o755,
"Permissions should be set to 755"
);
}
}
}
}
// Global singleton instance
lazy_static::lazy_static! {
static ref EXTRACTOR: Extractor = Extractor::new();
}