use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::ida::ida_user_dir;
use crate::plugin::metadata::{is_ida_version_compatible, is_platform_compatible, PluginMetadata};
pub fn plugins_dir() -> PathBuf {
ida_user_dir().join("plugins")
}
pub fn is_installed(name: &str) -> bool {
let path = plugins_dir().join(name);
path.exists() || std::fs::symlink_metadata(&path).is_ok()
}
#[derive(Debug, Clone)]
pub struct InstalledPlugin {
pub name: String,
pub version: Option<String>,
pub editable: bool,
}
pub fn installed_plugins() -> Result<Vec<InstalledPlugin>> {
let dir = plugins_dir();
if !dir.exists() {
return Ok(Vec::new());
}
let mut plugins = Vec::new();
for entry in std::fs::read_dir(&dir)?.flatten() {
if !entry.path().is_dir() {
continue;
}
let editable = std::fs::symlink_metadata(entry.path())
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let name = entry.file_name().to_string_lossy().to_string();
let version = read_installed_version(&entry.path());
plugins.push(InstalledPlugin {
name,
version,
editable,
});
}
plugins.sort_by(|a, b| a.name.cmp(&b.name));
Ok(plugins)
}
fn read_installed_version(plugin_dir: &Path) -> Option<String> {
let manifest_path = plugin_dir.join("ida-plugin.json");
if !manifest_path.exists() {
return None;
}
let text = std::fs::read_to_string(&manifest_path).ok()?;
let val: serde_json::Value = serde_json::from_str(&text).ok()?;
val.get("version")
.and_then(|v| v.as_str())
.map(String::from)
}
pub fn validate_can_install(metadata: &PluginMetadata, ida_version: Option<&str>) -> Result<()> {
if metadata.name.is_empty()
|| metadata.name.contains('/')
|| metadata.name.contains('\\')
|| metadata.name == "."
|| metadata.name == ".."
{
return Err(Error::InvalidPluginName(metadata.name.clone()));
}
if is_installed(&metadata.name) {
return Err(Error::PluginAlreadyInstalled(metadata.name.clone()));
}
if !is_platform_compatible(metadata) {
return Err(Error::PlatformIncompatible(format!(
"Plugin '{}' is not compatible with this platform",
metadata.name
)));
}
if let Some(ver) = ida_version
&& !is_ida_version_compatible(metadata, ver) {
return Err(Error::IdaVersionIncompatible(format!(
"Plugin '{}' is not compatible with IDA {}",
metadata.name, ver
)));
}
Ok(())
}
fn safe_join(base: &Path, entry_name: &str) -> Option<PathBuf> {
use std::path::Component;
let rel = Path::new(entry_name);
let mut out = base.to_path_buf();
for component in rel.components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
out.starts_with(base).then_some(out)
}
pub fn install_from_archive(
archive_path: &Path,
ida_version: Option<&str>,
force: bool,
) -> Result<PathBuf> {
let metadata = crate::plugin::metadata::read_metadata_from_archive(archive_path)?;
if !force {
validate_can_install(&metadata, ida_version)?;
}
let file = std::fs::File::open(archive_path)?;
let mut archive = zip::ZipArchive::new(file)?;
let target_dir = plugins_dir().join(&metadata.name);
for i in 0..archive.len() {
let entry = archive.by_index(i)?;
if entry.is_symlink() {
continue;
}
if safe_join(&target_dir, entry.name()).is_none() {
return Err(Error::PluginInstall(format!(
"archive contains an unsafe path: {}",
entry.name()
)));
}
}
std::fs::create_dir_all(&target_dir)?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let name = entry.name().to_owned();
if entry.is_symlink() {
continue;
}
let Some(out_path) = safe_join(&target_dir, &name) else {
continue; };
if entry.is_dir() {
std::fs::create_dir_all(&out_path)?;
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out_file = std::fs::File::create(&out_path)?;
std::io::copy(&mut entry, &mut out_file)?;
}
}
Ok(target_dir)
}
pub fn install_from_directory(
source_dir: &Path,
ida_version: Option<&str>,
force: bool,
) -> Result<PathBuf> {
let metadata = crate::plugin::metadata::read_metadata_from_directory(source_dir)?;
if !force {
validate_can_install(&metadata, ida_version)?;
}
let target_dir = plugins_dir().join(&metadata.name);
if force && (target_dir.exists() || std::fs::symlink_metadata(&target_dir).is_ok()) {
remove_plugin_dir(&target_dir)?;
}
copy_plugin_tree(source_dir, &target_dir)
.map_err(|e| Error::PluginInstall(format!("copy failed: {e}")))?;
Ok(target_dir)
}
fn copy_plugin_tree(src: &Path, dst: &Path) -> std::io::Result<()> {
const SKIP: &[&str] = &[".git", ".hg", ".svn", "__pycache__", ".venv", "venv", ".idea"];
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
if SKIP.contains(&name.to_string_lossy().as_ref()) {
continue;
}
let src_path = entry.path();
let dst_path = dst.join(&name);
if src_path.is_dir() {
copy_plugin_tree(&src_path, &dst_path)?;
} else if src_path.is_file() {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
pub fn install_editable(
source_dir: &Path,
ida_version: Option<&str>,
force: bool,
) -> Result<PathBuf> {
let source_dir = source_dir
.canonicalize()
.map_err(|e| Error::PluginInstall(format!("cannot resolve source directory: {e}")))?;
let metadata = crate::plugin::metadata::read_metadata_from_directory(&source_dir)?;
if !force {
validate_can_install(&metadata, ida_version)?;
}
let target = plugins_dir().join(&metadata.name);
if force && (target.exists() || std::fs::symlink_metadata(&target).is_ok()) {
remove_plugin_dir(&target)?;
}
std::fs::create_dir_all(plugins_dir())?;
#[cfg(unix)]
std::os::unix::fs::symlink(&source_dir, &target)
.map_err(|e| Error::PluginInstall(format!("symlink failed: {e}")))?;
#[cfg(windows)]
std::os::windows::fs::symlink_dir(&source_dir, &target).map_err(|e| {
Error::PluginInstall(format!(
"symlink failed: {e} (developer mode or admin rights may be required)"
))
})?;
Ok(target)
}
fn remove_plugin_dir(dir: &Path) -> Result<()> {
let meta = std::fs::symlink_metadata(dir)?;
if meta.file_type().is_symlink() {
#[cfg(unix)]
std::fs::remove_file(dir)?;
#[cfg(windows)]
std::fs::remove_dir(dir)?;
} else {
std::fs::remove_dir_all(dir)?;
}
Ok(())
}
pub fn uninstall(name: &str) -> Result<()> {
let dir = plugins_dir().join(name);
if !dir.exists() && std::fs::symlink_metadata(&dir).is_err() {
return Err(Error::PluginNotInstalled(name.into()));
}
remove_plugin_dir(&dir)
}
fn read_ida_config() -> serde_json::Value {
let path = ida_user_dir().join("ida-config.json");
if !path.exists() {
return serde_json::json!({});
}
std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(serde_json::json!({}))
}
fn write_ida_config(config: &serde_json::Value) -> Result<()> {
let path = ida_user_dir().join("ida-config.json");
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let text = serde_json::to_string_pretty(config)?;
std::fs::write(&path, text)?;
Ok(())
}
pub fn get_plugin_setting(plugin_name: &str, key: &str) -> Option<serde_json::Value> {
let config = read_ida_config();
config
.get("plugins")
.and_then(|p| p.get(plugin_name))
.and_then(|p| p.get("settings"))
.and_then(|s| s.get(key))
.cloned()
}
pub fn set_plugin_setting(plugin_name: &str, key: &str, value: serde_json::Value) -> Result<()> {
let mut config = read_ida_config();
let plugins = config
.as_object_mut()
.unwrap()
.entry("plugins")
.or_insert(serde_json::json!({}));
let plugin = plugins
.as_object_mut()
.unwrap()
.entry(plugin_name)
.or_insert(serde_json::json!({}));
let settings = plugin
.as_object_mut()
.unwrap()
.entry("settings")
.or_insert(serde_json::json!({}));
settings
.as_object_mut()
.unwrap()
.insert(key.to_owned(), value);
write_ida_config(&config)
}
pub fn del_plugin_setting(plugin_name: &str, key: &str) -> Result<()> {
let mut config = read_ida_config();
if let Some(settings) = config
.get_mut("plugins")
.and_then(|p| p.get_mut(plugin_name))
.and_then(|p| p.get_mut("settings"))
.and_then(|s| s.as_object_mut())
{
settings.remove(key);
write_ida_config(&config)?;
}
Ok(())
}
pub fn get_all_plugin_settings(plugin_name: &str) -> serde_json::Map<String, serde_json::Value> {
let config = read_ida_config();
config
.get("plugins")
.and_then(|p| p.get(plugin_name))
.and_then(|p| p.get("settings"))
.and_then(|s| s.as_object())
.cloned()
.unwrap_or_default()
}
pub fn read_installed_metadata(name: &str) -> Result<crate::plugin::PluginMetadata> {
let manifest_path = plugins_dir().join(name).join("ida-plugin.json");
if !manifest_path.exists() {
return Err(Error::PluginNotInstalled(name.into()));
}
let text = std::fs::read_to_string(&manifest_path)?;
let manifest: crate::plugin::PluginManifest = serde_json::from_str(&text)?;
Ok(manifest.metadata)
}
pub fn get_repo_url() -> Option<String> {
let config = read_ida_config();
config
.get("Settings")
.and_then(|s| s.get("plugin-repository"))
.and_then(|r| r.get("url"))
.and_then(|u| u.as_str())
.map(String::from)
}
pub fn detect_current_ida_version() -> Option<String> {
let install_dir = crate::ida::current_install_dir()?;
crate::ida::detect_ida_version(&install_dir)
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
pub fn upgrade_from_archive(archive_path: &Path, ida_version: Option<&str>) -> Result<PathBuf> {
let metadata = crate::plugin::metadata::read_metadata_from_archive(archive_path)?;
let target_dir = plugins_dir().join(&metadata.name);
if !target_dir.exists() {
return Err(Error::PluginNotInstalled(metadata.name.clone()));
}
let rollback_dir = target_dir.with_extension("rollback");
if rollback_dir.exists() {
std::fs::remove_dir_all(&rollback_dir)?;
}
copy_dir_recursive(&target_dir, &rollback_dir)
.map_err(|e| Error::PluginInstall(format!("rollback copy failed: {e}")))?;
std::fs::remove_dir_all(&target_dir)?;
match install_from_archive(archive_path, ida_version, true) {
Ok(path) => {
let _ = std::fs::remove_dir_all(&rollback_dir);
Ok(path)
}
Err(e) => {
let _ = std::fs::remove_dir_all(&target_dir);
let _ = std::fs::rename(&rollback_dir, &target_dir);
Err(e)
}
}
}