1use super::{Plugin, PluginError, PluginManager, PluginManifest, PluginResult, PluginType};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RegistryEntry {
12 pub manifest: PluginManifest,
13 pub install_path: PathBuf,
14 pub install_date: chrono::DateTime<chrono::Utc>,
15 pub last_updated: chrono::DateTime<chrono::Utc>,
16 pub enabled: bool,
17 pub auto_update: bool,
18 pub usage_count: u64,
19 pub last_used: Option<chrono::DateTime<chrono::Utc>>,
20 pub checksum: String,
21 pub metadata: HashMap<String, serde_json::Value>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RegistryConfig {
26 pub registry_path: PathBuf,
27 pub auto_discovery: bool,
28 pub auto_update_check: bool,
29 pub update_interval_hours: u64,
30 pub cleanup_unused_after_days: u64,
31 pub max_registry_entries: usize,
32}
33
34impl Default for RegistryConfig {
35 fn default() -> Self {
36 Self {
37 registry_path: dirs::config_dir()
38 .unwrap_or_default()
39 .join("voirs")
40 .join("plugin_registry.json"),
41 auto_discovery: true,
42 auto_update_check: false,
43 update_interval_hours: 24,
44 cleanup_unused_after_days: 90,
45 max_registry_entries: 1000,
46 }
47 }
48}
49
50pub struct PluginRegistry {
51 config: RegistryConfig,
52 entries: RwLock<HashMap<String, RegistryEntry>>,
53 manager: Arc<PluginManager>,
54}
55
56impl PluginRegistry {
57 pub fn new(config: RegistryConfig, manager: Arc<PluginManager>) -> Self {
58 Self {
59 config,
60 entries: RwLock::new(HashMap::new()),
61 manager,
62 }
63 }
64
65 pub async fn load(&self) -> PluginResult<()> {
66 if !self.config.registry_path.exists() {
67 self.save().await?;
69 return Ok(());
70 }
71
72 let content = tokio::fs::read_to_string(&self.config.registry_path).await?;
73 let entries: HashMap<String, RegistryEntry> = serde_json::from_str(&content)?;
74
75 {
76 let mut entries_guard = self.entries.write().await;
77 *entries_guard = entries;
78 }
79
80 Ok(())
81 }
82
83 pub async fn save(&self) -> PluginResult<()> {
84 let entries = self.entries.read().await;
85 let content = serde_json::to_string_pretty(&*entries)?;
86
87 if let Some(parent) = self.config.registry_path.parent() {
89 tokio::fs::create_dir_all(parent).await?;
90 }
91
92 tokio::fs::write(&self.config.registry_path, content).await?;
93 Ok(())
94 }
95
96 pub async fn register_plugin(
97 &self,
98 manifest: PluginManifest,
99 install_path: PathBuf,
100 ) -> PluginResult<()> {
101 let now = chrono::Utc::now();
102 let checksum = self.calculate_checksum(&install_path).await?;
103
104 let entry = RegistryEntry {
105 manifest: manifest.clone(),
106 install_path,
107 install_date: now,
108 last_updated: now,
109 enabled: true,
110 auto_update: false,
111 usage_count: 0,
112 last_used: None,
113 checksum,
114 metadata: HashMap::new(),
115 };
116
117 {
118 let mut entries_guard = self.entries.write().await;
119
120 if entries_guard.len() >= self.config.max_registry_entries {
122 return Err(PluginError::LoadingFailed(
123 "Registry is full. Please clean up unused plugins.".to_string(),
124 ));
125 }
126
127 entries_guard.insert(manifest.name.clone(), entry);
128 }
129
130 self.save().await?;
131 Ok(())
132 }
133
134 pub async fn unregister_plugin(&self, name: &str) -> PluginResult<()> {
135 {
136 let mut entries_guard = self.entries.write().await;
137 if entries_guard.remove(name).is_none() {
138 return Err(PluginError::NotFound(name.to_string()));
139 }
140 }
141
142 self.save().await?;
143 Ok(())
144 }
145
146 pub async fn get_plugin(&self, name: &str) -> Option<RegistryEntry> {
147 let entries = self.entries.read().await;
148 entries.get(name).cloned()
149 }
150
151 pub async fn list_plugins(&self) -> Vec<RegistryEntry> {
152 let entries = self.entries.read().await;
153 entries.values().cloned().collect()
154 }
155
156 pub async fn list_plugins_by_type(&self, plugin_type: PluginType) -> Vec<RegistryEntry> {
157 let entries = self.entries.read().await;
158 entries
159 .values()
160 .filter(|entry| entry.manifest.plugin_type == plugin_type)
161 .cloned()
162 .collect()
163 }
164
165 pub async fn enable_plugin(&self, name: &str) -> PluginResult<()> {
166 {
167 let mut entries_guard = self.entries.write().await;
168 if let Some(entry) = entries_guard.get_mut(name) {
169 entry.enabled = true;
170 entry.last_updated = chrono::Utc::now();
171 } else {
172 return Err(PluginError::NotFound(name.to_string()));
173 }
174 }
175
176 self.save().await?;
177 Ok(())
178 }
179
180 pub async fn disable_plugin(&self, name: &str) -> PluginResult<()> {
181 {
182 let mut entries_guard = self.entries.write().await;
183 if let Some(entry) = entries_guard.get_mut(name) {
184 entry.enabled = false;
185 entry.last_updated = chrono::Utc::now();
186 } else {
187 return Err(PluginError::NotFound(name.to_string()));
188 }
189 }
190
191 self.save().await?;
192 Ok(())
193 }
194
195 pub async fn record_usage(&self, name: &str) -> PluginResult<()> {
196 {
197 let mut entries_guard = self.entries.write().await;
198 if let Some(entry) = entries_guard.get_mut(name) {
199 entry.usage_count += 1;
200 entry.last_used = Some(chrono::Utc::now());
201 } else {
202 return Err(PluginError::NotFound(name.to_string()));
203 }
204 }
205
206 if fastrand::f32() < 0.1 {
208 self.save().await?;
209 }
210
211 Ok(())
212 }
213
214 pub async fn update_plugin_metadata(
215 &self,
216 name: &str,
217 key: &str,
218 value: serde_json::Value,
219 ) -> PluginResult<()> {
220 {
221 let mut entries_guard = self.entries.write().await;
222 if let Some(entry) = entries_guard.get_mut(name) {
223 entry.metadata.insert(key.to_string(), value);
224 entry.last_updated = chrono::Utc::now();
225 } else {
226 return Err(PluginError::NotFound(name.to_string()));
227 }
228 }
229
230 self.save().await?;
231 Ok(())
232 }
233
234 pub async fn search_plugins(&self, query: &str) -> Vec<RegistryEntry> {
235 let entries = self.entries.read().await;
236 let query_lower = query.to_lowercase();
237
238 entries
239 .values()
240 .filter(|entry| {
241 entry.manifest.name.to_lowercase().contains(&query_lower)
242 || entry
243 .manifest
244 .description
245 .to_lowercase()
246 .contains(&query_lower)
247 || entry.manifest.author.to_lowercase().contains(&query_lower)
248 })
249 .cloned()
250 .collect()
251 }
252
253 pub async fn get_enabled_plugins(&self) -> Vec<RegistryEntry> {
254 let entries = self.entries.read().await;
255 entries
256 .values()
257 .filter(|entry| entry.enabled)
258 .cloned()
259 .collect()
260 }
261
262 pub async fn get_stats(&self) -> RegistryStats {
263 let entries = self.entries.read().await;
264 let total = entries.len();
265 let enabled = entries.values().filter(|e| e.enabled).count();
266 let by_type = entries
267 .values()
268 .map(|e| e.manifest.plugin_type.clone())
269 .fold(HashMap::new(), |mut acc, t| {
270 *acc.entry(format!("{:?}", t)).or_insert(0) += 1;
271 acc
272 });
273
274 RegistryStats {
275 total_plugins: total,
276 enabled_plugins: enabled,
277 disabled_plugins: total - enabled,
278 plugins_by_type: by_type,
279 total_usage: entries.values().map(|e| e.usage_count).sum(),
280 }
281 }
282
283 pub async fn cleanup_unused(&self) -> PluginResult<Vec<String>> {
284 let cutoff_date = chrono::Utc::now()
285 - chrono::Duration::days(self.config.cleanup_unused_after_days as i64);
286 let mut removed = Vec::new();
287
288 {
289 let mut entries_guard = self.entries.write().await;
290 entries_guard.retain(|name, entry| {
291 let should_keep = entry.enabled
292 || entry
293 .last_used
294 .is_some_and(|last_used| last_used > cutoff_date)
295 || entry.usage_count > 0;
296
297 if !should_keep {
298 removed.push(name.clone());
299 }
300
301 should_keep
302 });
303 }
304
305 if !removed.is_empty() {
306 self.save().await?;
307 }
308
309 Ok(removed)
310 }
311
312 pub async fn validate_integrity(&self) -> PluginResult<Vec<String>> {
313 let mut issues = Vec::new();
314 let entries = self.entries.read().await;
315
316 for (name, entry) in entries.iter() {
317 if !entry.install_path.exists() {
319 issues.push(format!(
320 "Plugin '{}' file not found at {}",
321 name,
322 entry.install_path.display()
323 ));
324 continue;
325 }
326
327 match self.calculate_checksum(&entry.install_path).await {
329 Ok(current_checksum) => {
330 if current_checksum != entry.checksum {
331 issues.push(format!(
332 "Plugin '{}' checksum mismatch (possible corruption)",
333 name
334 ));
335 }
336 }
337 Err(e) => {
338 issues.push(format!(
339 "Plugin '{}' checksum calculation failed: {}",
340 name, e
341 ));
342 }
343 }
344
345 if entry.manifest.name.is_empty() || entry.manifest.version.is_empty() {
347 issues.push(format!("Plugin '{}' has invalid manifest", name));
348 }
349 }
350
351 Ok(issues)
352 }
353
354 async fn calculate_checksum(&self, path: &Path) -> PluginResult<String> {
355 use sha2::{Digest, Sha256};
356
357 let content = tokio::fs::read(path).await?;
358 let mut hasher = Sha256::new();
359 hasher.update(&content);
360 let result = hasher.finalize();
361 Ok(format!("{:x}", result))
362 }
363
364 pub async fn discover_and_register(&self) -> PluginResult<usize> {
365 if !self.config.auto_discovery {
366 return Ok(0);
367 }
368
369 let discovered = self.manager.discover_plugins().await?;
370 let mut registered_count = 0;
371
372 for plugin_info in discovered {
373 if !self
374 .entries
375 .read()
376 .await
377 .contains_key(&plugin_info.manifest.name)
378 {
379 self.register_plugin(plugin_info.manifest, plugin_info.path)
380 .await?;
381 registered_count += 1;
382 }
383 }
384
385 Ok(registered_count)
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct RegistryStats {
391 pub total_plugins: usize,
392 pub enabled_plugins: usize,
393 pub disabled_plugins: usize,
394 pub plugins_by_type: HashMap<String, usize>,
395 pub total_usage: u64,
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use std::sync::Arc;
402
403 fn create_test_manifest() -> PluginManifest {
404 PluginManifest {
405 name: "test-plugin".to_string(),
406 version: "1.0.0".to_string(),
407 description: "Test plugin".to_string(),
408 author: "Test Author".to_string(),
409 api_version: "1.0.0".to_string(),
410 plugin_type: PluginType::Extension,
411 entry_point: "test_plugin.dll".to_string(),
412 dependencies: vec![],
413 permissions: vec![],
414 configuration: None,
415 }
416 }
417
418 #[tokio::test]
419 async fn test_registry_creation() {
420 let config = RegistryConfig::default();
421 let manager = Arc::new(PluginManager::new());
422 let registry = PluginRegistry::new(config, manager);
423
424 let stats = registry.get_stats().await;
425 assert_eq!(stats.total_plugins, 0);
426 }
427
428 #[tokio::test]
429 async fn test_plugin_registration() {
430 let config = RegistryConfig {
431 registry_path: PathBuf::from("/tmp/test_registry.json"),
432 ..Default::default()
433 };
434 let manager = Arc::new(PluginManager::new());
435 let registry = PluginRegistry::new(config, manager);
436
437 let manifest = create_test_manifest();
438 let install_path = PathBuf::from("/tmp/test_plugin");
439
440 let result = registry.register_plugin(manifest, install_path).await;
442 assert!(result.is_err()); let stats = registry.get_stats().await;
445 assert_eq!(stats.total_plugins, 0); }
447
448 #[tokio::test]
449 async fn test_plugin_search() {
450 let config = RegistryConfig::default();
451 let manager = Arc::new(PluginManager::new());
452 let registry = PluginRegistry::new(config, manager);
453
454 let results = registry.search_plugins("test").await;
455 assert_eq!(results.len(), 0);
456 }
457
458 #[tokio::test]
459 async fn test_registry_stats() {
460 let config = RegistryConfig::default();
461 let manager = Arc::new(PluginManager::new());
462 let registry = PluginRegistry::new(config, manager);
463
464 let stats = registry.get_stats().await;
465 assert_eq!(stats.total_plugins, 0);
466 assert_eq!(stats.enabled_plugins, 0);
467 assert_eq!(stats.disabled_plugins, 0);
468 assert_eq!(stats.total_usage, 0);
469 }
470}