extr 0.1.0

The archive extractor that makes unpacking files as easy as typing their name. No more memorizing complex flags or commands - just point extr at any archive and let the magic happen. Supporting 30+ formats, extr is the Swiss Army knife for all your file extraction needs.
use anyhow::{Context, Result};
use lazy_static::lazy_static;
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use std::process::Command;
use std::sync::RwLock;

use crate::extractor::arj::ArjExtractor;
use crate::extractor::bzip2::Bzip2Extractor;
use crate::extractor::cab::CabExtractor;
use crate::extractor::compress::CompressExtractor;
use crate::extractor::gzip::GzipExtractor;
use crate::extractor::iso::IsoExtractor;
use crate::extractor::lha::LhaExtractor;
use crate::extractor::lzip::LzipExtractor;
use crate::extractor::lzma::LzmaExtractor;
use crate::extractor::lzop::LzopExtractor;
use crate::extractor::pkg::DebRpmExtractor;
use crate::extractor::rar::RarExtractor;
use crate::extractor::sevenz::SevenzExtractor;
use crate::extractor::sfx::SfxExtractor;
use crate::extractor::tar::TarExtractor;
use crate::extractor::xz::XzExtractor;
use crate::extractor::zip::ZipExtractor;
use crate::extractor::zstd::ZstdExtractor;
use crate::trust::{is_trusted_bin_path, run_command};

pub mod arj;
pub mod bzip2;
pub mod cab;
pub mod compress;
pub mod gzip;
pub mod iso;
pub mod lha;
pub mod lzip;
pub mod lzma;
pub mod lzop;
pub mod pkg;
pub mod rar;
pub mod sevenz;
pub mod sfx;
pub mod tar;
pub mod xz;
pub mod zip;
pub mod zstd;

pub fn register_extractors() {
    register_extractor(&ArjExtractor);
    register_extractor(&Bzip2Extractor);
    register_extractor(&CabExtractor);
    register_extractor(&CompressExtractor);
    register_extractor(&GzipExtractor);
    register_extractor(&IsoExtractor);
    register_extractor(&LhaExtractor);
    register_extractor(&LzipExtractor);
    register_extractor(&LzmaExtractor);
    register_extractor(&LzopExtractor);
    register_extractor(&DebRpmExtractor);
    register_extractor(&RarExtractor);
    register_extractor(&SevenzExtractor);
    register_extractor(&SfxExtractor);
    register_extractor(&TarExtractor);
    register_extractor(&XzExtractor);
    register_extractor(&ZipExtractor);
    register_extractor(&ZstdExtractor);
}

pub trait Extractor: Send + Sync {
    fn file_extensions(&self) -> Vec<&str>;

    fn binary_names(&self) -> Vec<&'static str>;

    fn get_verified_binary(&self) -> Result<std::path::PathBuf> {
        self.binary_names()
            .into_iter()
            .find_map(|binary| {
                if let Ok(path) = which::which(binary) {
                    if is_trusted_bin_path(&path) {
                        return Some(path);
                    }
                    None
                } else {
                    None
                }
            })
            .context(format!(
                "💥 Whoops! No suitable extraction tool found on your system for {} archives. Please use your package manager to install one of: {}",
                self.file_extensions().join(", "),
                self.binary_names().join(", ")
            ))
    }

    fn build_command(
        &self,
        binary: &Path,
        file: &Path,
        output_dir: &Path,
        verbose: bool,
    ) -> Result<Command>;

    fn extract(&self, file: &Path, output_dir: &Path, verbose: bool) -> Result<()> {
        let binary = self.get_verified_binary()?;
        let cmd = self.build_command(&binary, file, output_dir, verbose)?;
        std::fs::create_dir_all(output_dir)?;
        run_command(cmd, verbose)
    }
}

lazy_static! {
    static ref EXTRACTORS: RwLock<HashMap<&'static str, &'static dyn Extractor>> =
        RwLock::new(HashMap::new());
}

pub fn get_health_info() -> Vec<(String, Vec<(String, bool)>)> {
    let extractors = EXTRACTORS.read().unwrap();
    let mut formats = BTreeMap::new();

    for (ext, extractor) in extractors.iter() {
        let binaries = extractor
            .binary_names()
            .into_iter()
            .map(|bin| {
                let exists = which::which(bin)
                    .map(|path| is_trusted_bin_path(&path))
                    .unwrap_or(false);
                (bin.to_string(), exists)
            })
            .collect();
        formats.insert(ext.to_string(), binaries);
    }

    formats.into_iter().collect()
}

pub fn register_extractor(extractor: &'static dyn Extractor) {
    let mut extractors = EXTRACTORS.write().unwrap();
    for ext in extractor.file_extensions() {
        extractors.insert(ext, extractor);
    }
}

pub fn get_extractor(path: &Path) -> Option<&'static dyn Extractor> {
    let extension = get_extension(path)?;
    let extractors = EXTRACTORS.read().unwrap();
    extractors.get(extension.as_str()).copied()
}

fn get_extension(path: &Path) -> Option<String> {
    let file_name = path.file_name()?.to_str()?;
    let parts: Vec<&str> = file_name.split('.').collect();

    match parts.as_slice() {
        [.., "tar", last] => Some(format!("{}.{}", "tar", last).to_lowercase()),
        [.., last] => Some(last.to_lowercase()),
        _ => None,
    }
}