use super::installer::DependencyBinaryInstallError;
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
#[cfg_attr(test, mockall::automock)]
pub trait DependencyArchiveExtractor {
fn extract_binary(
&self,
archive_path: &Path,
expected_member_path: &str,
destination_dir: &Path,
) -> Result<PathBuf, DependencyBinaryInstallError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct RepositoryArchiveExtractor;
impl DependencyArchiveExtractor for RepositoryArchiveExtractor {
fn extract_binary(
&self,
archive_path: &Path,
expected_member_path: &str,
destination_dir: &Path,
) -> Result<PathBuf, DependencyBinaryInstallError> {
if archive_path
.extension()
.is_some_and(|extension| extension == "zip")
{
return extract_from_zip(archive_path, expected_member_path, destination_dir);
}
extract_from_tgz(archive_path, expected_member_path, destination_dir)
}
}
pub(crate) fn extract_from_tgz(
archive_path: &Path,
expected_member_path: &str,
destination_dir: &Path,
) -> Result<PathBuf, DependencyBinaryInstallError> {
let file = File::open(archive_path)?;
let decoder = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(decoder);
let map_archive_err = |error: io::Error| DependencyBinaryInstallError::Extraction {
archive: archive_path.to_path_buf(),
reason: error.to_string(),
};
for entry in archive.entries().map_err(map_archive_err)? {
let mut entry = entry.map_err(map_archive_err)?;
let path = entry.path().map_err(map_archive_err)?.into_owned();
if path == Path::new(expected_member_path) {
return extract_entry_to_destination(&mut entry, expected_member_path, destination_dir);
}
}
Err(DependencyBinaryInstallError::MissingBinaryInArchive {
binary: expected_member_path.to_owned(),
})
}
pub(crate) fn extract_from_zip(
archive_path: &Path,
expected_member_path: &str,
destination_dir: &Path,
) -> Result<PathBuf, DependencyBinaryInstallError> {
let file = File::open(archive_path)?;
let mut archive =
zip::ZipArchive::new(file).map_err(|error| DependencyBinaryInstallError::Extraction {
archive: archive_path.to_path_buf(),
reason: error.to_string(),
})?;
for index in 0..archive.len() {
let mut file =
archive
.by_index(index)
.map_err(|error| DependencyBinaryInstallError::Extraction {
archive: archive_path.to_path_buf(),
reason: error.to_string(),
})?;
if file.name() != expected_member_path {
continue;
}
return extract_entry_to_destination(&mut file, expected_member_path, destination_dir);
}
Err(DependencyBinaryInstallError::MissingBinaryInArchive {
binary: expected_member_path.to_owned(),
})
}
fn extract_entry_to_destination(
reader: &mut dyn Read,
expected_member_path: &str,
destination_dir: &Path,
) -> Result<PathBuf, DependencyBinaryInstallError> {
let binary_name = Path::new(expected_member_path).file_name().ok_or_else(|| {
DependencyBinaryInstallError::MissingBinaryInArchive {
binary: expected_member_path.to_owned(),
}
})?;
let destination = destination_dir.join(binary_name);
let mut temp_file = NamedTempFile::with_prefix_in(
format!(".tmp_{}", binary_name.to_string_lossy()),
destination_dir,
)
.map_err(DependencyBinaryInstallError::Io)?;
io::copy(reader, &mut temp_file)?;
temp_file.flush()?;
temp_file
.persist(&destination)
.map_err(|error| DependencyBinaryInstallError::Io(error.error))?;
Ok(destination)
}