use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginMetadata {
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
pub entry_point: Option<String>,
pub author: Option<String>,
pub authors: Option<Vec<Contact>>,
pub license: Option<String>,
pub urls: Option<Urls>,
pub categories: Option<Vec<String>>,
pub keywords: Option<Vec<String>>,
pub ida_versions: Option<Vec<String>>,
pub platforms: Option<Vec<String>>,
pub logo_path: Option<String>,
pub python_dependencies: Option<Vec<String>>,
pub settings: Option<HashMap<String, PluginSetting>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
pub name: String,
pub email: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Urls {
pub homepage: Option<String>,
pub repository: Option<String>,
pub documentation: Option<String>,
pub issues: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginSetting {
#[serde(rename = "type")]
pub setting_type: String, pub description: Option<String>,
pub default: Option<serde_json::Value>,
pub required: Option<bool>,
pub choices: Option<Vec<String>>,
pub pattern: Option<String>,
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(flatten)]
pub metadata: PluginMetadata,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinimalPluginMetadata {
pub name: String,
pub version: Option<String>,
pub path: String,
}
pub fn is_ida_version_compatible(plugin: &PluginMetadata, ida_version: &str) -> bool {
match &plugin.ida_versions {
None => true, Some(versions) => {
if versions.is_empty() {
return true;
}
versions.iter().any(|spec| {
ida_version.starts_with(spec)
|| spec.starts_with(ida_version)
|| spec == "*"
|| spec == ">=3.0"
})
}
}
}
pub const PLATFORM_WINDOWS: &str = "win";
pub const PLATFORM_LINUX: &str = "linux";
pub const PLATFORM_MACOS_INTEL: &str = "macx64";
pub const PLATFORM_MACOS_ARM: &str = "macarm";
pub fn is_platform_compatible(plugin: &PluginMetadata) -> bool {
match &plugin.platforms {
None => true,
Some(platforms) => {
if platforms.is_empty() {
return true;
}
let current = current_platform();
platforms.iter().any(|p| p == current || p == "all")
}
}
}
fn current_platform() -> &'static str {
if cfg!(target_os = "windows") {
PLATFORM_WINDOWS
} else if cfg!(target_os = "macos") {
if cfg!(target_arch = "aarch64") {
PLATFORM_MACOS_ARM
} else {
PLATFORM_MACOS_INTEL
}
} else {
PLATFORM_LINUX
}
}
pub fn read_metadata_from_archive(
archive_path: &std::path::Path,
) -> crate::error::Result<PluginMetadata> {
let file = std::fs::File::open(archive_path)?;
let mut archive = zip::ZipArchive::new(file)?;
for i in 0..archive.len() {
let entry = archive.by_index(i)?;
let name = entry.name().to_owned();
if name == "ida-plugin.json" || name.ends_with("/ida-plugin.json") {
let depth = name.matches('/').count();
if depth <= 1 {
let manifest: PluginManifest = serde_json::from_reader(entry)?;
return Ok(manifest.metadata);
}
}
}
Err(crate::error::Error::PluginInstall(
"ida-plugin.json not found in archive".into(),
))
}
pub fn read_metadata_from_directory(
dir: &std::path::Path,
) -> crate::error::Result<PluginMetadata> {
let manifest_path = dir.join("ida-plugin.json");
if !manifest_path.is_file() {
return Err(crate::error::Error::PluginInstall(format!(
"ida-plugin.json not found in {}",
dir.display()
)));
}
let text = std::fs::read_to_string(&manifest_path)?;
let manifest: PluginManifest = serde_json::from_str(&text)?;
Ok(manifest.metadata)
}
pub fn ida_plugin_json_schema() -> serde_json::Value {
let contact = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"url": {"type": "string"}
},
"required": ["name"]
});
serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ida-plugin.json",
"description": "Metadata descriptor for IDA Pro plugins",
"type": "object",
"properties": {
"$schema": {"type": "string"},
"name": {
"type": "string",
"description": "Unique plugin name; used as the installation directory name"
},
"version": {
"type": "string",
"description": "Plugin version (semantic versioning recommended)"
},
"description": {"type": "string"},
"entryPoint": {
"type": "string",
"description": "Plugin entry point (e.g. a .py file or native library)"
},
"author": {"type": "string"},
"authors": {"type": "array", "items": contact.clone()},
"license": {"type": "string"},
"urls": {
"type": "object",
"properties": {
"homepage": {"type": "string"},
"repository": {"type": "string"},
"documentation": {"type": "string"},
"issues": {"type": "string"}
}
},
"categories": {"type": "array", "items": {"type": "string"}},
"keywords": {"type": "array", "items": {"type": "string"}},
"idaVersions": {
"type": "array",
"items": {"type": "string"},
"description": "Compatible IDA versions or version ranges"
},
"platforms": {
"type": "array",
"items": {"type": "string", "enum": ["win", "linux", "macx64", "macarm", "all"]}
},
"logoPath": {"type": "string"},
"pythonDependencies": {
"type": "array",
"items": {"type": "string"},
"description": "PEP 508 dependency specifiers"
},
"settings": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["string", "boolean"]},
"description": {"type": "string"},
"default": {},
"required": {"type": "boolean"},
"choices": {"type": "array", "items": {"type": "string"}},
"pattern": {"type": "string"},
"prompt": {"type": "string"}
},
"required": ["type"]
}
}
},
"required": ["name", "version"]
})
}