use crate::{progress::ProgressContext, Error, Result};
use std::path::{Path, PathBuf};
pub mod binary;
pub mod tar;
pub mod zip;
#[async_trait::async_trait]
pub trait FormatHandler: Send + Sync {
fn name(&self) -> &str;
fn can_handle(&self, file_path: &Path) -> bool;
async fn extract(
&self,
source_path: &Path,
target_dir: &Path,
progress: &ProgressContext,
) -> Result<Vec<PathBuf>>;
fn get_executable_name(&self, tool_name: &str) -> String {
if cfg!(windows) {
format!("{}.exe", tool_name)
} else {
tool_name.to_string()
}
}
fn find_executables(&self, dir: &Path, tool_name: &str) -> Result<Vec<PathBuf>> {
let exe_name = self.get_executable_name(tool_name);
let mut executables = Vec::new();
let search_paths = vec![
dir.to_path_buf(),
dir.join("bin"),
dir.join("usr").join("bin"),
dir.join("usr").join("local").join("bin"),
];
for search_path in search_paths {
if !search_path.exists() {
continue;
}
let exe_path = search_path.join(&exe_name);
if exe_path.exists() && exe_path.is_file() {
executables.push(exe_path);
continue;
}
if let Ok(entries) = std::fs::read_dir(&search_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if filename == exe_name
|| (filename.starts_with(tool_name) && self.is_executable(&path))
{
executables.push(path);
}
}
}
}
}
}
if executables.is_empty() {
return Err(Error::executable_not_found(tool_name, dir));
}
Ok(executables)
}
fn is_executable(&self, path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path) {
let permissions = metadata.permissions();
permissions.mode() & 0o111 != 0
} else {
false
}
}
#[cfg(windows)]
{
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com")
} else {
false
}
}
#[cfg(not(any(unix, windows)))]
{
path.is_file()
}
}
#[cfg(unix)]
fn make_executable(&self, path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(not(unix))]
fn make_executable(&self, _path: &Path) -> Result<()> {
Ok(())
}
}
pub struct ArchiveExtractor {
handlers: Vec<Box<dyn FormatHandler>>,
}
impl ArchiveExtractor {
pub fn new() -> Self {
let handlers: Vec<Box<dyn FormatHandler>> = vec![
Box::new(zip::ZipHandler::new()),
Box::new(tar::TarHandler::new()),
Box::new(binary::BinaryHandler::new()),
];
Self { handlers }
}
pub fn with_handler(mut self, handler: Box<dyn FormatHandler>) -> Self {
self.handlers.push(handler);
self
}
pub async fn extract(
&self,
source_path: &Path,
target_dir: &Path,
progress: &ProgressContext,
) -> Result<Vec<PathBuf>> {
for handler in &self.handlers {
if handler.can_handle(source_path) {
return handler.extract(source_path, target_dir, progress).await;
}
}
Err(Error::unsupported_format(
source_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("unknown"),
))
}
pub fn find_best_executable(
&self,
extracted_files: &[PathBuf],
tool_name: &str,
) -> Result<PathBuf> {
let exe_name = if cfg!(windows) {
format!("{}.exe", tool_name)
} else {
tool_name.to_string()
};
for file in extracted_files {
if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
if filename == exe_name {
return Ok(file.clone());
}
}
}
for file in extracted_files {
if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
if filename.starts_with(tool_name) && self.is_executable_file(file) {
return Ok(file.clone());
}
}
}
for file in extracted_files {
if let Some(parent) = file.parent() {
if let Some(dir_name) = parent.file_name().and_then(|n| n.to_str()) {
if dir_name == "bin" && self.is_executable_file(file) {
return Ok(file.clone());
}
}
}
}
Err(Error::executable_not_found(
tool_name,
extracted_files
.first()
.and_then(|p| p.parent())
.unwrap_or_else(|| Path::new(".")),
))
}
fn is_executable_file(&self, path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path) {
let permissions = metadata.permissions();
permissions.mode() & 0o111 != 0
} else {
false
}
}
#[cfg(windows)]
{
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com")
} else {
false
}
}
#[cfg(not(any(unix, windows)))]
{
path.is_file()
}
}
}
impl Default for ArchiveExtractor {
fn default() -> Self {
Self::new()
}
}
pub fn detect_format(file_path: &Path) -> Option<&str> {
let filename = file_path.file_name()?.to_str()?;
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
Some("tar.gz")
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
Some("tar.xz")
} else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
Some("tar.bz2")
} else if filename.ends_with(".zip") {
Some("zip")
} else if filename.ends_with(".7z") {
Some("7z")
} else {
file_path.extension()?.to_str()
}
}