1use crate::error::{GhidraError, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8 pub ghidra_install_dir: Option<PathBuf>,
9 pub ghidra_project_dir: Option<PathBuf>,
10 pub default_program: Option<String>,
11 pub default_project: Option<String>,
12 pub default_output_format: Option<String>,
13 pub default_limit: Option<usize>,
14 pub timeout: Option<u64>,
15 pub aliases: std::collections::HashMap<String, String>,
16}
17
18impl Default for Config {
19 fn default() -> Self {
20 Self {
21 ghidra_install_dir: None,
22 ghidra_project_dir: None,
23 default_program: None,
24 default_project: None,
25 default_output_format: Some("auto".to_string()),
26 default_limit: Some(1000),
27 timeout: Some(300),
28 aliases: std::collections::HashMap::new(),
29 }
30 }
31}
32
33impl Config {
34 pub fn load() -> Result<Self> {
35 let config_path = Self::config_path()?;
36
37 if !config_path.exists() {
38 return Ok(Self::default());
39 }
40
41 let content = fs::read_to_string(&config_path)?;
42 let config: Config = serde_yaml::from_str(&content)?;
43
44 Ok(config)
45 }
46
47 pub fn save(&self) -> Result<()> {
48 let config_path = Self::config_path()?;
49
50 if let Some(parent) = config_path.parent() {
51 fs::create_dir_all(parent)?;
52 }
53
54 let content = serde_yaml::to_string(self)?;
55 fs::write(config_path, content)?;
56
57 Ok(())
58 }
59
60 pub fn config_path() -> Result<PathBuf> {
61 if let Ok(path) = std::env::var("GHIDRA_CLI_CONFIG") {
63 return Ok(PathBuf::from(path));
64 }
65
66 let config_dir = dirs::config_dir().ok_or_else(|| {
67 GhidraError::ConfigError("Could not determine config directory".to_string())
68 })?;
69
70 Ok(config_dir.join("ghidra-cli").join("config.yaml"))
71 }
72
73 pub fn get_ghidra_install_dir(&self) -> Result<PathBuf> {
74 if let Ok(dir) = std::env::var("GHIDRA_INSTALL_DIR") {
76 return Ok(PathBuf::from(dir));
77 }
78
79 if let Some(dir) = &self.ghidra_install_dir {
81 return Ok(dir.clone());
82 }
83
84 #[cfg(target_os = "windows")]
86 {
87 if let Some(dir) = Self::detect_ghidra_windows() {
88 return Ok(dir);
89 }
90 }
91
92 Err(GhidraError::GhidraNotFound)
93 }
94
95 pub fn get_project_dir(&self) -> Result<PathBuf> {
96 if let Ok(dir) = std::env::var("GHIDRA_PROJECT_DIR") {
98 return Ok(PathBuf::from(dir));
99 }
100
101 if let Some(dir) = &self.ghidra_project_dir {
103 return Ok(dir.clone());
104 }
105
106 let cache_dir = dirs::cache_dir().ok_or_else(|| {
108 GhidraError::ConfigError("Could not determine cache directory".to_string())
109 })?;
110
111 Ok(cache_dir.join("ghidra-cli").join("projects"))
112 }
113
114 #[cfg(target_os = "windows")]
115 pub fn detect_ghidra_windows() -> Option<PathBuf> {
116 let is_valid_ghidra =
118 |path: &PathBuf| -> bool { path.join("support").join("analyzeHeadless.bat").exists() };
119
120 let mut common_paths = vec![
122 PathBuf::from("C:\\Program Files\\Ghidra"),
123 PathBuf::from("C:\\Program Files (x86)\\Ghidra"),
124 PathBuf::from("C:\\ghidra"),
125 ];
126
127 if let Some(home) = dirs::home_dir() {
129 common_paths.push(home.join("ghidra"));
130 }
131
132 for path in common_paths {
133 if !path.exists() {
134 continue;
135 }
136
137 if is_valid_ghidra(&path) {
139 return Some(path);
140 }
141
142 if let Ok(entries) = fs::read_dir(&path) {
144 for entry in entries.flatten() {
145 let entry_path = entry.path();
146 if entry_path.is_dir() {
147 let name = entry_path.file_name()?.to_str()?;
148 if name.starts_with("ghidra_") && is_valid_ghidra(&entry_path) {
149 return Some(entry_path);
150 }
151 }
152 }
153 }
154 }
155
156 None
157 }
158
159 pub fn get_default_program(&self) -> Option<String> {
160 std::env::var("GHIDRA_DEFAULT_PROGRAM")
161 .ok()
162 .or_else(|| self.default_program.clone())
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_default_config() {
172 let config = Config::default();
173 assert_eq!(config.timeout, Some(300));
174 assert_eq!(config.default_limit, Some(1000));
175 }
176}