use anyhow::{format_err, Result};
use std::convert::TryFrom;
use crate::common::config::Config;
use crate::extension::{common, process};
mod github;
pub fn add_from_url(
url: &url::Url,
extensions_bin_directory: &std::path::PathBuf,
) -> Result<String> {
let archive_url = if is_supported_archive_url(&url)? {
url.clone()
} else {
match get_archive_url(&url)? {
Some(url) => url,
None => {
return Err(format_err!(
"Failed to obtain suitable release archive URL."
))
}
}
};
log::info!("Using archive URL: {}", archive_url);
let archive_type = crate::common::fs::archive::ArchiveType::try_from(
&std::path::PathBuf::from(archive_url.path()),
)?;
let tmp_dir = tempdir::TempDir::new("thirdpass_extension_add")?;
let tmp_directory_path = tmp_dir.path().to_path_buf();
log::info!(
"Downloading extension archive to temporary directory: {}",
tmp_directory_path.display()
);
let archive_path =
tmp_directory_path.join(format!("archive.{}", archive_type.try_to_string()?));
crate::common::fs::archive::download(&archive_url, &archive_path)?;
crate::common::fs::archive::extract(&archive_path, &tmp_directory_path)?;
let (bin_path, extension_name) = get_bin_file_metadata(&tmp_directory_path)?.ok_or(
format_err!("Failed to identify extension binary in archive."),
)?;
log::info!(
"Identified binary for extension {}: {}",
extension_name,
bin_path.display()
);
let bin_file_name = bin_path
.file_name()
.and_then(|name| name.to_str())
.ok_or(format_err!("Failed to derive extension binary file name."))?;
let bin_destination_path = extensions_bin_directory.join(bin_file_name);
log::info!("Copying binary to path: {}", bin_destination_path.display());
std::fs::copy(&bin_path, &bin_destination_path)?;
ensure_executable_permissions(&bin_destination_path)?;
tmp_dir.close()?;
Ok(extension_name)
}
#[cfg(target_family = "unix")]
fn ensure_executable_permissions(path: &std::path::PathBuf) -> Result<()> {
log::debug!(
"Setting executable permissions to 755 for file: {}",
path.display()
);
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(&path, permissions)?;
Ok(())
}
#[cfg(not(target_family = "unix"))]
fn ensure_executable_permissions(_path: &std::path::PathBuf) -> Result<()> {
Ok(())
}
fn get_bin_file_metadata(
directory: &std::path::PathBuf,
) -> Result<Option<(std::path::PathBuf, String)>> {
let regex_pattern = get_bin_name_regex()?;
for entry in std::fs::read_dir(&directory)? {
let entry = entry?;
let path = entry.path();
if let Some(name) = get_name_from_bin(&path, ®ex_pattern)? {
return Ok(Some((path, name)));
}
}
Ok(None)
}
fn get_bin_name_regex() -> Result<regex::Regex> {
Ok(regex::Regex::new(
r"thirdpass-(?P<name>[a-zA-Z0-9]*)(\.exe)?$",
)?)
}
fn get_name_from_bin(
path: &std::path::PathBuf,
regex_pattern: ®ex::Regex,
) -> Result<Option<String>> {
if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
match regex_pattern.captures(file_name) {
Some(captures) => Ok(Some(captures["name"].to_string())),
None => Ok(None),
}
} else {
Ok(None)
}
}
fn is_supported_archive_url(url: &url::Url) -> Result<bool> {
let path = std::path::PathBuf::from(url.path());
Ok(crate::common::fs::archive::ArchiveType::try_from(&path)?
!= crate::common::fs::archive::ArchiveType::Unknown)
}
fn get_archive_url(url: &url::Url) -> Result<Option<url::Url>> {
Ok(if url.host_str() == Some("github.com") {
github::get_archive_url(&url)?
} else {
None
})
}
#[cfg(test)]
mod tests {
use super::*;
mod get_extension_bin_name {
use super::*;
#[test]
fn test_matching_file_name() -> Result<()> {
let regex_pattern = get_bin_name_regex()?;
let bin_path = std::path::PathBuf::from(
"/tmp/thirdpass-extension_add/thirdpass-python/thirdpass-py",
);
let result = get_name_from_bin(&bin_path, ®ex_pattern)?;
let expected = Some("py".to_string());
assert_eq!(result, expected);
Ok(())
}
#[test]
fn test_not_matching_file_name() -> Result<()> {
let regex_pattern = get_bin_name_regex()?;
let bin_path = std::path::PathBuf::from(
"/tmp/thirdpass-extension_add/thirdpass-python/thirdpass-py.d",
);
let result = get_name_from_bin(&bin_path, ®ex_pattern)?;
let expected = None;
assert_eq!(result, expected);
Ok(())
}
}
}
pub fn update_config(config: &mut Config) -> Result<()> {
log::debug!("Discover extensions and update config.");
let extensions = process::get_all()?;
let extension_name_map: std::collections::BTreeMap<_, _> = extensions
.iter()
.map(|extension| (extension.name(), extension))
.collect();
let all_found_names: std::collections::BTreeSet<_> =
extension_name_map.keys().cloned().collect();
let configured_names: std::collections::BTreeSet<_> = config
.extensions
.enabled
.keys()
.map(|name| name.clone())
.collect();
let stale_names: Vec<_> = configured_names.difference(&all_found_names).collect();
let registries_map = config.extensions.registries.clone();
for name in &stale_names {
config.extensions.enabled.remove(*name);
for (registry, extension_name) in ®istries_map {
if *extension_name == **name {
config.extensions.registries.remove(registry);
}
}
}
let new_names: Vec<_> = all_found_names.difference(&configured_names).collect();
for name in &new_names {
config.extensions.enabled.insert((**name).clone(), true);
if let Some(extension) = extension_name_map.get(name.as_str()) {
for registry in extension.registries() {
config
.extensions
.registries
.insert(registry, (*name).clone());
}
}
}
if !stale_names.is_empty() || !new_names.is_empty() {
config.dump()?;
}
Ok(())
}
pub fn enable(name: &str, config: &mut Config) -> Result<()> {
if let Some(enabled_status) = config.extensions.enabled.get_mut(&name.to_string()) {
*enabled_status = true;
config.dump()?;
Ok(())
} else {
Err(format_err!("Failed to find extension."))
}
}
pub fn disable(name: &str, config: &mut Config) -> Result<()> {
if let Some(enabled_status) = config.extensions.enabled.get_mut(&name.to_string()) {
*enabled_status = false;
config.dump()?;
Ok(())
} else {
Err(format_err!("Failed to find extension."))
}
}
pub fn remove(name: &str) -> Result<()> {
let mut config = Config::load()?;
update_config(&mut config)?;
let all_extension_names = get_all_names(&config)?;
if !all_extension_names.contains(name) {
return Err(format_err!(
"Failed to find extension. Known extensions: {}",
all_extension_names
.into_iter()
.collect::<Vec<_>>()
.join(", ")
));
}
let path = common::get_config_path(&name)?;
if path.is_file() {
log::info!("Removing extension config file: {}", path.display());
std::fs::remove_file(&path)?;
}
let extension_paths = process::get_extension_paths()?;
if let Some(path) = extension_paths.get(name) {
log::info!("Deleting extension bin file: {}", path.display());
std::fs::remove_file(&path)?;
}
update_config(&mut config)?;
Ok(())
}
pub fn is_enabled(name: &str, config: &Config) -> Result<bool> {
Ok(*config.extensions.enabled.get(name).unwrap_or(&false))
}
pub fn get_enabled(
names: &std::collections::BTreeSet<String>,
config: &Config,
) -> Result<Vec<Box<dyn thirdpass_core::extension::Extension>>> {
log::debug!("Identifying enabled extensions.");
let extensions = process::get_all()?
.into_iter()
.filter(|extension| {
*config
.extensions
.enabled
.get(&extension.name())
.unwrap_or(&false)
})
.filter(|extension| names.contains(&extension.name()))
.collect();
Ok(extensions)
}
pub fn get_enabled_names(config: &Config) -> Result<std::collections::BTreeSet<String>> {
Ok(config
.extensions
.enabled
.iter()
.filter(|(_name, enabled_flag)| **enabled_flag)
.map(|(name, _enabled_flag)| name.clone())
.collect())
}
pub fn get_all_names(config: &Config) -> Result<std::collections::BTreeSet<String>> {
Ok(config
.extensions
.enabled
.iter()
.map(|(name, _enabled_flag)| name.clone())
.collect())
}
pub fn handle_extension_names_arg(
extension_names: &Option<Vec<String>>,
config: &Config,
) -> Result<std::collections::BTreeSet<String>> {
let names = match &extension_names {
Some(extension_names) => {
let disabled_names: Vec<_> = extension_names
.iter()
.cloned()
.filter(|name| !is_enabled(&name, &config).unwrap_or(false))
.collect();
if !disabled_names.is_empty() {
return Err(format_err!(
"The following disabled extensions were given: {}",
disabled_names.join(", ")
));
} else {
extension_names.into_iter().cloned().collect()
}
}
None => get_enabled_names(&config)?,
};
log::debug!("Using extensions: {:?}", names);
Ok(names)
}
pub fn clean_name(name: &str) -> String {
match &name.strip_prefix(process::EXTENSION_FILE_NAME_PREFIX) {
Some(name) => name.to_string(),
None => name.to_string(),
}
}